"""
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

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),
            })
        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) 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):
        yara = data.get("yara_analysis", {})
        if isinstance(yara, dict) and yara.get("match_details"):
            return len(yara["match_details"])
        scan_type_str = scan_type.value if hasattr(scan_type, "value") else str(scan_type)
        if scan_type_str == "quantum":
            cert = data.get("certificate_analysis", {})
            if isinstance(cert, dict) and cert.get("shor_vulnerable"):
                return 1
        if scan_type_str == "port":
            net = data.get("data", {})
            if isinstance(net, dict):
                return len(net.get("openPorts", []))
    return 0


def _extract_risk_score(scan_data: dict) -> Optional[float]:
    if not isinstance(scan_data, dict):
        return None
    data = scan_data.get("data", {})
    if not isinstance(data, dict):
        return None
    # Code analysis: top-level quantum_risk_score
    if "quantum_risk_score" in data:
        return data["quantum_risk_score"]
    # Certificate/quantum scan: nested under certificate_analysis
    cert = data.get("certificate_analysis", {})
    if isinstance(cert, dict) and "quantum_risk_score" in cert:
        return cert["quantum_risk_score"]
    # Legacy path
    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 _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", {})
        if isinstance(cert, dict):
            sig_algo = cert.get("signature_algorithm") or cert.get("public_key_algorithm")
            if sig_algo:
                components.append(_make_algo_component(str(sig_algo), location=target))
            key_size = cert.get("public_key_size") or cert.get("key_size")
            name_with_size = (
                f"{sig_algo}-{key_size}" if sig_algo and key_size else sig_algo
            )
            if name_with_size and name_with_size != sig_algo:
                components.append(_make_algo_component(str(name_with_size), location=target))
            cipher_suites = cert.get("cipher_suites") or cert.get("weak_cipher_suites") or []
            for cs in cipher_suites:
                if isinstance(cs, str):
                    components.append(_make_algo_component(cs, 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:
                    proto_type = ProtocolPropertiesType.TLS
                    props = CryptoProperties(
                        asset_type=CryptoAssetType.PROTOCOL,
                        protocol_properties=ProtocolProperties(
                            type=proto_type,
                            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

    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
