"""Core data models for findings, evidence, and scan results."""

from __future__ import annotations

from datetime import datetime
from enum import StrEnum
from typing import Any

from pydantic import BaseModel, Field


class Severity(StrEnum):
    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"
    INFO = "info"

    @property
    def weight(self) -> int:
        """Numeric weight for sorting/scoring."""
        return {"critical": 5, "high": 4, "medium": 3, "low": 2, "info": 1}[self.value]

    @property
    def colour(self) -> str:
        """Rich terminal colour."""
        return {
            "critical": "bold red",
            "high": "red",
            "medium": "yellow",
            "low": "cyan",
            "info": "dim",
        }[self.value]


class Evidence(BaseModel):
    """A piece of evidence supporting a finding."""

    file: str
    line: int | None = None
    snippet: str | None = None  # redacted for secrets: first4****last4
    context: str | None = None  # surrounding lines / explanation

    model_config = {"frozen": True}


class ComplianceMapping(BaseModel):
    """Maps a finding to one or more compliance framework controls."""

    soc2: list[str] = Field(default_factory=list)       # e.g. ["CC6.1", "CC8.1"]
    cis: list[str] = Field(default_factory=list)         # e.g. ["CIS-GitLab-2.3"]
    owasp_cicd: list[str] = Field(default_factory=list)  # e.g. ["CICD-SEC-4"]
    iso27001: list[str] = Field(default_factory=list)    # e.g. ["A.12.6.1"]

    def is_empty(self) -> bool:
        return not any([self.soc2, self.cis, self.owasp_cicd, self.iso27001])

    def all_controls(self) -> list[str]:
        return self.soc2 + self.cis + self.owasp_cicd + self.iso27001


class Finding(BaseModel):
    """A single security finding produced by an analyzer."""

    id: str                     # e.g. "cicd.no_sast_stage"
    title: str
    description: str
    severity: Severity
    module: str                 # cicd | scm | secrets | deps
    evidence: list[Evidence] = Field(default_factory=list)
    remediation: str
    compliance: ComplianceMapping = Field(default_factory=ComplianceMapping)
    tags: list[str] = Field(default_factory=list)

    def dedup_key(self) -> str:
        """Key for deduplication — same rule + same primary evidence location."""
        primary = self.evidence[0].file if self.evidence else ""
        line = self.evidence[0].line if self.evidence and self.evidence[0].line else 0
        return f"{self.id}:{primary}:{line}"


class ScanStats(BaseModel):
    """Per-module statistics collected during a scan."""

    files_scanned: int = 0
    rules_evaluated: int = 0
    findings_count: int = 0
    extras: dict[str, Any] = Field(default_factory=dict)


class ScanResult(BaseModel):
    """Output of a single module's collection + analysis pass."""

    module: str
    target_path: str
    findings: list[Finding] = Field(default_factory=list)
    stats: ScanStats = Field(default_factory=ScanStats)
    scan_time_ms: int = 0
    errors: list[str] = Field(default_factory=list)  # non-fatal errors during scan

    def by_severity(self, severity: Severity) -> list[Finding]:
        return [f for f in self.findings if f.severity == severity]

    def critical_count(self) -> int:
        return len(self.by_severity(Severity.CRITICAL))

    def high_count(self) -> int:
        return len(self.by_severity(Severity.HIGH))


class RiskGrade(StrEnum):
    """Overall risk grade for the executive summary."""
    A = "A"
    B = "B"
    C = "C"
    D = "D"
    F = "F"


class AggregatedReport(BaseModel):
    """Merged output from all modules — input to all reporters."""

    client_name: str
    engagement_date: str
    target_path: str
    generated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
    sentinel_version: str = "0.1.0"
    results: list[ScanResult] = Field(default_factory=list)
    errors: list[str] = Field(default_factory=list)

    @property
    def all_findings(self) -> list[Finding]:
        findings: list[Finding] = []
        for r in self.results:
            findings.extend(r.findings)
        return findings

    @property
    def critical_count(self) -> int:
        return sum(r.critical_count() for r in self.results)

    @property
    def high_count(self) -> int:
        return sum(r.high_count() for r in self.results)

    @property
    def risk_grade(self) -> RiskGrade:
        c, h = self.critical_count, self.high_count
        if c >= 3:
            return RiskGrade.F
        if c in (1, 2):
            return RiskGrade.D
        if h > 3:
            return RiskGrade.C
        if h > 0:
            return RiskGrade.B
        return RiskGrade.A

    @property
    def findings_by_severity(self) -> dict[str, list[Finding]]:
        result: dict[str, list[Finding]] = {s.value: [] for s in Severity}
        for f in self.all_findings:
            result[f.severity.value].append(f)
        return result

    @property
    def findings_by_module(self) -> dict[str, list[Finding]]:
        result: dict[str, list[Finding]] = {}
        for f in self.all_findings:
            result.setdefault(f.module, []).append(f)
        return result
