"""Dashboard API routes — push, projects, scans, findings.

All endpoints authenticated via API key (X-API-Key header).
"""

from __future__ import annotations

import logging
import re
import secrets
from datetime import UTC, datetime
from typing import Any

from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, ConfigDict

from backend.api.middleware.api_key_auth import APIKeyContext, require_api_key

_log = logging.getLogger(__name__)

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

_PROJECT_LABEL_RE = re.compile(r"^[a-z0-9-]{1,63}$")
_auth_dep = Depends(require_api_key)


# ---------------------------------------------------------------------------
# Request / response models
# ---------------------------------------------------------------------------


class DashboardPushRequest(BaseModel):
    """Request body for POST /v1/dashboard/push."""

    model_config = ConfigDict(strict=True)

    project_label: str
    client_label: str | None = None
    cloud: str
    frameworks: list[str]
    scan_source: str
    score: float
    passed: int
    failed: int
    manual_review: int
    not_assessed: int
    severity_counts: dict[str, int]
    resource_count: int
    assessment_duration_ms: int
    findings: list[dict[str, Any]]
    state_hash: str | None = None
    cloud_intelligence: dict[str, Any] | None = None


# ---------------------------------------------------------------------------
# POST /v1/dashboard/push
# ---------------------------------------------------------------------------


@router.post("/push")
async def dashboard_push(
    request: Request,
    body: DashboardPushRequest,
    auth: APIKeyContext = _auth_dep,
) -> JSONResponse:
    """Accept scan results from CLI, store findings in GCS, update project."""
    from backend.shared.firestore import get_db
    from backend.shared.gcs import upload_findings

    # 1. Validate project_label
    if not _PROJECT_LABEL_RE.match(body.project_label):
        return JSONResponse(
            {"error": "Invalid project_label. Must match [a-z0-9-]{1,63}."},
            status_code=400,
        )

    # 2. Check tier includes hosted_dashboard
    if "hosted_dashboard" not in auth.features:
        return JSONResponse(
            {
                "error": (
                    f"Feature 'hosted_dashboard' is not available on {auth.tier} tier."
                ),
            },
            status_code=403,
        )

    customer_id = auth.customer_id
    project_id = body.project_label
    db = get_db()
    project_ref = (
        db.collection("customers")
        .document(customer_id)
        .collection("projects")
        .document(project_id)
    )

    # 3. Check project limit (pre-transaction check for new projects)
    project_doc = await project_ref.get()
    if not project_doc.exists and auth.project_limit is not None:
        projects_query = (
            db.collection("customers").document(customer_id).collection("projects")
        )
        existing = [doc async for doc in projects_query.stream()]
        if len(existing) >= auth.project_limit:
            return JSONResponse(
                {
                    "error": (
                        f"Your {auth.tier} tier allows"
                        f" {auth.project_limit}"
                        " project(s). Limit reached."
                    ),
                },
                status_code=409,
            )

    # 4. Check if project is suspended
    if project_doc.exists:
        proj_data: dict[str, Any] = project_doc.to_dict()  # type: ignore[assignment]
        if proj_data.get("status") == "suspended":
            return JSONResponse(
                {
                    "error": (
                        f"Project '{body.project_label}' is suspended"
                        f" on the {auth.tier} tier due to an unpaid"
                        " overage invoice."
                    ),
                },
                status_code=403,
            )

    # 5. Upload findings to GCS
    scan_id = secrets.token_hex(16)
    findings_uri = await upload_findings(
        customer_id=customer_id,
        project_id=project_id,
        scan_id=scan_id,
        findings=body.findings,
    )

    # 6-7. Firestore transaction — atomic score_delta computation
    now = datetime.now(UTC)

    @db.transactional
    async def push_transaction(transaction: Any) -> dict[str, Any]:
        snapshot = await project_ref.get(transaction=transaction)
        previous_score: float | None = None
        if snapshot.exists:
            snap_data: dict[str, Any] = snapshot.to_dict()  # type: ignore[assignment]
            latest = snap_data.get("latest_scan", {})
            previous_score = latest.get("score") if latest else None

        score_delta: float | None = None
        if previous_score is not None:
            score_delta = body.score - previous_score

        project_update: dict[str, Any] = {
            "project_label": body.project_label,
            "client_label": body.client_label,
            "cloud": body.cloud,
            "frameworks": body.frameworks,
            "state_source": body.scan_source,
            "last_score": body.score,
            "status": "active",
            "score_delta": score_delta,
            "latest_scan": {
                "score": body.score,
                "passed": body.passed,
                "failed": body.failed,
                "findings_uri": findings_uri,
                "findings_count": len(body.findings),
                "timestamp": now.isoformat(),
                "cloud_intelligence": body.cloud_intelligence,
            },
            "last_scan": now.isoformat(),
            "severity_counts": body.severity_counts,
            "resource_count": body.resource_count,
        }

        if snapshot.exists:
            snap_dict = snapshot.to_dict() or {}
            if snap_dict.get("dormant_since"):
                project_update["dormant_since"] = None

        transaction.set(project_ref, project_update, merge=True)

        scan_ref = project_ref.collection("scan_history").document(scan_id)
        transaction.set(
            scan_ref,
            {
                "scan_id": scan_id,
                "score": body.score,
                "passed": body.passed,
                "failed": body.failed,
                "timestamp": now.isoformat(),
                "scan_source": body.scan_source,
                "cloud_intelligence": body.cloud_intelligence,
            },
        )

        return {
            "scan_id": scan_id,
            "project_id": project_id,
            "score_delta": score_delta,
        }

    try:
        transaction = db.transaction()
        result = await push_transaction(transaction)
    except Exception as exc:
        _log.exception(
            "Push transaction failed for %s/%s",
            customer_id,
            project_id,
        )
        if "contention" in str(exc).lower() or "aborted" in str(exc).lower():
            return JSONResponse(
                {"error": "Concurrent push conflict. Please retry."},
                status_code=409,
            )
        raise

    # Audit: dashboard.scan_pushed (non-fatal)
    try:
        import hashlib

        from backend.dashboard.audit import write_audit_event
        from backend.shared.tier_config import TIER_CONFIG

        _host = request.client.host if request.client else "unknown"
        _ip_hash = hashlib.sha256(_host.encode()).hexdigest()
        _retention = TIER_CONFIG.get(auth.tier, {}).get("retention_months")
        await write_audit_event(
            customer_id=auth.customer_id,
            event="dashboard.scan_pushed",
            actor_email="api_key",
            actor_role="api",
            ip_hash=_ip_hash,
            metadata={
                "project_label": body.project_label,
                "score": str(body.score),
            },
            retention_months=_retention,
        )
    except Exception:
        _log.exception("Failed to write scan_pushed audit event")

    return JSONResponse(result, status_code=200)


