"""REST API routes."""

import json
from datetime import UTC, datetime
from pathlib import Path
from uuid import UUID

from fastapi import APIRouter, Depends, Query
from fastapi.responses import FileResponse
from sqlalchemy import distinct, func, select
from sqlalchemy.orm import Session, selectinload

from gnexus_creds.auth import actor_from_request, require_admin
from gnexus_creds.backup import create_backup, list_backups, restore_backup
from gnexus_creds.config import get_settings
from gnexus_creds.db import get_db
from gnexus_creds.errors import AppError
from gnexus_creds.models import ApiToken, AuditEvent, Secret, SecretTag, User
from gnexus_creds.schemas import (
    ApiTokenCreate,
    ApiTokenCreated,
    ApiTokenRead,
    AuditEventRead,
    ExportResponse,
    ExtensionBuildRead,
    ExtensionRead,
    ImportPayload,
    Page,
    Scope,
    SecretCreate,
    SecretFieldIn,
    SecretRead,
    SecretReveal,
    SecretStatus,
    SecretUpdate,
    SecretVersionRead,
    StatsRead,
    UserRead,
    UserUpdate,
)
from gnexus_creds.services import (
    Actor,
    audit,
    check_actor_rate_limit,
    create_api_token,
    create_secret,
    delete_secret,
    get_secret,
    get_version,
    list_secrets,
    list_versions,
    reveal_secret,
    serialize_secret,
    update_secret,
)

router = APIRouter(prefix="/api/v1")


def _export_secret(row: Secret, actor: Actor, db: Session) -> dict:
    revealed = serialize_secret(row, reveal=True, db=db, user=actor.user)
    return SecretCreate(
        title=revealed.title,
        purpose=revealed.purpose,
        category=revealed.category,
        source=revealed.source,
        notes=revealed.notes,
        tags=revealed.tags,
        status=revealed.status,
        archived=revealed.archived,
        allow_ui=revealed.allow_ui,
        allow_rest_api=revealed.allow_rest_api,
        allow_mcp=revealed.allow_mcp,
        fields=[
            SecretFieldIn(
                name=field.name,
                value=field.value or "",
                encrypted=field.encrypted,
                masked=field.masked,
                position=field.position,
            )
            for field in revealed.fields
        ],
    ).model_dump(mode="json")


@router.get("/me", response_model=UserRead, tags=["user"], summary="Get current user")
async def me(actor: Actor = Depends(actor_from_request)) -> UserRead:
    settings = get_settings()
    profile = actor.user.profile or {}
    return UserRead(
        id=actor.user.id,
        email=actor.user.email,
        display_name=actor.user.display_name,
        locale=actor.user.locale,
        role=actor.user.system_role,
        status=actor.user.status,
        avatar_url=profile.get("avatar_url"),
        auth_profile_url=f"{settings.auth_base_url}/account/profile",
    )


