"""Nexus — Auth API Routes (Login / TOTP / JWT / Refresh / Logout)
Presentation layer — receives HTTP requests, delegates to AuthService.

A2: TOTP endpoints now require JWT authentication via get_current_admin.
Security: Refresh token stored in HttpOnly + Secure + SameSite=Lax cookie.
"""

import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
import bcrypt

from server.api.dependencies import get_auth_service, get_db
from server.api.auth_jwt import get_current_admin
from server.application.services.auth_service import AuthService, JWT_REFRESH_TOKEN_EXPIRE_DAYS
from server.domain.models import Admin, AuditLog

router = APIRouter(prefix="/api/auth", tags=["auth"])

logger = logging.getLogger("nexus.auth")

# ── Refresh Token Cookie Helpers ──

REFRESH_COOKIE_NAME = "nexus_refresh"
REFRESH_COOKIE_PATH = "/"


def _is_secure_request(request: Request) -> bool:
    """Determine if the request is over HTTPS (direct or via reverse proxy)."""
    forwarded_proto = request.headers.get("X-Forwarded-Proto", "").lower()
    if forwarded_proto == "https":
        return True
    return request.url.scheme == "https"


def _set_refresh_cookie(response: Response, refresh_token: str, request: Request):
    """Set HttpOnly cookie for refresh token.

    - HttpOnly: not accessible from JavaScript (XSS protection)
    - Secure: only sent over HTTPS
    - SameSite=Lax: not sent on cross-site POST (CSRF protection)
    - Path=/: sent to all paths (needed by AppAuthMiddleware on /app/ pages)
    """
    response.set_cookie(
        key=REFRESH_COOKIE_NAME,
        value=refresh_token,
        max_age=JWT_REFRESH_TOKEN_EXPIRE_DAYS * 86400,
        httponly=True,
        secure=_is_secure_request(request),
        samesite="lax",
        path=REFRESH_COOKIE_PATH,
    )


def _clear_refresh_cookie(response: Response, request: Request):
    """Clear the refresh token cookie. Path and flags must match the original set_cookie."""
    response.delete_cookie(
        key=REFRESH_COOKIE_NAME,
        path=REFRESH_COOKIE_PATH,
        secure=_is_secure_request(request),
        httponly=True,
        samesite="lax",
    )


# ── Request Models ──

class LoginRequest(BaseModel):
    username: str = Field(..., min_length=1, max_length=100)
    password: str = Field(..., min_length=1, max_length=255)
    totp_code: Optional[str] = Field(None, min_length=6, max_length=6)


class RefreshRequest(BaseModel):
    """Body optional — refresh token is read from HttpOnly cookie first."""

    refresh_token: Optional[str] = Field(default=None, min_length=1)


class LogoutRequest(BaseModel):
    refresh_token: Optional[str] = None


class TotpSetupRequest(BaseModel):
    admin_id: int


class TotpDisableRequest(BaseModel):
    admin_id: int
    current_password: str = Field(..., min_length=1, max_length=255)
    totp_code: Optional[str] = Field(None, min_length=6, max_length=6)


class TotpVerifyRequest(BaseModel):
    admin_id: int
    totp_code: str = Field(..., min_length=6, max_length=6)


class ChangePasswordRequest(BaseModel):
    current_password: str = Field(..., min_length=1, max_length=255)
    new_password: str = Field(..., min_length=6, max_length=255)
    totp_code: Optional[str] = Field(None, min_length=6, max_length=6)  # S-09: required if TOTP enabled


class WebsshTokenRequest(BaseModel):
    server_id: int = Field(..., ge=1)


# ── Public Routes (no JWT required) ──

