"""
Admin routes — API key lifecycle management.

POST   /admin/api-keys                    issue a new key (plain secret shown once)
GET    /admin/api-keys                    list all keys (no secrets)
GET    /admin/api-keys/{key_id}           get one key
GET    /admin/api-keys/{key_id}/secret    reveal plain secret (HMAC keys only)
POST   /admin/api-keys/{key_id}/rotate    rotate (regenerate) a key's secret in-place
PATCH  /admin/api-keys/{key_id}           revoke / update a key

All routes require an admin JWT (Caller Type 1, role=admin).
External API keys issued here are for Caller Types 2 & 3.

Key rotation policy (24 h grace):
  In-place rotation (same key_id, new secret):
    POST /admin/api-keys/{key_id}/rotate
    Subscriber only needs to update the stored secret — key_id stays the same.

  Full key replacement (new key_id + new secret):
    Issue new key via POST /admin/api-keys
    Allow 24 h for the subscriber to swap key_id + secret in their config
    Revoke old key via PATCH → is_active=false

  Emergency revocation (security incident):
    PATCH → is_active=false immediately, then issue a replacement.
"""

from __future__ import annotations

import uuid
from datetime import datetime, timezone

import structlog
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.security import (
    decrypt_api_secret,
    encrypt_api_secret,
    generate_api_secret,
    hash_api_secret_bcrypt,
)
from app.db.models import ApiKey
from app.db.session import get_db
from app.dependencies.auth import CallerContext, _write_audit_event, get_client_ip, require_role
from app.schemas.api_key import (
    ApiKeyCreate,
    ApiKeyCreated,
    ApiKeyResponse,
    ApiKeyRotated,
    ApiKeySecretResponse,
    ApiKeyUpdate,
)

log = structlog.get_logger(__name__)
router = APIRouter(prefix="/admin", tags=["admin"])

_admin_only = require_role("admin")


def _is_hmac_key(scopes: list[str]) -> bool:
    """
    True if the key is for an external HMAC subscriber (Type 2).
    False if it is an internal cron key (Type 3).

    HMAC keys need Fernet-encrypted secrets (recoverable for HMAC recompute).
    Cron keys use bcrypt (one-way — just verify, no recompute needed).
    """
    internal_scopes = {"internal:job"}
    return not set(scopes).issubset(internal_scopes)


# ===========================================================================
# POST /admin/api-keys
# ===========================================================================

@router.post("/api-keys", response_model=ApiKeyCreated, status_code=201)
async def create_api_key(
    payload: ApiKeyCreate,
    caller: CallerContext = Depends(_admin_only),
    db: AsyncSession = Depends(get_db),
) -> ApiKeyCreated:
    """
    Issue a new API key and return the plain secret ONCE.

    The plain secret is generated here, stored as either:
      Fernet-encrypted (HMAC keys)  — recoverable for server-side HMAC
      bcrypt-hashed   (cron keys)   — one-way, verify only

    The caller receives the plain secret in this response.
    It is NEVER stored in plaintext and NEVER returned again.
    The subscriber is responsible for storing it securely.
    """
    plain_secret = generate_api_secret()   # 32 bytes of entropy (256-bit)

    # Choose storage strategy based on key type
    if _is_hmac_key(payload.scopes):
        stored_secret = encrypt_api_secret(plain_secret)   # Fernet — recoverable
    else:
        stored_secret = hash_api_secret_bcrypt(plain_secret)  # bcrypt — one-way

    issuer_id: uuid.UUID | None = None
    if hasattr(caller, "sub"):
        try:
            issuer_id = uuid.UUID(caller.sub)
        except ValueError:
            pass

    key = ApiKey(
        secret_hash=stored_secret,
        service_name=payload.service_name,
        scopes=payload.scopes,
        is_active=True,
        rate_limit=payload.rate_limit,
        description=payload.description,
        expires_at=payload.expires_at,
        created_by=issuer_id,
    )
    db.add(key)
    await db.flush()  # populate key.key_id before building response

    log.info(
        "api_key_created",
        key_id=str(key.key_id),
        service=payload.service_name,
        scopes=payload.scopes,
        issuer=str(issuer_id),
    )

    return ApiKeyCreated(
        key_id=key.key_id,
        service_name=key.service_name,
        scopes=list(key.scopes or []),
        is_active=key.is_active,
        rate_limit=key.rate_limit,
        description=key.description,
        expires_at=key.expires_at,
        created_at=key.created_at,
        last_used_at=key.last_used_at,
        secret=plain_secret,   # shown ONCE — not persisted in plain form
    )


