"""Scan result persistence — save and load serialized scan results.

A ScanResult captures everything needed to regenerate reports without re-scanning:
the enriched findings, a graph summary, and scan metadata. Results are written as
a single JSON file (``scan-result.json``) so they can be committed to version
control or archived alongside CI artifacts.
"""

from __future__ import annotations

import dataclasses
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path

import sanicode
from sanicode.compliance.enrichment import EnrichedFinding
from sanicode.graph.builder import KnowledgeGraph


@dataclass
class ScanResult:
    """Serialized output of a single sanicode scan run."""

    sanicode_version: str
    scan_id: str
    scanned_path: str
    scan_timestamp: str
    summary: dict
    findings: list[dict] = field(default_factory=list)


def serialize_finding(finding: EnrichedFinding, base: Path) -> dict:
    """Convert an EnrichedFinding to a JSON-serializable dict.

    The file path is stored relative to ``base`` for portability. If the
    finding's file is not under ``base``, the absolute path is used instead.
    """
    try:
        rel_file = str(finding.file.relative_to(base))
    except ValueError:
        rel_file = str(finding.file)

    compliance: dict | None = None
    if finding.compliance is not None:
        c = finding.compliance
        compliance = {
            "cwe_id": c.cwe_id,
            "cwe_name": c.cwe_name,
            "description": c.description,
            "owasp_asvs": c.owasp_asvs,
            "nist_800_53": c.nist_800_53,
            "asd_stig": c.asd_stig,
            "pci_dss": c.pci_dss,
            "asvs_level": c.asvs_level,
            "stig_category": c.stig_category,
            "remediation": c.remediation,
        }

    return {
        "file": rel_file,
        "line": finding.line,
        "column": finding.column,
        "rule_id": finding.rule_id,
        "message": finding.message,
        "severity": finding.severity,
        "cwe_id": finding.cwe_id,
        "cwe_name": finding.cwe_name,
        "compliance": compliance,
        "derived_severity": finding.derived_severity,
        "remediation": finding.remediation,
    }


def _build_summary(
    findings: list[EnrichedFinding], kg: KnowledgeGraph
) -> dict:
    """Compute the summary block from findings and graph statistics."""
    by_severity: dict[str, int] = {}
    by_cwe: dict[str, int] = {}

    for f in findings:
        by_severity[f.severity] = by_severity.get(f.severity, 0) + 1
        cwe_key = str(f.cwe_id)
        by_cwe[cwe_key] = by_cwe.get(cwe_key, 0) + 1

    entry_points = kg.nodes_by_kind("entry_point")
    sinks = kg.nodes_by_kind("sink")
    sanitizers = kg.nodes_by_kind("sanitizer")

    return {
        "total_findings": len(findings),
        "by_severity": by_severity,
        "by_cwe": by_cwe,
        "graph_nodes": kg.node_count,
        "graph_edges": kg.edge_count,
        "graph_entry_points": len(entry_points),
        "graph_sinks": len(sinks),
        "graph_sanitizers": len(sanitizers),
    }


def build_scan_result(
    enriched_findings: list[EnrichedFinding],
    scanned_path: Path,
    kg: KnowledgeGraph,
) -> ScanResult:
    """Build a ScanResult from enriched findings and a knowledge graph.

    File paths within findings are stored relative to ``scanned_path`` so the
    result file can be moved or shared without breaking path references.

    Args:
        enriched_findings: Compliance-enriched findings from the scan.
        scanned_path: Root path that was scanned (used as the path anchor).
        kg: The knowledge graph built during the scan.

    Returns:
        A populated ScanResult ready for serialization.
    """
    scanned_path = scanned_path.resolve()
    now = datetime.now(timezone.utc)
    scan_id = now.strftime("%Y%m%dT%H%M%SZ")
    scan_timestamp = now.isoformat()

    return ScanResult(
        sanicode_version=sanicode.__version__,
        scan_id=scan_id,
        scanned_path=str(scanned_path),
        scan_timestamp=scan_timestamp,
        summary=_build_summary(enriched_findings, kg),
        findings=[serialize_finding(f, scanned_path) for f in enriched_findings],
    )


def save_scan_result(result: ScanResult, output_dir: Path) -> Path:
    """Write a ScanResult to ``scan-result.json`` in the given directory.

    Creates ``output_dir`` (and any parents) if it does not already exist.

    Args:
        result: The scan result to persist.
        output_dir: Directory to write into.

    Returns:
        The path to the written JSON file.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    out_path = output_dir / "scan-result.json"
    payload = dataclasses.asdict(result)
    out_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
    return out_path


def load_scan_result(path: Path) -> ScanResult:
    """Read and deserialize a ``scan-result.json`` file.

    Args:
        path: Path to the JSON file written by :func:`save_scan_result`.

    Returns:
        A ScanResult populated from the file contents.

    Raises:
        FileNotFoundError: If ``path`` does not exist.
        ValueError: If the file is missing required fields.
    """
    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"Scan result file not found: {path}")

    raw = json.loads(path.read_text(encoding="utf-8"))

    required = {"sanicode_version", "scan_id", "scanned_path", "scan_timestamp", "summary"}
    missing = required - raw.keys()
    if missing:
        raise ValueError(
            f"Scan result file is missing required fields: {', '.join(sorted(missing))}"
        )

    return ScanResult(
        sanicode_version=raw["sanicode_version"],
        scan_id=raw["scan_id"],
        scanned_path=raw["scanned_path"],
        scan_timestamp=raw["scan_timestamp"],
        summary=raw["summary"],
        findings=raw.get("findings", []),
    )