@router.post("/login")
async def login(
    request: Request,
    response: Response,
    payload: LoginRequest,
    service: AuthService = Depends(get_auth_service),
):
    """Authenticate admin user (password + optional TOTP) → return JWT tokens

    Security: Refresh token is set as HttpOnly cookie (not in JSON body).
    Access token is returned in JSON for client-side Bearer auth.
    """
    ip_address = request.client.host if request.client else "unknown"
    result = await service.login(
        username=payload.username,
        password=payload.password,
        ip_address=ip_address,
        totp_code=payload.totp_code,
    )
    if not result["success"]:
        status_code = 401
        if result.get("reason") == "account_locked":
            status_code = 429
        elif result.get("reason") == "totp_required":
            status_code = 202  # Accepted but needs TOTP
        raise HTTPException(status_code=status_code, detail=result.get("message", "Login failed"))

    # Set refresh token as HttpOnly cookie (not accessible from JS)
    _set_refresh_cookie(response, result["refresh_token"], request)

    # Return access token + admin info (NOT refresh_token — it's in the cookie)
    return {
        "success": True,
        "access_token": result["access_token"],
        "token_type": "bearer",
        "expires_in": result["expires_in"],
        "admin": result["admin"],
    }


@router.post("/refresh")
async def refresh_token(
    request: Request,
    response: Response,
    payload: RefreshRequest | None = None,
    service: AuthService = Depends(get_auth_service),
):
    """Exchange refresh token for new access token (with rotation + version-based reuse detection)

    Security: Refresh token read from HttpOnly cookie (preferred) or request body (fallback).
    SameSite=Lax cookie prevents CSRF on cross-site POST requests.
    """
    # Read refresh token from cookie (primary) or request body (fallback)
    refresh_token = request.cookies.get(REFRESH_COOKIE_NAME)
    if not refresh_token and payload and payload.refresh_token:
        refresh_token = payload.refresh_token

    if not refresh_token:
        raise HTTPException(status_code=401, detail="Missing refresh token")

    ip_address = request.client.host if request.client else ""
    result = await service.refresh_token(refresh_token, ip_address=ip_address)
    if not result["success"]:
        # Clear the cookie on failure (invalid/expired token)
        _clear_refresh_cookie(response, request)
        raise HTTPException(status_code=401, detail=result.get("message", "Invalid refresh token"))

    # Rotate: set new refresh token cookie
    _set_refresh_cookie(response, result["refresh_token"], request)

    return {
        "success": True,
        "access_token": result["access_token"],
        "token_type": "bearer",
        "expires_in": result["expires_in"],
    }


@router.post("/logout")
async def logout(
    request: Request,
    response: Response,
    service: AuthService = Depends(get_auth_service),
):
    """Invalidate refresh token (read from HttpOnly cookie) and clear cookie.

    Security: No request body needed — refresh token comes from cookie.
    """
    refresh_token = request.cookies.get(REFRESH_COOKIE_NAME)
    if refresh_token:
        await service.logout_by_token(refresh_token)

    # Always clear the cookie (even if not found, for defense in depth)
    _clear_refresh_cookie(response, request)
    return {"success": True, "message": "已登出"}


# ── Protected Routes (JWT required) ──

@router.post("/totp/setup")
async def setup_totp(
    payload: TotpSetupRequest,
    admin: Admin = Depends(get_current_admin),
    service: AuthService = Depends(get_auth_service),
):
    """Generate TOTP secret for admin user (requires JWT auth)

    A2: JWT authentication required — admin can only setup TOTP for themselves.
    """
    # Security: only allow setting up TOTP for the authenticated admin
    if payload.admin_id != admin.id:
        raise HTTPException(status_code=403, detail="Can only setup TOTP for yourself")

    result = await service.setup_totp(admin.id)
    if not result["success"]:
        raise HTTPException(status_code=400, detail=result.get("reason", "Setup failed"))
    return result


@router.post("/totp/enable")
async def enable_totp(
    payload: TotpVerifyRequest,
    admin: Admin = Depends(get_current_admin),
    service: AuthService = Depends(get_auth_service),
):
    """Enable TOTP after verification (requires JWT auth)

    A2: JWT authentication required — admin can only enable TOTP for themselves.
    """
    if payload.admin_id != admin.id:
        raise HTTPException(status_code=403, detail="Can only enable TOTP for yourself")

    result = await service.enable_totp(admin.id, payload.totp_code)
    if not result["success"]:
        raise HTTPException(status_code=400, detail=result.get("message", "Enable failed"))
    return result


