import logging
from contextlib import suppress
from datetime import datetime, timedelta
from uuid import UUID

from fastapi import (
    Body,
    Cookie,
    Depends,
    FastAPI,
    HTTPException,
    Query,
    Request,
    Response,
)
from fastapi.responses import JSONResponse
from fastapi.security import HTTPBearer

from passkey.util import frontend

from .. import aaguid
from ..authsession import (
    EXPIRES,
    delete_credential,
    expires,
    get_reset,
    get_session,
    refresh_session_token,
)
from ..globals import db
from ..globals import passkey as global_passkey
from ..util import hostutil, passphrase, permutil, tokens
from ..util.tokens import session_key
from . import authz, session

bearer_auth = HTTPBearer(auto_error=True)

app = FastAPI()

# Refresh only if at least this much of the session lifetime has been *consumed*.
# Consumption is derived from (now + EXPIRES) - current_expires.
# This guarantees a minimum spacing between DB writes even with frequent /validate calls.
_REFRESH_INTERVAL = timedelta(minutes=5)


@app.exception_handler(ValueError)
async def value_error_handler(_request: Request, exc: ValueError):
    return JSONResponse(status_code=400, content={"detail": str(exc)})


@app.exception_handler(Exception)
async def general_exception_handler(_request: Request, exc: Exception):
    logging.exception("Unhandled exception in API app")
    return JSONResponse(status_code=500, content={"detail": "Internal server error"})


@app.post("/validate")
async def validate_token(
    response: Response, perm: list[str] = Query([]), auth=Cookie(None)
):
    """Validate the current session and extend its expiry.

    Always refreshes the session (sliding expiration) and re-sets the cookie with a
    renewed max-age. This keeps active users logged in without needing a separate
    refresh endpoint.
    """
    ctx = await authz.verify(auth, perm)
    renewed = False
    if auth:
        consumed = EXPIRES - (ctx.session.expires - datetime.now())
        if not timedelta(0) < consumed < _REFRESH_INTERVAL:
            try:
                await refresh_session_token(auth)
                session.set_session_cookie(response, auth)
                renewed = True
            except ValueError:
                # Session disappeared, e.g. due to concurrent logout
                raise HTTPException(status_code=401, detail="Session expired")
    return {
        "valid": True,
        "user_uuid": str(ctx.session.user_uuid),
        "renewed": renewed,
    }


@app.get("/forward")
async def forward_authentication(perm: list[str] = Query([]), auth=Cookie(None)):
    """Forward auth validation for Caddy/Nginx.

    Query Params:
    - perm: repeated permission IDs the authenticated user must possess (ALL required).

    Success: 204 No Content with Remote-* headers describing the authenticated user.
    Failure (unauthenticated / unauthorized): 4xx JSON body with detail.
    """
    try:
        ctx = await authz.verify(auth, perm)
        role_permissions = set(ctx.role.permissions or [])
        if ctx.permissions:
            role_permissions.update(permission.id for permission in ctx.permissions)

        remote_headers: dict[str, str] = {
            "Remote-User": str(ctx.user.uuid),
            "Remote-Name": ctx.user.display_name,
            "Remote-Groups": ",".join(sorted(role_permissions)),
            "Remote-Org": str(ctx.org.uuid),
            "Remote-Org-Name": ctx.org.display_name,
            "Remote-Role": str(ctx.role.uuid),
            "Remote-Role-Name": ctx.role.display_name,
            "Remote-Session-Expires": ctx.session.expires.isoformat(),
            "Remote-Credential": str(ctx.session.credential_uuid),
        }
        return Response(status_code=204, headers=remote_headers)
    except HTTPException as e:
        html = frontend.file("restricted", "index.html").read_bytes()
        return Response(html, status_code=e.status_code, media_type="text/html")


@app.get("/settings")
async def get_settings():
    pk = global_passkey.instance
    base_path = hostutil.ui_base_path()
    return {
        "rp_id": pk.rp_id,
        "rp_name": pk.rp_name,
        "ui_base_path": base_path,
        "auth_host": hostutil.configured_auth_host(),
    }


