"""FastAPI application for Sanicode server mode.

Start with: uvicorn sanicode.server.app:app --host 0.0.0.0 --port 8080
Or via CLI: sanicode serve

REST API surface:
  GET  /api/v1/health                    — liveness check
  POST /api/v1/scan                      — submit a scan job
  GET  /api/v1/scan/{id}                 — poll scan status
  GET  /api/v1/scan/{id}/findings        — retrieve findings
  GET  /api/v1/scan/{id}/graph           — retrieve knowledge graph
  POST /api/v1/analyze                   — ad-hoc snippet analysis
  GET  /api/v1/compliance/map            — compliance cross-reference lookup
  GET  /metrics                          — Prometheus metrics
"""

from __future__ import annotations

import ast
import asyncio
import dataclasses
import time
from pathlib import Path

from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import JSONResponse
from prometheus_client import Counter, Gauge, Histogram, make_asgi_app

from sanicode import __version__
from sanicode.compliance.enrichment import enrich_findings
from sanicode.compliance.mapper import ComplianceMapper
from sanicode.config import SanicodeConfig
from sanicode.report.persist import serialize_finding
from sanicode.report.sarif import generate_sarif
from sanicode.scanner.executor import ScanOutput, run_scan
from sanicode.scanner.patterns import check_patterns
from sanicode.server.models import (
    AnalyzeRequest,
    AnalyzeResponse,
    FindingsResponse,
    GraphResponse,
    HealthResponse,
    ScanRequest,
    ScanStatus,
    ScanStatusResponse,
    ScanSubmitResponse,
)
from sanicode.server.state import ScanJob, ScanStore

# ---------------------------------------------------------------------------
# Prometheus metrics
# ---------------------------------------------------------------------------

SCANS_TOTAL = Counter("sanicode_scans_total", "Total scan jobs submitted")
SCAN_DURATION = Histogram("sanicode_scan_duration_seconds", "Scan duration in seconds")
FINDINGS_TOTAL = Counter(
    "sanicode_findings_total", "Total findings detected", ["severity", "cwe_id"]
)
ACTIVE_SCANS = Gauge("sanicode_active_scans", "Number of currently running scans")
ANALYZE_TOTAL = Counter("sanicode_analyze_total", "Total snippet analysis requests")

# ---------------------------------------------------------------------------
# Module-level state (set via configure())
# ---------------------------------------------------------------------------

_store: ScanStore = ScanStore()
_config: SanicodeConfig = SanicodeConfig()
_mapper: ComplianceMapper = ComplianceMapper()

# ---------------------------------------------------------------------------
# FastAPI app
# ---------------------------------------------------------------------------

app = FastAPI(
    title="Sanicode API",
    description="AI-assisted code sanitization scanner — server mode.",
    version=__version__,
    docs_url="/api/v1/docs",
    redoc_url="/api/v1/redoc",
    openapi_url="/api/v1/openapi.json",
)

app.mount("/metrics", make_asgi_app())


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@app.get("/api/v1/health", response_model=HealthResponse, tags=["meta"])
async def health():
    """Liveness check with LLM tier availability."""
    tiers = {}
    for name in ("fast", "analysis", "reasoning"):
        tier = getattr(_config.llm, name)
        tiers[name] = "configured" if tier.endpoint else "not_configured"
    return HealthResponse(status="ok", version=__version__, llm_tiers=tiers)


@app.post("/api/v1/scan", response_model=ScanSubmitResponse, status_code=202, tags=["scan"])
async def submit_scan(request: ScanRequest):
    """Submit an asynchronous scan job.

    Returns 202 Accepted with a scan ID for polling via GET /api/v1/scan/{id}.
    """
    target = Path(request.path)
    if not target.exists():
        raise HTTPException(status_code=400, detail=f"Path does not exist: {request.path}")

    job = _store.create_job(request.path, request.config_path)
    asyncio.create_task(_run_scan_task(job))
    return ScanSubmitResponse(scan_id=job.scan_id, status=job.status)


@app.get("/api/v1/scan/{scan_id}", response_model=ScanStatusResponse, tags=["scan"])
async def scan_status(scan_id: str):
    """Poll the status of a scan job."""
    job = _store.get_job(scan_id)
    if job is None:
        raise HTTPException(status_code=404, detail=f"Unknown scan ID: {scan_id}")

    summary = None
    if job.status == ScanStatus.completed and job.result is not None:
        summary = job.result.summary

    return ScanStatusResponse(
        scan_id=job.scan_id,
        status=job.status,
        target_path=job.target_path,
        summary=summary,
        error=job.error,
    )