# ===========================================================================
# GET /admin/api-keys
# ===========================================================================

@router.get("/api-keys", response_model=list[ApiKeyResponse])
async def list_api_keys(
    caller: CallerContext = Depends(_admin_only),
    db: AsyncSession = Depends(get_db),
) -> list[ApiKeyResponse]:
    """List all API keys. Secrets are never included."""
    result = await db.execute(
        select(ApiKey).order_by(ApiKey.created_at.desc())
    )
    keys = result.scalars().all()
    return [ApiKeyResponse.model_validate(k) for k in keys]


# ===========================================================================
# GET /admin/api-keys/{key_id}
# ===========================================================================

@router.get("/api-keys/{key_id}", response_model=ApiKeyResponse)
async def get_api_key(
    key_id: uuid.UUID,
    caller: CallerContext = Depends(_admin_only),
    db: AsyncSession = Depends(get_db),
) -> ApiKeyResponse:
    """Fetch a single API key by ID. Secret is never included."""
    result = await db.execute(
        select(ApiKey).where(ApiKey.key_id == key_id)
    )
    key = result.scalar_one_or_none()
    if not key:
        raise HTTPException(status_code=404, detail="API key not found")
    return ApiKeyResponse.model_validate(key)


# ===========================================================================
# GET /admin/api-keys/{key_id}/secret
# ===========================================================================

@router.get("/api-keys/{key_id}/secret", response_model=ApiKeySecretResponse)
async def reveal_api_key_secret(
    key_id: uuid.UUID,
    request: Request,
    caller: CallerContext = Depends(_admin_only),
    db: AsyncSession = Depends(get_db),
) -> ApiKeySecretResponse:
    """
    Reveal the plain-text secret for an API key.

    Storage strategy determines availability:
      HMAC keys  (scopes other than 'internal:job') — secret stored Fernet-encrypted
                 → decryptable → plain secret returned here.
      Cron keys  (scope 'internal:job' only)        — secret stored as bcrypt hash
                 → one-way → 409 returned, secret is not recoverable.

    Security notes:
      • Every call is written to auth_audit_log (event: 'key_secret_revealed').
      • Use only for legitimate key recovery / subscriber on-boarding support.
      • Prefer key rotation (POST new + PATCH old) over revealing existing secrets.
      • This endpoint is admin-only and should be monitored for abuse.
    """
    result = await db.execute(
        select(ApiKey).where(ApiKey.key_id == key_id)
    )
    key = result.scalar_one_or_none()
    if not key:
        raise HTTPException(status_code=404, detail="API key not found")

    # Determine key type by attempting Fernet decryption
    plain_secret = decrypt_api_secret(key.secret_hash)

    if plain_secret is None:
        # bcrypt-hashed secret (cron key) — mathematically irreversible
        raise HTTPException(
            status_code=409,
            detail=(
                "Secret is not recoverable for this key type. "
                "Cron keys use bcrypt (one-way). "
                "Rotate the key via POST /admin/api-keys instead."
            ),
        )

    key_type = "cron" if set(key.scopes or []) == {"internal:job"} else "hmac"

    # Audit every secret reveal — security-sensitive operation
    issuer_id = getattr(caller, "sub", None)
    ip = get_client_ip(request)
    await _write_audit_event(
        db,
        event_type="key_secret_revealed",
        caller_type="user",
        user_id=issuer_id,
        key_id=str(key.key_id),
        ip_address=ip,
        metadata={
            "service_name": key.service_name,
            "key_type": key_type,
        },
    )
    log.warning(
        "key_secret_revealed",
        key_id=str(key.key_id),
        service=key.service_name,
        revealed_by=issuer_id,
        ip=ip,
    )

    return ApiKeySecretResponse(
        key_id=key.key_id,
        service_name=key.service_name,
        scopes=list(key.scopes or []),
        key_type=key_type,
        secret=plain_secret,
        revealed_at=datetime.now(timezone.utc),
    )


# ===========================================================================
# POST /admin/api-keys/{key_id}/rotate
# ===========================================================================

class _RotateRequest(BaseModel):
    reason: str | None = Field(None, max_length=256, description="Audit trail note")