# ---------------------------------------------------------------------------
# GET /v1/dashboard/projects
# ---------------------------------------------------------------------------


@router.get("/projects")
async def list_projects(
    auth: APIKeyContext = _auth_dep,
) -> JSONResponse:
    """List project summaries for authenticated customer."""
    from backend.shared.firestore import get_db

    db = get_db()
    projects_ref = (
        db.collection("customers").document(auth.customer_id).collection("projects")
    )
    docs = [doc async for doc in projects_ref.stream()]

    projects: list[dict[str, Any]] = []
    for doc in docs:
        data: dict[str, Any] = doc.to_dict()  # type: ignore[assignment]
        status = data.get("status", "")
        if status not in ("active", "dormant"):
            continue

        latest = data.get("latest_scan", {})
        projects.append(
            {
                "project_id": doc.id,
                "project_label": data.get("project_label", ""),
                "client_label": data.get("client_label"),
                "cloud": data.get("cloud", ""),
                "frameworks": data.get("frameworks", []),
                "status": status,
                "score": latest.get("score"),
                "score_delta": data.get("score_delta"),
                "passed": latest.get("passed", 0),
                "failed": latest.get("failed", 0),
                "severity_counts": data.get("severity_counts", {}),
                "last_scan_at": data.get("last_scan"),
                "drift_monitoring": data.get("drift_monitoring", False),
                "drift_mode": data.get("drift_mode"),
            }
        )

    return JSONResponse(projects, status_code=200)


# ---------------------------------------------------------------------------
# GET /v1/dashboard/projects/{project_id}/scans
# ---------------------------------------------------------------------------


@router.get("/projects/{project_id}/scans")
async def list_scans(
    project_id: str,
    auth: APIKeyContext = _auth_dep,
    after: str | None = None,
) -> JSONResponse:
    """Paginated scan history for a project (DESC, limit 25)."""
    from backend.shared.firestore import get_db

    db = get_db()
    project_ref = (
        db.collection("customers")
        .document(auth.customer_id)
        .collection("projects")
        .document(project_id)
    )

    project_doc = await project_ref.get()
    if not project_doc.exists:
        return JSONResponse({"error": "Project not found"}, status_code=404)

    query = (
        project_ref.collection("scan_history")
        .order_by("timestamp", direction="DESCENDING")
        .limit(25)
    )

    if after:
        query = query.start_after({"timestamp": after})

    scans: list[dict[str, Any]] = []
    async for doc in query.stream():
        scans.append(doc.to_dict())  # type: ignore[arg-type]

    return JSONResponse(scans, status_code=200)


# ---------------------------------------------------------------------------
# GET /v1/dashboard/projects/{project_id}/findings
# ---------------------------------------------------------------------------


@router.get("/projects/{project_id}/findings")
async def get_findings(
    project_id: str,
    auth: APIKeyContext = _auth_dep,
) -> JSONResponse:
    """Return findings JSON from GCS for latest scan."""
    from backend.shared.firestore import get_db
    from backend.shared.gcs import download_findings

    db = get_db()
    project_ref = (
        db.collection("customers")
        .document(auth.customer_id)
        .collection("projects")
        .document(project_id)
    )

    project_doc = await project_ref.get()
    if not project_doc.exists:
        return JSONResponse(
            {"error": "Project not found"},
            status_code=404,
        )

    data: dict[str, Any] = project_doc.to_dict()  # type: ignore[assignment]
    latest = data.get("latest_scan", {})
    findings_uri = latest.get("findings_uri", "")

    if not findings_uri:
        return JSONResponse([], status_code=200)

    findings = await download_findings(findings_uri)
    return JSONResponse(findings, status_code=200)