@app.post("/user-info")
async def api_user_info(reset: str | None = None, auth=Cookie(None)):
    authenticated = False
    try:
        if reset:
            if not passphrase.is_well_formed(reset):
                raise ValueError("Invalid reset token")
            s = await get_reset(reset)
        else:
            if auth is None:
                raise ValueError("Authentication Required")
            s = await get_session(auth)
            authenticated = True
    except ValueError as e:
        raise HTTPException(401, str(e))

    u = await db.instance.get_user_by_uuid(s.user_uuid)

    if not authenticated:  # minimal response for reset tokens
        return {
            "authenticated": False,
            "session_type": s.info.get("type"),
            "user": {"user_uuid": str(u.uuid), "user_name": u.display_name},
        }

    assert authenticated and auth is not None

    ctx = await permutil.session_context(auth)
    credential_ids = await db.instance.get_credentials_by_user_uuid(s.user_uuid)
    credentials: list[dict] = []
    user_aaguids: set[str] = set()
    for cred_id in credential_ids:
        try:
            c = await db.instance.get_credential_by_id(cred_id)
        except ValueError:
            continue
        aaguid_str = str(c.aaguid)
        user_aaguids.add(aaguid_str)
        credentials.append(
            {
                "credential_uuid": str(c.uuid),
                "aaguid": aaguid_str,
                "created_at": c.created_at.isoformat(),
                "last_used": c.last_used.isoformat() if c.last_used else None,
                "last_verified": c.last_verified.isoformat()
                if c.last_verified
                else None,
                "sign_count": c.sign_count,
                "is_current_session": s.credential_uuid == c.uuid,
            }
        )
    credentials.sort(key=lambda cred: cred["created_at"])
    aaguid_info = aaguid.filter(user_aaguids)

    role_info = None
    org_info = None
    effective_permissions: list[str] = []
    is_global_admin = False
    is_org_admin = False
    if ctx:
        role_info = {
            "uuid": str(ctx.role.uuid),
            "display_name": ctx.role.display_name,
            "permissions": ctx.role.permissions,
        }
        org_info = {
            "uuid": str(ctx.org.uuid),
            "display_name": ctx.org.display_name,
            "permissions": ctx.org.permissions,
        }
        effective_permissions = [p.id for p in (ctx.permissions or [])]
        is_global_admin = "auth:admin" in (role_info["permissions"] or [])
        is_org_admin = any(
            p.startswith("auth:org:") for p in (role_info["permissions"] or [])
        )

    return {
        "authenticated": True,
        "session_type": s.info.get("type"),
        "user": {
            "user_uuid": str(u.uuid),
            "user_name": u.display_name,
            "created_at": u.created_at.isoformat() if u.created_at else None,
            "last_seen": u.last_seen.isoformat() if u.last_seen else None,
            "visits": u.visits,
        },
        "org": org_info,
        "role": role_info,
        "permissions": effective_permissions,
        "is_global_admin": is_global_admin,
        "is_org_admin": is_org_admin,
        "credentials": credentials,
        "aaguid_info": aaguid_info,
    }


@app.put("/user/display-name")
async def user_update_display_name(payload: dict = Body(...), auth=Cookie(None)):
    if not auth:
        raise HTTPException(status_code=401, detail="Authentication Required")
    s = await get_session(auth)
    new_name = (payload.get("display_name") or "").strip()
    if not new_name:
        raise HTTPException(status_code=400, detail="display_name required")
    if len(new_name) > 64:
        raise HTTPException(status_code=400, detail="display_name too long")
    await db.instance.update_user_display_name(s.user_uuid, new_name)
    return {"status": "ok"}


@app.post("/logout")
async def api_logout(response: Response, auth=Cookie(None)):
    if not auth:
        return {"message": "Already logged out"}
    with suppress(Exception):
        await db.instance.delete_session(session_key(auth))
    response.delete_cookie("auth")
    return {"message": "Logged out successfully"}


@app.post("/set-session")
async def api_set_session(response: Response, auth=Depends(bearer_auth)):
    user = await get_session(auth.credentials)
    session.set_session_cookie(response, auth.credentials)
    return {
        "message": "Session cookie set successfully",
        "user_uuid": str(user.user_uuid),
    }


@app.delete("/credential/{uuid}")
async def api_delete_credential(uuid: UUID, auth: str = Cookie(None)):
    await delete_credential(uuid, auth)
    return {"message": "Credential deleted successfully"}


@app.post("/create-link")
async def api_create_link(request: Request, auth=Cookie(None)):
    s = await get_session(auth)
    token = passphrase.generate()
    await db.instance.create_session(
        user_uuid=s.user_uuid,
        key=tokens.reset_key(token),
        expires=expires(),
        info=session.infodict(request, "device addition"),
    )
    url = hostutil.reset_link_url(
        token, request.url.scheme, request.headers.get("host")
    )
    return {
        "message": "Registration link generated successfully",
        "url": url,
        "expires": expires().isoformat(),
    }