@app.get("/api/v1/scan/{scan_id}/findings", tags=["scan"])
async def scan_findings(
    scan_id: str,
    format: str = Query("json", description="Output format: 'json' or 'sarif'."),
):
    """Retrieve findings for a completed scan.

    Returns JSON by default or SARIF 2.1.0 when ``?format=sarif`` is specified.
    """
    job = _store.get_job(scan_id)
    if job is None:
        raise HTTPException(status_code=404, detail=f"Unknown scan ID: {scan_id}")
    if job.status != ScanStatus.completed:
        raise HTTPException(
            status_code=409,
            detail=f"Scan is not completed (status: {job.status.value})",
        )

    if format == "sarif":
        sarif = generate_sarif(job.result)
        return JSONResponse(content=sarif)

    findings = job.result.findings
    return FindingsResponse(scan_id=scan_id, findings=findings, total=len(findings))


@app.get("/api/v1/scan/{scan_id}/graph", response_model=GraphResponse, tags=["scan"])
async def scan_graph(scan_id: str):
    """Retrieve the knowledge graph for a completed scan."""
    job = _store.get_job(scan_id)
    if job is None:
        raise HTTPException(status_code=404, detail=f"Unknown scan ID: {scan_id}")
    if job.status != ScanStatus.completed:
        raise HTTPException(
            status_code=409,
            detail=f"Scan is not completed (status: {job.status.value})",
        )

    graph = job.graph_data or {}
    return GraphResponse(
        scan_id=scan_id,
        nodes=len(graph.get("nodes", [])),
        edges=len(graph.get("links", [])),
        graph=graph,
    )


@app.post("/api/v1/analyze", response_model=AnalyzeResponse, tags=["analyze"])
async def analyze_snippet(request: AnalyzeRequest):
    """Synchronous analysis of a code snippet.

    Parses the provided code, runs pattern detection, enriches findings with
    compliance data, and returns the results immediately.
    """
    ANALYZE_TOTAL.inc()

    try:
        tree = ast.parse(request.code)
    except SyntaxError as exc:
        raise HTTPException(status_code=400, detail=f"Syntax error: {exc}") from exc

    file_path = Path(request.filename)
    raw_findings = check_patterns(tree, file_path)
    enriched = enrich_findings(raw_findings, _mapper)

    serialized = [serialize_finding(ef, Path(".")) for ef in enriched]
    for ef in enriched:
        sev = ef.derived_severity or ef.severity
        FINDINGS_TOTAL.labels(severity=sev, cwe_id=str(ef.cwe_id)).inc()

    return AnalyzeResponse(filename=request.filename, findings=serialized, total=len(serialized))


@app.get("/api/v1/compliance/map", tags=["compliance"])
async def compliance_map(
    cwe_id: int | None = Query(None, description="CWE ID to look up (forward mode)."),
    framework: str | None = Query(
        None, description="Compliance framework: owasp_asvs, nist_800_53, asd_stig, pci_dss."
    ),
    control_id: str | None = Query(None, description="Control identifier within the framework."),
):
    """Compliance cross-reference lookup.

    Forward mode: provide ``cwe_id`` to get all mapped controls.
    Reverse mode: provide ``framework`` and ``control_id`` to find associated CWEs.
    """
    if cwe_id is not None:
        mapping = _mapper.map_cwe(cwe_id)
        return JSONResponse(content=dataclasses.asdict(mapping))

    if framework is not None and control_id is not None:
        mappings = _mapper.map_control(framework, control_id)
        return JSONResponse(content=[dataclasses.asdict(m) for m in mappings])

    raise HTTPException(
        status_code=400,
        detail=(
            "Provide either 'cwe_id' (forward mode) or both"
            " 'framework' and 'control_id' (reverse mode)."
        ),
    )


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


async def _run_scan_task(job: ScanJob) -> None:
    """Run a scan job in a background thread (CPU-bound work)."""
    job.status = ScanStatus.running
    ACTIVE_SCANS.inc()
    SCANS_TOTAL.inc()
    start = time.monotonic()
    try:
        loop = asyncio.get_event_loop()
        output = await loop.run_in_executor(None, _execute_scan, job)
        job.result = output.result
        job.graph_data = output.graph_data
        job.status = ScanStatus.completed
        for ef in output.enriched_findings:
            sev = ef.derived_severity or ef.severity
            FINDINGS_TOTAL.labels(severity=sev, cwe_id=str(ef.cwe_id)).inc()
    except Exception as exc:
        job.status = ScanStatus.failed
        job.error = str(exc)
    finally:
        ACTIVE_SCANS.dec()
        SCAN_DURATION.observe(time.monotonic() - start)


def _execute_scan(job: ScanJob) -> ScanOutput:
    """Synchronous scan execution (runs in thread pool)."""
    from sanicode.config import load_config

    cfg = load_config(Path(job.config_path) if job.config_path else None)
    return run_scan(Path(job.target_path), cfg)


def configure(cfg: SanicodeConfig) -> None:
    """Set server-wide configuration. Call before starting the server."""
    global _store, _config, _mapper
    _store = ScanStore()
    _config = cfg
    _mapper = ComplianceMapper()