@router.post("/totp/disable")
async def disable_totp(
    payload: TotpDisableRequest,
    admin: Admin = Depends(get_current_admin),
    service: AuthService = Depends(get_auth_service),
):
    """Disable TOTP — requires current password + TOTP code when enabled."""
    if payload.admin_id != admin.id:
        raise HTTPException(status_code=403, detail="Can only disable TOTP for yourself")

    result = await service.disable_totp(
        admin.id,
        current_password=payload.current_password,
        totp_code=payload.totp_code,
    )
    if not result.get("success"):
        raise HTTPException(status_code=400, detail=result.get("message", "Disable failed"))
    return result


@router.put("/password")
async def change_password(
    request: Request,
    payload: ChangePasswordRequest,
    admin: Admin = Depends(get_current_admin),
    db: AsyncSession = Depends(get_db),
):
    """Change the current admin's password (requires current password verification)"""
    from server.infrastructure.database.admin_repo import AdminRepositoryImpl
    from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl

    admin_repo = AdminRepositoryImpl(db)
    current_admin = await admin_repo.get_by_id(admin.id)
    if not current_admin:
        raise HTTPException(status_code=404, detail="Admin not found")

    # Verify current password
    if not bcrypt.checkpw(payload.current_password.encode(), current_admin.password_hash.encode()):
        raise HTTPException(status_code=400, detail="当前密码错误")

    # S-09: If TOTP is enabled, require TOTP code for password change
    if current_admin.totp_enabled:
        if not payload.totp_code:
            raise HTTPException(status_code=400, detail="已启用 TOTP 的账户修改密码需要验证码")
        from server.application.services.auth_service import AuthService
        if not AuthService._verify_totp(payload.totp_code, current_admin.totp_secret):
            raise HTTPException(status_code=400, detail="TOTP 验证码错误")

    # Hash new password
    new_hash = bcrypt.hashpw(payload.new_password.encode(), bcrypt.gensalt()).decode()

    current_admin.password_hash = new_hash
    # Security: invalidate all existing sessions (refresh tokens + JWT token_version)
    current_admin.token_version = (current_admin.token_version or 0) + 1
    await admin_repo.update(current_admin)

    # Clear all refresh tokens from Redis (all devices logged out)
    try:
        from server.infrastructure.redis.client import get_redis
        redis = get_redis()
        await redis.delete(f"refresh_tokens:{admin.id}")
    except Exception:
        logger.warning(f"Failed to clear refresh tokens from Redis for admin {admin.id}", exc_info=True)

    # Audit log with client IP
    ip_address = request.client.host if request.client else ""
    audit_repo = AuditLogRepositoryImpl(db)
    await audit_repo.create(AuditLog(
        admin_username=admin.username,
        action="change_password",
        target_type="admin",
        target_id=admin.id,
        detail=f"{admin.username} 修改密码（所有会话已失效）",
        ip_address=ip_address,
    ))

    return {"success": True, "message": "密码已修改，请重新登录"}


@router.post("/webssh-token")
async def issue_webssh_token(
    payload: WebsshTokenRequest,
    request: Request,
    admin: Admin = Depends(get_current_admin),
    service: AuthService = Depends(get_auth_service),
    db: AsyncSession = Depends(get_db),
):
    """Issue a short-lived JWT bound to one server for WebSSH (P0 IDOR fix)."""
    from server.infrastructure.database.server_repo import ServerRepositoryImpl

    server_repo = ServerRepositoryImpl(db)
    server = await server_repo.get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="Server not found")
    if not server.domain:
        raise HTTPException(status_code=400, detail="Server has no domain configured")

    result = await service.create_webssh_token(admin, payload.server_id)
    return {**result, "server_name": server.name}


@router.get("/me")
async def get_me(
    admin: Admin = Depends(get_current_admin),
    db: AsyncSession = Depends(get_db),
):
    """Get current authenticated admin info (JWT required)"""
    # Include system_name from settings for frontend branding
    system_name = None
    try:
        from server.infrastructure.database.setting_repo import SettingRepositoryImpl
        repo = SettingRepositoryImpl(db)
        system_name = await repo.get("system_name")
    except Exception as e:
        logger.warning(f"Failed to load system_name in /me: {e}")

    return {
        "id": admin.id,
        "username": admin.username,
        "email": admin.email,
        "totp_enabled": admin.totp_enabled,
        "is_active": admin.is_active,
        "last_login": str(admin.last_login) if admin.last_login else None,
        "system_name": system_name,
    }