"""
Reports API Routes

Provides endpoints for report generation and bulk export.
"""

import csv
import io
import json
import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import Response as RawResponse
from sqlmodel import select

from app.auth import get_current_user
from app.database import SessionDep
from app.models import ScanRecord, User
from app.services.dashboard_metrics import get_metrics_provider, _extract_analyzer_findings_and_risk

logger = logging.getLogger(__name__)

router = APIRouter(tags=["reports"])


@router.get("/reports/summary")
async def get_report_summary(
    session: SessionDep,
    user: User = Depends(get_current_user),
) -> Dict[str, Any]:
    """
    Get aggregated report summary data — same metrics as dashboard
    plus a flat list of all scans for the export table.
    """
    try:
        provider = get_metrics_provider(session)
        metrics = await provider.get_metrics(user.id)

        # Latest scan per (target, scan_type) — no duplicates for report selection
        # Subquery gets the max timestamp per group, outer query fetches those rows.
        from sqlalchemy import func
        from sqlmodel import col

        subq = (
            select(
                ScanRecord.target,
                ScanRecord.scan_type,
                func.max(ScanRecord.timestamp).label("max_ts"),
            )
            .where(ScanRecord.user_id == user.id)
            .group_by(ScanRecord.target, ScanRecord.scan_type)
            .subquery()
        )

        scans = session.exec(
            select(
                ScanRecord.id,
                ScanRecord.target,
                ScanRecord.scan_type,
                ScanRecord.timestamp,
            )
            .where(ScanRecord.user_id == user.id)
            .join(
                subq,
                (ScanRecord.target == subq.c.target)
                & (ScanRecord.scan_type == subq.c.scan_type)
                & (ScanRecord.timestamp == subq.c.max_ts),
            )
            .order_by(ScanRecord.timestamp.desc())
        ).all()

        scan_list = [
            {
                "id": s.id,
                "target": s.target,
                "scan_type": s.scan_type.value if hasattr(s.scan_type, "value") else str(s.scan_type),
                "timestamp": s.timestamp.isoformat(),
            }
            for s in scans
        ]

        return {
            "success": True,
            "data": {
                "metrics": metrics,
                "scans": scan_list,
                "generated_at": datetime.now(timezone.utc).isoformat(),
            },
        }
    except Exception as e:
        logger.error(f"Failed to get report summary: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"Failed to get report summary: {str(e)}")


@router.get("/reports/export")
async def export_all_scans(
    format: str = "json",
    scan_type: Optional[str] = None,
    session: SessionDep = None,
    user: User = Depends(get_current_user),
):
    """
    Export all scan records for the current user.

    Query Parameters:
        format: 'json' or 'csv'
        scan_type: optional filter (code, quantum, port, binary)
    """
    stmt = (
        select(ScanRecord)
        .where(ScanRecord.user_id == user.id)
        .order_by(ScanRecord.timestamp.desc())
        .limit(500)
    )
    if scan_type:
        stmt = stmt.where(ScanRecord.scan_type == scan_type)

    scans = session.exec(stmt).all()

    timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")

    if format == "json":
        rows = []
        for s in scans:
            sd = s.scan_data or {}
            rows.append({
                "id": s.id,
                "target": s.target,
                "scan_type": s.scan_type.value if hasattr(s.scan_type, "value") else str(s.scan_type),
                "timestamp": s.timestamp.isoformat(),
                "findings_count": _extract_findings_count(sd, s.scan_type),
                "risk_score": _extract_risk_score(sd, s.scan_type),
            })
        content = json.dumps({"exported_at": timestamp, "total": len(rows), "scans": rows}, indent=2)
        return RawResponse(
            content=content.encode(),
            media_type="application/json",
            headers={"Content-Disposition": f'attachment; filename="rivet_export_{timestamp}.json"'},
        )

    elif format == "csv":
        output = io.StringIO()
        writer = csv.writer(output)
        writer.writerow(["id", "target", "scan_type", "timestamp", "findings_count", "risk_score"])
        for s in scans:
            sd = s.scan_data or {}
            scan_type_str = s.scan_type.value if hasattr(s.scan_type, "value") else str(s.scan_type)
            writer.writerow([
                s.id,
                s.target,
                scan_type_str,
                s.timestamp.isoformat(),
                _extract_findings_count(sd, s.scan_type),
                _extract_risk_score(sd, s.scan_type) or "",
            ])
        content = output.getvalue()
        return RawResponse(
            content=content.encode(),
            media_type="text/csv",
            headers={"Content-Disposition": f'attachment; filename="rivet_export_{timestamp}.csv"'},
        )

    else:
        raise HTTPException(status_code=400, detail=f"Unsupported format: {format}. Use 'json' or 'csv'.")


@router.get("/reports/export/cbom")
async def export_cbom(
    scan_ids: Optional[str] = Query(default=None, description="Comma-separated scan IDs; omit for all scans"),
    scan_type: Optional[str] = Query(default=None, description="Filter by scan type: code, quantum, binary"),
    session: SessionDep = None,
    user: User = Depends(get_current_user),
):
    """
    Export a CycloneDX 1.6 Cryptographic Bill of Materials (CBOM) in JSON format.

    Aggregates cryptographic assets discovered across code, certificate, and binary scans
    and emits a machine-readable document suitable for CISA/NSM-10 compliance reporting.

    Query Parameters:
        scan_ids: comma-separated list of scan record IDs to include (default: all)
        scan_type: filter to a specific scan type
    """
    try:
        from cyclonedx.model.bom import Bom
        from cyclonedx.model.component import Component, ComponentType
        from cyclonedx.model.crypto import (
            AlgorithmProperties,
            CertificateProperties,
            CryptoAssetType,
            CryptoPrimitive,
            CryptoProperties,
            ProtocolProperties,
            ProtocolPropertiesType,
            ProtocolPropertiesCipherSuite,
        )
        from cyclonedx.output.json import JsonV1Dot6
        from cyclonedx.model import XsUri
        from packageurl import PackageURL
    except ImportError as exc:
        raise HTTPException(status_code=500, detail=f"CycloneDX library not available: {exc}")

    stmt = select(ScanRecord).where(ScanRecord.user_id == user.id)
    if scan_ids:
        id_list = [s.strip() for s in scan_ids.split(",") if s.strip()]
        stmt = stmt.where(ScanRecord.id.in_(id_list))
    if scan_type:
        stmt = stmt.where(ScanRecord.scan_type == scan_type)
    stmt = stmt.order_by(ScanRecord.timestamp.desc()).limit(500)
    scans = session.exec(stmt).all()

    bom = Bom()
    seen: set[str] = set()

    for scan in scans:
        sd = scan.scan_data or {}
        scan_type_str = scan.scan_type.value if hasattr(scan.scan_type, "value") else str(scan.scan_type)
        components = _scan_to_crypto_components(sd, scan_type_str, scan.target)
        for comp in components:
            key = f"{comp.name}::{scan_type_str}"
            if key not in seen:
                seen.add(key)
                bom.components.add(comp)

    serializer = JsonV1Dot6(bom)
    content = serializer.output_as_string(indent=2)
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    return RawResponse(
        content=content.encode(),
        media_type="application/vnd.cyclonedx+json",
        headers={"Content-Disposition": f'attachment; filename="cerebion_cbom_{timestamp}.json"'},
    )


# --- helpers ---

def _extract_findings_count(scan_data: dict, scan_type) -> int:
    if not isinstance(scan_data, dict):
        return 0
    if "findings_count" in scan_data:
        return scan_data["findings_count"]
    if "enriched_findings" in scan_data:
        return len(scan_data["enriched_findings"])
    data = scan_data.get("data", {})
    if isinstance(data, dict):
        scan_type_str = scan_type.value if hasattr(scan_type, "value") else str(scan_type)
        if scan_type_str in ("quantum", "port"):
            findings_count, _ = _extract_analyzer_findings_and_risk(scan_type_str, data)
            return findings_count
        yara = data.get("yara_analysis", {})
        if isinstance(yara, dict) and yara.get("match_details"):
            return len(yara["match_details"])
    return 0


def _extract_risk_score(scan_data: dict, scan_type=None) -> Optional[float]:
    if not isinstance(scan_data, dict):
        return None
    # Single-file code analysis scans: top-level quantum_risk_score (directory
    # scans don't compute one at all, so this legitimately stays None for those)
    if "quantum_risk_score" in scan_data:
        return scan_data["quantum_risk_score"]
    data = scan_data.get("data", {})
    if not isinstance(data, dict):
        return None
    scan_type_str = (scan_type.value if hasattr(scan_type, "value") else str(scan_type)) if scan_type else None
    # Certificate/network scans: real score lives nested under
    # data.certificate_analysis or per-service under data.data.certificates
    if scan_type_str in ("quantum", "port"):
        _, risk_score = _extract_analyzer_findings_and_risk(scan_type_str, data)
        if risk_score is not None:
            return risk_score
    # Legacy path (binary scans)
    ra = data.get("quantum_risk_assessment", {})
    if isinstance(ra, dict):
        return ra.get("quantum_risk_score")
    return None


# --- CBOM mapping ---

_ALGO_PRIMITIVE_MAP: Dict[str, Any] = {
    "rsa": "pke",
    "ecdsa": "signature",
    "ecdh": "key-agree",
    "dh": "key-agree",
    "dsa": "signature",
    "aes": "block-cipher",
    "3des": "block-cipher",
    "des": "block-cipher",
    "rc4": "stream-cipher",
    "md5": "hash",
    "sha-1": "hash",
    "sha1": "hash",
    "sha-256": "hash",
    "sha-384": "hash",
    "sha-512": "hash",
    "ml-kem": "kem",
    "kyber": "kem",
    "ml-dsa": "signature",
    "dilithium": "signature",
    "slh-dsa": "signature",
    "sphincs": "signature",
}

_NIST_QUANTUM_LEVEL_MAP: Dict[str, int] = {
    "ml-kem": 3,
    "kyber": 3,
    "ml-dsa": 3,
    "dilithium": 3,
    "slh-dsa": 3,
    "sphincs": 3,
    "rsa": 0,
    "ecdsa": 0,
    "ecdh": 0,
    "dh": 0,
    "dsa": 0,
    "rc4": 0,
    "md5": 0,
    "sha-1": 0,
    "des": 0,
    "3des": 0,
}


def _primitive_for(algo_name: str):
    """Return a CryptoPrimitive value string for a given algorithm name."""
    lower = algo_name.lower()
    for key, val in _ALGO_PRIMITIVE_MAP.items():
        if key in lower:
            return val
    return "unknown"


def _nist_level_for(algo_name: str) -> Optional[int]:
    lower = algo_name.lower()
    for key, val in _NIST_QUANTUM_LEVEL_MAP.items():
        if key in lower:
            return val
    return None


def _make_algo_component(name: str, location: Optional[str] = None):
    """Build a CycloneDX cryptographic-asset Component for an algorithm finding."""
    from cyclonedx.model.component import Component, ComponentType
    from cyclonedx.model.crypto import (
        AlgorithmProperties, CryptoAssetType, CryptoPrimitive, CryptoProperties,
    )

    prim_str = _primitive_for(name)
    try:
        prim = CryptoPrimitive(prim_str)
    except ValueError:
        prim = CryptoPrimitive.UNKNOWN

    nist_lvl = _nist_level_for(name)

    props = CryptoProperties(
        asset_type=CryptoAssetType.ALGORITHM,
        algorithm_properties=AlgorithmProperties(
            primitive=prim,
            nist_quantum_security_level=nist_lvl,
        ),
    )

    comp = Component(
        type=ComponentType.CRYPTOGRAPHIC_ASSET,
        name=name,
        crypto_properties=props,
    )
    if location:
        from cyclonedx.model import Property
        comp.properties.add(Property(name="rivet:location", value=location))
    return comp


def _components_from_cert_analysis(cert: dict, target: str) -> list:
    """Build CBOM components from one certificate_analysis-shaped dict.

    Shared by certificate (quantum) scans, which have a single certificate, and
    network (port) scans, which have one such dict per scanned TLS service.
    """
    components = []
    if not isinstance(cert, dict):
        return components

    # Prefer public_key_algorithm (human-readable, e.g. "ECDSA", "RSA").
    # signature_algorithm is stored as a raw OID dotted string (e.g.
    # "1.2.840.10045.4.3.2") by the certificate analyzer -- useless in a
    # compliance document, so only fall back to it if nothing else is set.
    algo = cert.get("public_key_algorithm") or cert.get("signature_algorithm")
    if algo:
        components.append(_make_algo_component(str(algo), location=target))
        key_size = cert.get("public_key_size") or cert.get("key_size")
        name_with_size = f"{algo}-{key_size}" if key_size else None
        if name_with_size and name_with_size != algo:
            components.append(_make_algo_component(str(name_with_size), location=target))

    # cipher_suite is a single string field (e.g. "TLS_AES_256_GCM_SHA384"),
    # not a list -- there is no "cipher_suites"/"weak_cipher_suites" key.
    cipher_suite = cert.get("cipher_suite")
    if isinstance(cipher_suite, str) and cipher_suite:
        components.append(_make_algo_component(cipher_suite, location=target))

    tls_version = cert.get("tls_version") or cert.get("protocol_version")
    if tls_version:
        from cyclonedx.model.component import Component, ComponentType
        from cyclonedx.model.crypto import (
            CryptoAssetType, CryptoProperties, ProtocolProperties,
            ProtocolPropertiesType,
        )
        try:
            props = CryptoProperties(
                asset_type=CryptoAssetType.PROTOCOL,
                protocol_properties=ProtocolProperties(
                    type=ProtocolPropertiesType.TLS,
                    version=str(tls_version),
                ),
            )
            comp = Component(
                type=ComponentType.CRYPTOGRAPHIC_ASSET,
                name=f"TLS {tls_version}",
                crypto_properties=props,
            )
            from cyclonedx.model import Property
            comp.properties.add(Property(name="rivet:location", value=target))
            components.append(comp)
        except Exception:
            pass

    return components


def _scan_to_crypto_components(scan_data: dict, scan_type_str: str, target: str) -> list:
    """Convert a Rivet scan_data payload into a list of CycloneDX Component objects."""
    components = []
    data = scan_data.get("data", {}) if isinstance(scan_data, dict) else {}
    if not isinstance(data, dict):
        return components

    if scan_type_str in ("code_analysis", "config"):
        findings = data.get("enriched_findings") or scan_data.get("enriched_findings") or []
        for f in findings:
            if not isinstance(f, dict):
                continue
            rule_id = f.get("check_id", "")
            algo = _algo_from_rule_id(rule_id) or rule_id.split(".")[-1].replace("-", " ").upper()
            location = f.get("path") or f.get("file_path") or target
            if algo:
                components.append(_make_algo_component(algo, location))

    elif scan_type_str in ("quantum", "domain"):
        cert = data.get("certificate_analysis", {})
        components.extend(_components_from_cert_analysis(cert, target))

    elif scan_type_str == "port":
        # Network scans store one certificate_analysis-shaped dict per scanned
        # TLS service, keyed by port, under data.data.certificates.
        net_data = data.get("data", {})
        certs = net_data.get("certificates", {}) if isinstance(net_data, dict) else {}
        if isinstance(certs, dict):
            cert_list = list(certs.values())
        elif isinstance(certs, list):
            cert_list = certs
        else:
            cert_list = []
        for cert in cert_list:
            components.extend(_components_from_cert_analysis(cert, target))

    elif scan_type_str == "binary":
        yara = data.get("yara_analysis", {})
        if isinstance(yara, dict):
            for match in yara.get("match_details", []):
                if isinstance(match, dict):
                    rule = match.get("rule", "")
                    algo = _algo_from_rule_id(rule) or rule
                    if algo:
                        components.append(_make_algo_component(algo, location=target))
        crypto_findings = data.get("crypto_findings") or data.get("findings") or []
        for f in crypto_findings:
            if isinstance(f, dict):
                algo = f.get("algorithm") or f.get("name")
                if algo:
                    components.append(_make_algo_component(str(algo), location=target))

    return components


def _algo_from_rule_id(rule_id: str) -> Optional[str]:
    """Extract a clean algorithm name from a Rivet rule ID."""
    rule_id_lower = rule_id.lower()
    mappings = [
        ("rsa", "RSA"),
        ("ecdsa", "ECDSA"),
        ("ecdh", "ECDH"),
        ("-dh-", "DH"),
        ("aes-128", "AES-128"),
        ("aes-256", "AES-256"),
        ("aes128", "AES-128"),
        ("aes256", "AES-256"),
        ("3des", "3DES"),
        ("des-", "DES"),
        ("rc4", "RC4"),
        ("md5", "MD5"),
        ("sha-1", "SHA-1"),
        ("sha1", "SHA-1"),
        ("sha-256", "SHA-256"),
        ("sha256", "SHA-256"),
        ("ml-kem", "ML-KEM"),
        ("kyber", "Kyber"),
        ("ml-dsa", "ML-DSA"),
        ("dilithium", "Dilithium"),
        ("slh-dsa", "SLH-DSA"),
        ("sphincs", "SPHINCS+"),
        ("tls-rsa-static", "RSA-static-key-exchange"),
        ("weak-cipher", "weak-cipher-suite"),
        ("rsa-static-key-exchange", "RSA-static-key-exchange"),
    ]
    for fragment, label in mappings:
        if fragment in rule_id_lower:
            return label
    return None
