"""ReportData builder: transform raw assessment findings into report models.

Contract: ReportData is the ONLY interface between the assessment engine
and report renderers (HTML, future PDF). The builder normalises, filters,
sorts, and enriches findings into a self-contained data structure.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from complyform import __version__

if TYPE_CHECKING:
    from complyform.core.assessor.control_matcher import Finding
    from complyform.core.profiles.models import CrossMappingIndex, FrameworkRegistry


# -- Severity weights (shared with scoring module) ---------------------------

_SEVERITY_WEIGHT: dict[str, int] = {
    "CRITICAL": 10,
    "HIGH": 5,
    "MEDIUM": 2,
    "LOW": 1,
}

_SEVERITY_ORDER: list[str] = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]

FRAMEWORK_COLORS: list[str] = [
    "#2563eb",  # blue
    "#7c3aed",  # violet
    "#059669",  # emerald
    "#d97706",  # amber
    "#dc2626",  # red
    "#0891b2",  # cyan
    "#4f46e5",  # indigo
    "#be185d",  # pink
]


# -- Data models -------------------------------------------------------------


@dataclass(frozen=True)
class ReportMetadata:
    """Report envelope metadata."""

    tool_version: str
    generated_at: str  # ISO 8601
    cloud: str  # "gcp" | "aws" | "azure"
    scan_source: str  # "state" | "api"
    state_hash: str | None  # SHA-256 of input state
    frameworks: list[str]  # Framework IDs assessed
    tier: str
    project_id: str | None  # Cloud project ID (if known)


@dataclass(frozen=True)
class ReportSummary:
    """Aggregate assessment counts and score."""

    total_controls: int
    passed: int
    failed: int
    manual_review: int
    not_assessed: int
    score: float  # 0-100, severity-weighted
    severity_counts: dict[str, int]
    resource_count: int
    assessment_duration_ms: int


@dataclass(frozen=True)
class NotAssessedGroup:
    """Group of resources with no framework mapping."""

    resource_type: str
    count: int
    reason: str  # "no_mapping" | "unsupported_provider_version"


@dataclass(frozen=True)
class ReportFinding:
    """Single finding for report rendering."""

    control_id: str
    framework: str
    framework_name: str  # Human-readable: "SOC 2 Type II"
    resource_address: str
    resource_type: str
    status: str  # "FAIL" | "MANUAL_REVIEW"
    severity: str  # "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"
    title: str
    description: str
    remediation: str
    remediation_available: bool
    cross_mappings: list[dict[str, str]]
    checkov_id: str | None
    native_ids: dict[str, Any] | None
    confidence: str  # "verified" | "inferred"


@dataclass(frozen=True)
class CrossMappingSection:
    """Cross-framework mapping summary."""

    total_shared: int
    by_category: dict[str, list[dict[str, Any]]]
    overlap_matrix: list[dict[str, Any]] | None


@dataclass(frozen=True)
class CostImpactSection:
    """Team+ only. None for Pro."""

    total_monthly_delta: float
    total_annual_delta: float
    breakdown: list[dict[str, Any]]
    confidence: str


@dataclass(frozen=True)
class CrossValidationSection:
    """Team+ only. None for Pro."""

    score: float
    confirmed: int
    complyform_only: int
    native_only: int
    native_tool: str


@dataclass(frozen=True)
class ReportData:
    """Complete report data structure — the ONLY interface to renderers."""

    metadata: ReportMetadata
    summary: ReportSummary
    findings: list[ReportFinding]
    not_assessed_groups: list[NotAssessedGroup]
    cross_mappings: CrossMappingSection
    cost_impact: CostImpactSection | None  # Team+ only
    cross_validation: CrossValidationSection | None  # Team+ only
    frameworks_detail: list[dict[str, Any]]
    upgrade_cta: dict[str, str] | None


# -- Builder ------------------------------------------------------------------


def _compute_score(findings: list[Finding]) -> float:
    """Severity-weighted score: (total - failed) / total * 100."""
    total_weight = 0
    failed_weight = 0
    for f in findings:
        w = _SEVERITY_WEIGHT.get(f.severity, 1)
        total_weight += w
        if f.status == "FAIL":
            failed_weight += w
    if total_weight == 0:
        return 100.0
    return round(((total_weight - failed_weight) / total_weight) * 100, 1)


def _fw_display_name(
    framework_id: str,
    registry: FrameworkRegistry | None,
) -> str:
    """Resolve human-readable framework name from registry."""
    if registry is not None:
        for fw in registry.frameworks:
            if fw.id == framework_id:
                return fw.name
    return framework_id


def _build_overlap_matrix(
    findings: list[ReportFinding],
    framework_ids: list[str],
) -> list[dict[str, Any]] | None:
    """Build pairwise overlap matrix for 2+ frameworks."""
    if len(framework_ids) < 2:
        return None

    fw_controls: dict[str, set[str]] = {fw: set() for fw in framework_ids}
    for f in findings:
        if f.framework in fw_controls:
            fw_controls[f.framework].add(f.control_id)

    matrix: list[dict[str, Any]] = []
    for i, fw_a in enumerate(framework_ids):
        for fw_b in framework_ids[i + 1 :]:
            shared = fw_controls[fw_a] & fw_controls[fw_b]
            total = fw_controls[fw_a] | fw_controls[fw_b]
            pct = round(len(shared) / len(total) * 100, 1) if total else 0.0
            matrix.append(
                {
                    "fw_a": fw_a,
                    "fw_b": fw_b,
                    "shared_count": len(shared),
                    "overlap_pct": pct,
                }
            )
    return matrix


def _build_cross_mapping_section(
    report_findings: list[ReportFinding],
    framework_ids: list[str],
) -> CrossMappingSection:
    """Build the cross-mapping section from enriched findings."""
    all_cross: list[dict[str, str]] = []
    for f in report_findings:
        all_cross.extend(f.cross_mappings)

    by_category: dict[str, list[dict[str, Any]]] = {}
    seen: set[str] = set()
    for cm in all_cross:
        key = f"{cm.get('framework', '')}:{cm.get('control_id', '')}"
        if key not in seen:
            seen.add(key)
            cat = cm.get("category", "general")
            by_category.setdefault(cat, []).append(cm)

    overlap_matrix = _build_overlap_matrix(report_findings, framework_ids)

    return CrossMappingSection(
        total_shared=len(seen),
        by_category=by_category,
        overlap_matrix=overlap_matrix,
    )


def build_report_data(
    findings: list[Finding],
    cross_mapping_index: CrossMappingIndex | None,
    framework_registry: FrameworkRegistry | None,
    tier: str,
    metadata: dict[str, Any],
    cost_impact: CostImpactSection | None = None,
    cross_validation: CrossValidationSection | None = None,
) -> ReportData:
    """Build ReportData from raw assessment.

    - Filters to FAIL + MANUAL_REVIEW (unless include_passing=True in metadata)
    - Sorts: severity DESC -> framework -> control_id
    - Resolves display names from framework_registry
    - Computes per-framework scores
    - Sets upgrade_cta for Pro (None for Team/Agency/Enterprise)
    """
    from datetime import UTC, datetime

    include_passing: bool = metadata.get("include_passing", False)

    # Partition findings
    assessed = [f for f in findings if f.status != "NOT_ASSESSED"]
    not_assessed = [f for f in findings if f.status == "NOT_ASSESSED"]

    # Filter
    if include_passing:
        visible = assessed
    else:
        visible = [f for f in assessed if f.status in ("FAIL", "MANUAL_REVIEW")]

    # Sort: severity DESC -> framework -> control_id
    def _sort_key(f: Finding) -> tuple[int, str, str]:
        idx = _SEVERITY_ORDER.index(f.severity) if f.severity in _SEVERITY_ORDER else 99
        return (idx, f.framework, f.control_id)

    visible = sorted(visible, key=_sort_key)

    # Collect framework IDs
    framework_ids: list[str] = metadata.get("frameworks", [])
    if not framework_ids:
        seen_fw: list[str] = []
        for f in findings:
            if f.framework not in seen_fw:
                seen_fw.append(f.framework)
        framework_ids = seen_fw

    # Build ReportFinding list
    report_findings: list[ReportFinding] = []
    for f in visible:
        cross: list[dict[str, str]] = []
        if f.cross_mappings:
            for cm in f.cross_mappings:
                entry: dict[str, str] = {
                    "framework": cm.get("framework", ""),
                    "control_id": cm.get("control_id", ""),
                }
                fw_name = _fw_display_name(cm.get("framework", ""), framework_registry)
                entry["framework_name"] = fw_name
                cross.append(entry)

        # Map Finding.finding -> title/description
        confidence = f.confidence
        if confidence not in ("verified", "inferred"):
            # Normalise "high"/"medium"/"low" to "inferred"
            confidence = "inferred" if confidence != "verified" else confidence

        report_findings.append(
            ReportFinding(
                control_id=f.control_id,
                framework=f.framework,
                framework_name=_fw_display_name(f.framework, framework_registry),
                resource_address=f.resource_address,
                resource_type=f.resource_type,
                status=f.status,
                severity=f.severity,
                title=f.finding,
                description=f.finding,
                remediation=f.remediation,
                remediation_available=bool(f.remediation),
                cross_mappings=cross,
                checkov_id=f.checkov_id,
                native_ids=f.native_ids,
                confidence=confidence,
            )
        )

    # Not-assessed groups
    na_counts: dict[str, int] = {}
    for f in not_assessed:
        na_counts[f.resource_type] = na_counts.get(f.resource_type, 0) + 1

    not_assessed_groups = [
        NotAssessedGroup(resource_type=rt, count=ct, reason="no_mapping")
        for rt, ct in sorted(na_counts.items())
    ]

    # Summary
    passed_count = sum(1 for f in assessed if f.status == "PASS")
    failed_count = sum(1 for f in assessed if f.status == "FAIL")
    manual_count = sum(1 for f in assessed if f.status == "MANUAL_REVIEW")
    sev_counts: dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "low": 0}
    for f in assessed:
        if f.status == "FAIL":
            sev_counts[f.severity.lower()] = sev_counts.get(f.severity.lower(), 0) + 1

    score = _compute_score(assessed)

    resource_addresses = {f.resource_address for f in findings}

    summary = ReportSummary(
        total_controls=len(assessed),
        passed=passed_count,
        failed=failed_count,
        manual_review=manual_count,
        not_assessed=len(not_assessed),
        score=score,
        severity_counts=sev_counts,
        resource_count=metadata.get("resource_count", len(resource_addresses)),
        assessment_duration_ms=metadata.get("assessment_duration_ms", 0),
    )

    # Per-framework detail
    frameworks_detail: list[dict[str, Any]] = []
    for fw_id in framework_ids:
        fw_findings = [f for f in assessed if f.framework == fw_id]
        fw_score = _compute_score(fw_findings)
        fw_pass = sum(1 for f in fw_findings if f.status == "PASS")
        fw_fail = sum(1 for f in fw_findings if f.status == "FAIL")
        fw_name = _fw_display_name(fw_id, framework_registry)

        fw_meta = None
        if framework_registry is not None:
            for fm in framework_registry.frameworks:
                if fm.id == fw_id:
                    fw_meta = fm
                    break

        frameworks_detail.append(
            {
                "id": fw_id,
                "name": fw_name,
                "version": fw_meta.version if fw_meta else "",
                "tier": fw_meta.tier if fw_meta else "",
                "control_count": len(fw_findings),
                "pass_count": fw_pass,
                "fail_count": fw_fail,
                "score": fw_score,
            }
        )

    # Cross-mapping section
    cross_mapping_section = _build_cross_mapping_section(report_findings, framework_ids)

    # Upgrade CTA
    upgrade_cta: dict[str, str] | None = None
    if tier == "pro":
        upgrade_cta = {
            "message": (
                "Unlock trending, drift alerts, and cross-validation with Team"
            ),
            "url": "https://complyform.dev/pricing",
        }

    # Metadata
    report_metadata = ReportMetadata(
        tool_version=__version__,
        generated_at=metadata.get("generated_at", datetime.now(UTC).isoformat()),
        cloud=metadata.get("cloud", ""),
        scan_source=metadata.get("scan_source", "state"),
        state_hash=metadata.get("state_hash"),
        frameworks=framework_ids,
        tier=tier,
        project_id=metadata.get("project_id"),
    )

    return ReportData(
        metadata=report_metadata,
        summary=summary,
        findings=report_findings,
        not_assessed_groups=not_assessed_groups,
        cross_mappings=cross_mapping_section,
        cost_impact=cost_impact,
        cross_validation=cross_validation,
        frameworks_detail=frameworks_detail,
        upgrade_cta=upgrade_cta,
    )