@router.post("/api-keys/{key_id}/rotate", response_model=ApiKeyRotated, status_code=200)
async def rotate_api_key_secret(
    key_id: uuid.UUID,
    payload: _RotateRequest,
    request: Request,
    caller: CallerContext = Depends(_admin_only),
    db: AsyncSession = Depends(get_db),
) -> ApiKeyRotated:
    """
    Rotate (regenerate) an existing key's secret in-place.

    The key_id stays the same — the subscriber only needs to update
    their stored secret value, not their entire integration config.

    Behaviour:
      • Generates a new 32-byte random secret.
      • Re-encrypts (Fernet) or re-hashes (bcrypt) using the same storage
        strategy as the original key (determined by scopes).
      • Overwrites secret_hash on the existing row atomically.
      • Returns the new plain secret ONCE — store it immediately.
      • Writes a 'key_rotated' event to auth_audit_log.

    Use for:
      • Scheduled credential hygiene (e.g. rotate every 90 days).
      • Post-incident rotation when the secret may have been exposed.
      • Subscriber request to reset their credentials.

    After rotation the old secret is immediately invalid — any in-flight
    requests signed with the old secret will fail on the next DB lookup.
    """
    result = await db.execute(
        select(ApiKey).where(ApiKey.key_id == key_id)
    )
    key = result.scalar_one_or_none()
    if not key:
        raise HTTPException(status_code=404, detail="API key not found")

    if not key.is_active:
        raise HTTPException(
            status_code=409,
            detail="Cannot rotate a revoked key. Reactivate it first or issue a new key.",
        )

    new_plain_secret = generate_api_secret()

    if _is_hmac_key(list(key.scopes or [])):
        key.secret_hash = encrypt_api_secret(new_plain_secret)     # type: ignore[assignment]
        key_type = "hmac"
    else:
        key.secret_hash = hash_api_secret_bcrypt(new_plain_secret)  # type: ignore[assignment]
        key_type = "cron"

    rotated_at = datetime.now(timezone.utc)
    issuer_id = getattr(caller, "sub", None)
    ip = get_client_ip(request)

    await _write_audit_event(
        db,
        event_type="key_rotated",
        caller_type="user",
        user_id=issuer_id,
        key_id=str(key.key_id),
        ip_address=ip,
        metadata={
            "service_name": key.service_name,
            "key_type": key_type,
            "reason": payload.reason,
        },
    )
    log.info(
        "api_key_rotated",
        key_id=str(key.key_id),
        service=key.service_name,
        key_type=key_type,
        rotated_by=issuer_id,
        reason=payload.reason,
        ip=ip,
    )

    await db.flush()
    return ApiKeyRotated(
        key_id=key.key_id,
        service_name=key.service_name,
        scopes=list(key.scopes or []),
        is_active=key.is_active,
        rate_limit=key.rate_limit,
        description=key.description,
        expires_at=key.expires_at,
        created_at=key.created_at,
        last_used_at=key.last_used_at,
        new_secret=new_plain_secret,
        rotated_at=rotated_at,
        rotation_reason=payload.reason,
    )


# ===========================================================================
# PATCH /admin/api-keys/{key_id}
# ===========================================================================

@router.patch("/api-keys/{key_id}", response_model=ApiKeyResponse)
async def update_api_key(
    key_id: uuid.UUID,
    payload: ApiKeyUpdate,
    caller: CallerContext = Depends(_admin_only),
    db: AsyncSession = Depends(get_db),
) -> ApiKeyResponse:
    """
    Revoke (or re-activate) an API key.

    For key rotation:
      1. Create a new key with POST /admin/api-keys
      2. Allow 24 h for the subscriber to update their config
      3. Revoke the old key with PATCH → is_active=false

    Emergency revocation (security incident):
      Set is_active=false immediately — effective on next request.
      The subscriber's active WebSocket connections will drop at next heartbeat (30s).
    """
    result = await db.execute(
        select(ApiKey).where(ApiKey.key_id == key_id)
    )
    key = result.scalar_one_or_none()
    if not key:
        raise HTTPException(status_code=404, detail="API key not found")

    key.is_active = payload.is_active  # type: ignore[assignment]

    action = "revoked" if not payload.is_active else "reactivated"
    log.info(
        "api_key_updated",
        key_id=str(key_id),
        action=action,
        reason=payload.reason,
        issuer=getattr(caller, "sub", "unknown"),
    )

    await db.flush()
    return ApiKeyResponse.model_validate(key)
