"""MCP Scan Diff — Differential scanning for mcpaudit.

Compares two scan results (or a scan vs baseline) and shows:
- New findings (introduced since previous scan)
- Resolved findings (no longer present)
- Modified findings (severity changed)
- Trust score delta

Zero external dependencies. Part of mcpaudit v0.8.0.

Usage:
    mcpaudit diff scan1.json scan2.json
    mcpaudit diff --baseline baseline.json current.json
    mcpaudit diff scan1.json scan2.json --format json
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from .models import Finding, Severity


def _finding_fingerprint(finding_dict: dict[str, Any]) -> str:
    """Generate a stable fingerprint for a finding dict.

    Uses rule_id + file basename + code_snippet for stability
    across line number changes.
    """
    rule_id = finding_dict.get("rule_id", "")
    file_path = finding_dict.get("file", "")
    file_base = Path(file_path).name if file_path else ""
    code = finding_dict.get("code_snippet", "").strip()

    raw = f"{rule_id}|{file_base}|{code}"
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]


def _finding_key(finding_dict: dict[str, Any]) -> str:
    """Generate a key that identifies a finding for diff purposes.

    Uses rule_id + file + description for matching.
    More lenient than fingerprint — catches the 'same issue' even
    if code snippet changed slightly.
    """
    rule_id = finding_dict.get("rule_id", "")
    file_path = finding_dict.get("file", "")
    file_base = Path(file_path).name if file_path else ""
    title = finding_dict.get("title", "")

    raw = f"{rule_id}|{file_base}|{title}"
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]


@dataclass
class DiffFinding:
    """A finding in the diff result."""

    status: str  # "new", "resolved", "modified", "unchanged"
    finding: dict[str, Any]
    previous_severity: str = ""  # Only for "modified"

    def to_dict(self) -> dict[str, Any]:
        result = {
            "status": self.status,
            "finding": self.finding,
        }
        if self.previous_severity:
            result["previous_severity"] = self.previous_severity
        return result


@dataclass
class ScoreDelta:
    """Trust score comparison between two scans."""

    before_score: int = 0
    after_score: int = 0
    before_grade: str = ""
    after_grade: str = ""

    @property
    def delta(self) -> int:
        return self.after_score - self.before_score

    @property
    def improved(self) -> bool:
        return self.delta > 0

    @property
    def degraded(self) -> bool:
        return self.delta < 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "before_score": self.before_score,
            "after_score": self.after_score,
            "delta": self.delta,
            "before_grade": self.before_grade,
            "after_grade": self.after_grade,
            "improved": self.improved,
            "degraded": self.degraded,
        }


@dataclass
class DiffResult:
    """Result of comparing two scans."""

    file_before: str = ""
    file_after: str = ""
    new_findings: list[DiffFinding] = field(default_factory=list)
    resolved_findings: list[DiffFinding] = field(default_factory=list)
    modified_findings: list[DiffFinding] = field(default_factory=list)
    unchanged_findings: list[DiffFinding] = field(default_factory=list)
    score_delta: ScoreDelta = field(default_factory=ScoreDelta)
    severity_delta: dict[str, int] = field(default_factory=dict)

    @property
    def total_new(self) -> int:
        return len(self.new_findings)

    @property
    def total_resolved(self) -> int:
        return len(self.resolved_findings)

    @property
    def total_modified(self) -> int:
        return len(self.modified_findings)

    @property
    def total_unchanged(self) -> int:
        return len(self.unchanged_findings)

    @property
    def exit_code(self) -> int:
        """Exit code: 0=no regressions, 1=new high, 2=new critical."""
        for df in self.new_findings:
            sev = df.finding.get("severity", "")
            if sev == "CRITICAL":
                return 2
        for df in self.new_findings:
            sev = df.finding.get("severity", "")
            if sev == "HIGH":
                return 1
        return 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "file_before": self.file_before,
            "file_after": self.file_after,
            "summary": {
                "new": self.total_new,
                "resolved": self.total_resolved,
                "modified": self.total_modified,
                "unchanged": self.total_unchanged,
                "exit_code": self.exit_code,
            },
            "score_delta": self.score_delta.to_dict(),
            "severity_delta": self.severity_delta,
            "new_findings": [f.to_dict() for f in self.new_findings],
            "resolved_findings": [f.to_dict() for f in self.resolved_findings],
            "modified_findings": [f.to_dict() for f in self.modified_findings],
        }


class ScanDiff:
    """Compares two mcpaudit scan results and produces a diff.

    Identifies new, resolved, and modified findings between two scans.
    """

    def __init__(self) -> None:
        pass

    def diff_files(self, before_path: Path, after_path: Path) -> DiffResult:
        """Compare two scan result JSON files.

        Args:
            before_path: Path to the 'before' scan JSON.
            after_path: Path to the 'after' scan JSON.

        Returns:
            DiffResult with categorized findings.
        """
        before_text = before_path.read_text(encoding="utf-8")
        after_text = after_path.read_text(encoding="utf-8")

        before_data = json.loads(before_text)
        after_data = json.loads(after_text)

        result = self.diff_data(before_data, after_data)
        result.file_before = str(before_path)
        result.file_after = str(after_path)
        return result

    def diff_data(self, before: dict[str, Any], after: dict[str, Any]) -> DiffResult:
        """Compare two scan result dicts.

        Supports both full scan output format and baseline format.

        Args:
            before: 'Before' scan data dict.
            after: 'After' scan data dict.

        Returns:
            DiffResult with categorized findings.
        """
        result = DiffResult()

        before_findings = self._extract_findings(before)
        after_findings = self._extract_findings(after)

        # Build indices by key
        before_by_key: dict[str, dict[str, Any]] = {}
        for f in before_findings:
            key = _finding_key(f)
            before_by_key[key] = f

        after_by_key: dict[str, dict[str, Any]] = {}
        for f in after_findings:
            key = _finding_key(f)
            after_by_key[key] = f

        before_keys = set(before_by_key.keys())
        after_keys = set(after_by_key.keys())

        # New findings: in after but not in before
        for key in sorted(after_keys - before_keys):
            result.new_findings.append(DiffFinding(
                status="new",
                finding=after_by_key[key],
            ))

        # Resolved findings: in before but not in after
        for key in sorted(before_keys - after_keys):
            result.resolved_findings.append(DiffFinding(
                status="resolved",
                finding=before_by_key[key],
            ))

        # Modified or unchanged: in both
        for key in sorted(before_keys & after_keys):
            bf = before_by_key[key]
            af = after_by_key[key]
            before_sev = bf.get("severity", "")
            after_sev = af.get("severity", "")

            if before_sev != after_sev:
                result.modified_findings.append(DiffFinding(
                    status="modified",
                    finding=af,
                    previous_severity=before_sev,
                ))
            else:
                result.unchanged_findings.append(DiffFinding(
                    status="unchanged",
                    finding=af,
                ))

        # Compute severity delta
        before_counts = self._count_severities(before_findings)
        after_counts = self._count_severities(after_findings)
        for sev in Severity:
            key_name = sev.value
            result.severity_delta[key_name] = (
                after_counts.get(key_name, 0) - before_counts.get(key_name, 0)
            )

        # Compute score delta
        result.score_delta = self._compute_score_delta(before, after)

        return result

    def _extract_findings(self, data: dict[str, Any]) -> list[dict[str, Any]]:
        """Extract finding dicts from various scan output formats."""
        findings: list[dict[str, Any]] = []

        # Standard mcpaudit output: {"results": [{"findings": [...]}]}
        if "results" in data:
            for r in data["results"]:
                if isinstance(r, dict) and "findings" in r:
                    findings.extend(r["findings"])
            return findings

        # Baseline format: {"findings": [{"fingerprint": ..., "rule_id": ...}]}
        if "findings" in data and isinstance(data["findings"], list):
            return data["findings"]

        # Guard output: {"findings": [...]}
        if "findings" in data:
            return data["findings"] if isinstance(data["findings"], list) else []

        return findings

    def _count_severities(self, findings: list[dict[str, Any]]) -> dict[str, int]:
        """Count findings by severity."""
        counts: dict[str, int] = {}
        for f in findings:
            sev = f.get("severity", "UNKNOWN")
            counts[sev] = counts.get(sev, 0) + 1
        return counts

    def _compute_score_delta(self, before: dict[str, Any],
                             after: dict[str, Any]) -> ScoreDelta:
        """Extract trust scores from scan data and compute delta."""
        delta = ScoreDelta()

        # Try to extract trust score from summary
        before_summary = before.get("summary", {})
        after_summary = after.get("summary", {})

        before_trust = before_summary.get("trust_score", {})
        after_trust = after_summary.get("trust_score", {})

        if isinstance(before_trust, dict):
            delta.before_score = before_trust.get("score", 0)
            delta.before_grade = before_trust.get("grade", "")
        elif isinstance(before_trust, (int, float)):
            delta.before_score = int(before_trust)

        if isinstance(after_trust, dict):
            delta.after_score = after_trust.get("score", 0)
            delta.after_grade = after_trust.get("grade", "")
        elif isinstance(after_trust, (int, float)):
            delta.after_score = int(after_trust)

        # Fallback: compute from severity counts
        if delta.before_score == 0 and delta.after_score == 0:
            before_findings = self._extract_findings(before)
            after_findings = self._extract_findings(after)
            delta.before_score = self._estimate_score(before_findings)
            delta.after_score = self._estimate_score(after_findings)

        return delta

    def _estimate_score(self, findings: list[dict[str, Any]]) -> int:
        """Estimate a trust score from findings (100 = clean)."""
        if not findings:
            return 100
        weights = {
            "CRITICAL": 10,
            "HIGH": 5,
            "MEDIUM": 2,
            "LOW": 1,
            "INFO": 0,
        }
        penalty = sum(weights.get(f.get("severity", ""), 0) for f in findings)
        return max(0, 100 - penalty)


# --- Formatting ---


def format_diff_text(result: DiffResult) -> str:
    """Format diff result as human-readable text with indicators."""
    lines: list[str] = []
    lines.append(f"\n{'=' * 60}")
    lines.append("  MCP Audit Diff — Scan Comparison")
    lines.append(f"{'=' * 60}")

    if result.file_before:
        lines.append(f"  Before: {result.file_before}")
    if result.file_after:
        lines.append(f"  After:  {result.file_after}")

    lines.append(f"\n  New findings:       +{result.total_new}")
    lines.append(f"  Resolved findings:  -{result.total_resolved}")
    lines.append(f"  Modified findings:   {result.total_modified}")
    lines.append(f"  Unchanged:           {result.total_unchanged}")

    # Score delta
    sd = result.score_delta
    if sd.before_score or sd.after_score:
        delta_str = f"+{sd.delta}" if sd.delta >= 0 else str(sd.delta)
        lines.append(f"\n  Trust score: {sd.before_score} -> {sd.after_score} ({delta_str})")
        if sd.before_grade or sd.after_grade:
            lines.append(f"  Grade:       {sd.before_grade} -> {sd.after_grade}")

    # Severity delta
    if result.severity_delta:
        parts = []
        for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
            d = result.severity_delta.get(sev, 0)
            if d != 0:
                sign = "+" if d > 0 else ""
                parts.append(f"{sev}: {sign}{d}")
        if parts:
            lines.append(f"  Severity delta: {', '.join(parts)}")

    lines.append(f"\n{'=' * 60}")

    # New findings (most important)
    if result.new_findings:
        lines.append(f"\n  [+] NEW FINDINGS ({result.total_new}):")
        lines.append(f"  {'-' * 40}")
        for df in result.new_findings:
            f = df.finding
            sev = f.get("severity", "?")
            rule = f.get("rule_id", "?")
            title = f.get("title", f.get("description", ""))
            file_name = f.get("file", "")
            line_no = f.get("line", 0)
            lines.append(f"  + [{sev}] {rule}: {title}")
            if file_name:
                lines.append(f"    File: {file_name}:{line_no}")

    # Resolved findings
    if result.resolved_findings:
        lines.append(f"\n  [-] RESOLVED FINDINGS ({result.total_resolved}):")
        lines.append(f"  {'-' * 40}")
        for df in result.resolved_findings:
            f = df.finding
            sev = f.get("severity", "?")
            rule = f.get("rule_id", "?")
            title = f.get("title", f.get("description", ""))
            lines.append(f"  - [{sev}] {rule}: {title}")

    # Modified findings
    if result.modified_findings:
        lines.append(f"\n  [~] MODIFIED FINDINGS ({result.total_modified}):")
        lines.append(f"  {'-' * 40}")
        for df in result.modified_findings:
            f = df.finding
            sev = f.get("severity", "?")
            rule = f.get("rule_id", "?")
            title = f.get("title", f.get("description", ""))
            prev = df.previous_severity
            lines.append(f"  ~ [{rule}] {title}: {prev} -> {sev}")

    lines.append("")
    return "\n".join(lines)


def format_diff_json(result: DiffResult) -> str:
    """Format diff result as JSON."""
    return json.dumps(result.to_dict(), indent=2, ensure_ascii=False)


def format_diff_sarif(result: DiffResult) -> str:
    """Format diff result as SARIF 2.1.0.

    Only includes new findings (regressions) in SARIF output,
    as resolved/unchanged are not actionable in SARIF viewers.
    """
    from .reporters.sarif_reporter import (
        SARIF_SCHEMA,
        SARIF_VERSION,
        TOOL_NAME,
        TOOL_URI,
        TOOL_VERSION,
        _severity_to_sarif_level,
        _severity_to_sarif_rank,
    )

    rules_map: dict[str, dict[str, Any]] = {}
    sarif_results: list[dict[str, Any]] = []

    for df in result.new_findings:
        f = df.finding
        rule_id = f.get("rule_id", "UNKNOWN")
        severity_str = f.get("severity", "INFO")

        try:
            severity = Severity[severity_str]
        except KeyError:
            severity = Severity.INFO

        if rule_id not in rules_map:
            rules_map[rule_id] = {
                "id": rule_id,
                "name": f.get("title", rule_id),
                "shortDescription": {"text": f.get("title", "")},
                "fullDescription": {"text": f.get("description", "")},
                "defaultConfiguration": {
                    "level": _severity_to_sarif_level(severity),
                },
                "properties": {
                    "security-severity": str(_severity_to_sarif_rank(severity)),
                    "tags": ["security", "regression"],
                },
            }

        sarif_result: dict[str, Any] = {
            "ruleId": rule_id,
            "level": _severity_to_sarif_level(severity),
            "message": {"text": f.get("description", "")},
            "locations": [
                {
                    "physicalLocation": {
                        "artifactLocation": {
                            "uri": f.get("file", ""),
                            "uriBaseId": "%SRCROOT%",
                        },
                        "region": {
                            "startLine": max(1, f.get("line", 1)),
                            "startColumn": 1,
                        },
                    },
                }
            ],
            "properties": {
                "diffStatus": "new",
            },
        }
        sarif_results.append(sarif_result)

    sarif: dict[str, Any] = {
        "$schema": SARIF_SCHEMA,
        "version": SARIF_VERSION,
        "runs": [
            {
                "tool": {
                    "driver": {
                        "name": TOOL_NAME,
                        "version": TOOL_VERSION,
                        "informationUri": TOOL_URI,
                        "rules": list(rules_map.values()),
                    },
                },
                "results": sarif_results,
                "properties": {
                    "diffSummary": {
                        "new": result.total_new,
                        "resolved": result.total_resolved,
                        "modified": result.total_modified,
                    },
                },
            }
        ],
    }

    return json.dumps(sarif, indent=2, ensure_ascii=False)