@router.patch("/me", response_model=UserRead, tags=["user"], summary="Update current user profile")
async def update_me(
    payload: UserUpdate,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> UserRead:
    actor.require(Scope.write)
    if payload.display_name is not None:
        actor.user.display_name = payload.display_name
    if payload.locale is not None:
        actor.user.locale = payload.locale
    db.commit()
    db.refresh(actor.user)
    settings = get_settings()
    profile = actor.user.profile or {}
    return UserRead(
        id=actor.user.id,
        email=actor.user.email,
        display_name=actor.user.display_name,
        locale=actor.user.locale,
        role=actor.user.system_role,
        status=actor.user.status,
        avatar_url=profile.get("avatar_url"),
        auth_profile_url=f"{settings.auth_base_url}/account/profile",
    )


@router.get("/secrets", response_model=Page[SecretRead], tags=["secrets"], summary="List secrets")
async def secrets_list(
    q: str | None = None,
    category: str | None = None,
    status: SecretStatus | None = None,
    include_archived: bool = False,
    offset: int = Query(0, ge=0),
    limit: int = Query(50, ge=1, le=200),
    sort_by: str = Query("updated_at"),
    sort_dir: str = Query("desc"),
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> Page[SecretRead]:
    items, total = list_secrets(
        db,
        actor,
        q=q,
        category=category,
        status=status,
        include_archived=include_archived,
        offset=offset,
        limit=limit,
        sort_by=sort_by,
        sort_dir=sort_dir,
    )
    return Page(
        items=[item.model_dump() for item in items], total=total, offset=offset, limit=limit
    )


@router.post("/secrets", response_model=SecretRead, tags=["secrets"], summary="Create a secret")
async def secrets_create(
    payload: SecretCreate,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> SecretRead:
    result = create_secret(db, actor, payload)
    db.commit()
    return result


@router.get(
    "/secrets/{secret_id}", response_model=SecretRead, tags=["secrets"], summary="Get a secret"
)
async def secrets_get(
    secret_id: UUID,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> SecretRead:
    return get_secret(db, actor, secret_id)


@router.patch(
    "/secrets/{secret_id}", response_model=SecretRead, tags=["secrets"], summary="Update a secret"
)
async def secrets_update(
    secret_id: UUID,
    payload: SecretUpdate,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> SecretRead:
    result = update_secret(db, actor, secret_id, payload)
    db.commit()
    return result


@router.delete(
    "/secrets/{secret_id}",
    status_code=204,
    tags=["secrets"],
    summary="Delete a secret",
)
async def secrets_delete(
    secret_id: UUID,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> None:
    delete_secret(db, actor, secret_id)
    db.commit()


@router.post(
    "/secrets/{secret_id}/reveal",
    response_model=SecretReveal,
    tags=["secrets"],
    summary="Reveal secret values",
)
async def secrets_reveal(
    secret_id: UUID,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> SecretReveal:
    result = reveal_secret(db, actor, secret_id)
    db.commit()
    return result


@router.get(
    "/secrets/{secret_id}/versions",
    response_model=list[SecretVersionRead],
    tags=["secrets"],
    summary="List secret versions",
)
async def versions_list(
    secret_id: UUID,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> list[SecretVersionRead]:
    return list_versions(db, actor, secret_id)


@router.get(
    "/secrets/{secret_id}/versions/{version_id}",
    response_model=SecretVersionRead,
    tags=["secrets"],
    summary="Get a specific version",
)
async def versions_get(
    secret_id: UUID,
    version_id: UUID,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> SecretVersionRead:
    return get_version(db, actor, secret_id, version_id)


@router.post(
    "/secrets/{secret_id}/versions/{version_id}/reveal",
    response_model=SecretReveal,
    tags=["secrets"],
    summary="Reveal a specific version",
)
async def versions_reveal(
    secret_id: UUID,
    version_id: UUID,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> SecretReveal:
    result = reveal_secret(db, actor, secret_id, version_id=version_id)
    db.commit()
    return result


@router.get("/categories", response_model=list[str], tags=["taxonomy"], summary="List categories")
async def categories(
    db: Session = Depends(get_db), actor: Actor = Depends(actor_from_request)
) -> list[str]:
    actor.require(Scope.read)
    return list(
        db.scalars(
            select(distinct(Secret.category))
            .where(Secret.user_id == actor.user.id, Secret.category.is_not(None))
            .order_by(Secret.category)
        )
    )


@router.get("/tags", response_model=list[str], tags=["taxonomy"], summary="List tags")
async def tags(
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> list[str]:
    actor.require(Scope.read)
    return list(
        db.scalars(
            select(distinct(SecretTag.name))
            .where(SecretTag.user_id == actor.user.id)
            .order_by(SecretTag.name)
        )
    )


@router.get(
    "/suggestions",
    response_model=dict[str, list[str]],
    tags=["taxonomy"],
    summary="Search suggestions",
)
async def suggestions(
    q: str = Query("", max_length=120),
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> dict[str, list[str]]:
    actor.require(Scope.read)
    like = f"%{q.lower()}%"
    category_rows = db.scalars(
        select(distinct(Secret.category))
        .where(
            Secret.user_id == actor.user.id,
            Secret.category.is_not(None),
            func.lower(Secret.category).like(like),
        )
        .limit(20)
    )
    tag_rows = db.scalars(
        select(distinct(SecretTag.name))
        .where(SecretTag.user_id == actor.user.id, func.lower(SecretTag.name).like(like))
        .order_by(SecretTag.name)
        .limit(20)
    )
    return {"categories": list(category_rows), "tags": list(tag_rows)}


@router.get(
    "/audit-events",
    response_model=Page[AuditEventRead],
    tags=["audit"],
    summary="List audit events",
)
async def audit_events(
    offset: int = Query(0, ge=0),
    limit: int = Query(50, ge=1, le=200),
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> Page[AuditEventRead]:
    actor.require(Scope.read)
    stmt = select(AuditEvent).where(AuditEvent.user_id == actor.user.id)
    total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0
    rows = db.scalars(stmt.order_by(AuditEvent.created_at.desc()).offset(offset).limit(limit))
    return Page(
        items=[AuditEventRead.model_validate(row, from_attributes=True) for row in rows],
        total=total,
        offset=offset,
        limit=limit,
    )


@router.get(
    "/secrets/{secret_id}/audit-events",
    response_model=Page[AuditEventRead],
    tags=["audit"],
    summary="List audit events for a secret",
)
async def secret_audit_events(
    secret_id: UUID,
    offset: int = Query(0, ge=0),
    limit: int = Query(50, ge=1, le=200),
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> Page[AuditEventRead]:
    actor.require(Scope.read)
    stmt = select(AuditEvent).where(
        AuditEvent.user_id == actor.user.id, AuditEvent.secret_id == secret_id
    )
    total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0
    rows = db.scalars(stmt.order_by(AuditEvent.created_at.desc()).offset(offset).limit(limit))
    return Page(
        items=[AuditEventRead.model_validate(row, from_attributes=True) for row in rows],
        total=total,
        offset=offset,
        limit=limit,
    )


@router.get(
    "/api-tokens",
    response_model=list[ApiTokenRead],
    tags=["tokens"],
    summary="List API tokens",
)
async def api_tokens_list(
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
):
    actor.require(Scope.read)
    rows = db.scalars(
        select(ApiToken)
        .where(ApiToken.user_id == actor.user.id)
        .order_by(ApiToken.created_at.desc())
    )
    return [ApiTokenRead.model_validate(row, from_attributes=True) for row in rows]


@router.post(
    "/api-tokens",
    response_model=ApiTokenCreated,
    tags=["tokens"],
    summary="Create an API token",
)
async def api_tokens_create(
    payload: ApiTokenCreate,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
):
    row, token = create_api_token(db, actor, payload)
    db.commit()
    return ApiTokenCreated(
        id=row.id,
        public_id=row.public_id,
        name=row.name,
        scopes=row.scopes,
        created_at=row.created_at,
        revoked_at=row.revoked_at,
        last_used_at=row.last_used_at,
        token=token,
    )


@router.patch(
    "/api-tokens/{token_id}/revoke",
    status_code=204,
    tags=["tokens"],
    summary="Revoke an API token",
)
async def api_tokens_revoke(
    token_id: UUID,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> None:
    actor.require(Scope.write)
    row = db.get(ApiToken, token_id)
    if row and row.user_id == actor.user.id:
        row.revoked_at = datetime.now(UTC)
        audit(db, actor, action="api_token.revoked", metadata={"name": row.name})
    db.commit()


@router.post(
    "/export", response_model=ExportResponse, tags=["data"], summary="Export all secrets"
)
async def export_data(
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> ExportResponse:
    actor.require(Scope.reveal)
    check_actor_rate_limit(db, actor, "export")
    rows = db.scalars(
        select(Secret)
        .where(Secret.user_id == actor.user.id)
        .options(selectinload(Secret.versions), selectinload(Secret.tags))
        .order_by(Secret.created_at)
    ).unique()
    exported = [_export_secret(row, actor, db) for row in rows]
    audit(db, actor, action="export.created")
    db.commit()
    return ExportResponse(
        format="gnexus-creds-export",
        version=1,
        exported_at=datetime.now(UTC),
        secrets=exported,
    )


@router.post("/import", tags=["data"], summary="Import secrets")
async def import_data(
    payload: ImportPayload,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> dict[str, int]:
    actor.require(Scope.write)
    check_actor_rate_limit(db, actor, "import")
    if payload.format != "gnexus-creds-export" or payload.version != 1:
        raise AppError("unsupported_import_format", "Unsupported import format.", status_code=400)
    created = 0
    for item in payload.secrets:
        create_secret(db, actor, item)
        created += 1
    db.commit()
    return {"created": created}


@router.delete(
    "/account-data",
    status_code=204,
    tags=["account"],
    summary="Delete all account secrets",
)
async def delete_account_data(
    db: Session = Depends(get_db), actor: Actor = Depends(actor_from_request)
) -> None:
    actor.require(Scope.write)
    db.query(Secret).filter(Secret.user_id == actor.user.id).delete(synchronize_session=False)
    audit(db, actor, action="account_data.deleted")
    db.commit()


admin_router = APIRouter(prefix="/api/v1/admin", dependencies=[Depends(require_admin)])


@admin_router.get("/users", response_model=Page[UserRead], tags=["admin"], summary="List all users")
async def admin_users(
    offset: int = Query(0, ge=0),
    limit: int = Query(50, ge=1, le=200),
    db: Session = Depends(get_db),
) -> Page[UserRead]:
    stmt = select(User)
    total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0
    rows = db.scalars(stmt.order_by(User.created_at.desc()).offset(offset).limit(limit))
    return Page(
        items=[
            UserRead(
                id=row.id,
                email=row.email,
                display_name=row.display_name,
                locale=row.locale,
                status=row.status,
                role=row.system_role,
            )
            for row in rows
        ],
        total=total,
        offset=offset,
        limit=limit,
    )


@admin_router.post("/backup", tags=["admin"], summary="Create database backup")
async def admin_backup() -> dict:
    return create_backup()


@admin_router.get("/backups", tags=["admin"], summary="List database backups")
async def admin_backups() -> list[dict]:
    return list_backups()


@admin_router.post("/restore", tags=["admin"], summary="Restore database from backup")
async def admin_restore(payload: dict) -> dict:
    result = restore_backup(payload["filename"])
    return {"restored": True, **result}


@admin_router.get("/backups/{filename}", tags=["admin"], summary="Download a backup file")
async def admin_download_backup(filename: str) -> FileResponse:
    backups = list_backups()
    match = next((b for b in backups if b["filename"] == filename), None)
    if not match:
        raise AppError("backup_not_found", "Backup file not found.", status_code=404)
    return FileResponse(match["path"], filename=filename)


@router.get("/stats", response_model=StatsRead, tags=["stats"], summary="Get user statistics")
async def stats(
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> StatsRead:
    actor.require(Scope.read)
    total = db.scalar(
        select(func.count()).where(Secret.user_id == actor.user.id)
    ) or 0
    active = db.scalar(
        select(func.count()).where(Secret.user_id == actor.user.id, Secret.archived.is_(False))
    ) or 0
    mcp = db.scalar(
        select(func.count()).where(Secret.user_id == actor.user.id, Secret.allow_mcp.is_(True))
    ) or 0
    return StatsRead(
        total_secrets=total,
        active_secrets=active,
        mcp_enabled_secrets=mcp,
    )


def _extension_dir() -> Path:
    return Path(get_settings().extension_dir)


def _load_extension_manifest() -> dict:
    manifest_path = _extension_dir() / "manifest.json"
    if not manifest_path.is_file():
        raise AppError("extension_not_found", "Extension manifest not found.", status_code=404)
    with manifest_path.open(encoding="utf-8") as fh:
        return json.load(fh)


def _extension_build_path(filename: str) -> Path:
    return _extension_dir() / "dist" / filename


@router.get(
    "/extension", response_model=ExtensionRead, tags=["extension"], summary="List extension builds"
)
async def extension_info(actor: Actor = Depends(actor_from_request)) -> ExtensionRead:
    actor.require(Scope.read)
    raw = _load_extension_manifest()
    builds: list[ExtensionBuildRead] = []
    for build in raw.get("builds", []):
        filename = build["filename"]
        path = _extension_build_path(filename)
        if not path.is_file():
            continue
        builds.append(
            ExtensionBuildRead(
                browser=build["browser"],
                filename=filename,
                sha256=build["sha256"],
                size=build.get("size", path.stat().st_size),
                download_url=f"/api/v1/extension/download/{filename}",
                released_at=raw.get("released_at"),
                notes=raw.get("notes"),
            )
        )
    return ExtensionRead(
        version=raw.get("version", ""),
        released_at=raw.get("released_at"),
        notes=raw.get("notes"),
        builds=builds,
    )


@router.get(
    "/extension/download/{filename}",
    tags=["extension"],
    summary="Download an extension build",
)
async def extension_download(
    filename: str,
    db: Session = Depends(get_db),
    actor: Actor = Depends(actor_from_request),
) -> FileResponse:
    actor.require(Scope.read)
    raw = _load_extension_manifest()
    match = next((b for b in raw.get("builds", []) if b["filename"] == filename), None)
    if match is None:
        raise AppError("not_found", "Extension build not found.", status_code=404)
    path = _extension_build_path(filename)
    if not path.is_file():
        raise AppError("not_found", "Extension build not found.", status_code=404)
    audit(
        db,
        actor,
        action="extension.downloaded",
        metadata={"browser": match["browser"], "filename": filename},
    )
    db.commit()
    return FileResponse(path, filename=filename)
