"""MCP Threat Intelligence Database.

Local threat intelligence database for known MCP security threats.
Maps scan findings to known threat patterns, provides context, IOCs,
and mitigation guidance. Zero external dependencies.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class ThreatSeverity(Enum):
    """Threat severity levels."""

    CRITICAL = "CRITICAL"
    HIGH = "HIGH"
    MEDIUM = "MEDIUM"
    LOW = "LOW"
    INFO = "INFO"

    @classmethod
    def from_string(cls, value: str) -> ThreatSeverity:
        """Parse a severity string (case-insensitive)."""
        try:
            return cls[value.upper()]
        except KeyError:
            valid = ", ".join(s.name.lower() for s in cls)
            raise ValueError(f"Invalid severity '{value}'. Valid: {valid}")


@dataclass
class ThreatEntry:
    """A known MCP threat."""

    id: str
    name: str
    description: str
    severity: ThreatSeverity
    affected_tools: list[str] = field(default_factory=list)
    indicators: list[str] = field(default_factory=list)
    mitigation: str = ""
    references: list[str] = field(default_factory=list)
    tags: list[str] = field(default_factory=list)
    rule_ids: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        """Serialize to dictionary."""
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "severity": self.severity.value,
            "affected_tools": self.affected_tools,
            "indicators": self.indicators,
            "mitigation": self.mitigation,
            "references": self.references,
            "tags": self.tags,
            "rule_ids": self.rule_ids,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ThreatEntry:
        """Deserialize from dictionary."""
        return cls(
            id=data["id"],
            name=data["name"],
            description=data["description"],
            severity=ThreatSeverity.from_string(data["severity"]),
            affected_tools=data.get("affected_tools", []),
            indicators=data.get("indicators", []),
            mitigation=data.get("mitigation", ""),
            references=data.get("references", []),
            tags=data.get("tags", []),
            rule_ids=data.get("rule_ids", []),
        )


@dataclass
class ThreatMatch:
    """A match between a scan finding and a known threat."""

    threat: ThreatEntry
    finding_rule_id: str
    finding_title: str
    finding_file: str
    finding_line: int
    confidence: str = "HIGH"

    def to_dict(self) -> dict[str, Any]:
        """Serialize to dictionary."""
        return {
            "threat_id": self.threat.id,
            "threat_name": self.threat.name,
            "threat_severity": self.threat.severity.value,
            "finding_rule_id": self.finding_rule_id,
            "finding_title": self.finding_title,
            "finding_file": self.finding_file,
            "finding_line": self.finding_line,
            "confidence": self.confidence,
            "mitigation": self.threat.mitigation,
        }


@dataclass
class ThreatReport:
    """Report of threat matches against scan results."""

    matches: list[ThreatMatch] = field(default_factory=list)
    total_threats: int = 0
    total_findings_analyzed: int = 0
    unmatched_threats: list[str] = field(default_factory=list)

    @property
    def severity_counts(self) -> dict[str, int]:
        """Count matches by severity."""
        counts = {s.value: 0 for s in ThreatSeverity}
        for m in self.matches:
            counts[m.threat.severity.value] += 1
        return counts

    @property
    def critical_count(self) -> int:
        return self.severity_counts.get("CRITICAL", 0)

    @property
    def high_count(self) -> int:
        return self.severity_counts.get("HIGH", 0)

    def to_dict(self) -> dict[str, Any]:
        """Serialize to dictionary."""
        return {
            "total_threats_in_db": self.total_threats,
            "total_findings_analyzed": self.total_findings_analyzed,
            "matches_found": len(self.matches),
            "severity_counts": self.severity_counts,
            "matches": [m.to_dict() for m in self.matches],
            "unmatched_threats": self.unmatched_threats,
        }


# ---------------------------------------------------------------------------
# Built-in Threat Database (23 entries)
# ---------------------------------------------------------------------------

def _build_threat_db() -> list[ThreatEntry]:
    """Build the default threat intelligence database."""
    return [
        ThreatEntry(
            id="THREAT-001",
            name="Confused Deputy Attack",
            description=(
                "An MCP server acts as a deputy for the user but can be tricked "
                "into performing actions the user did not intend, by manipulating "
                "tool inputs or contexts."
            ),
            severity=ThreatSeverity.CRITICAL,
            affected_tools=["all tool handlers"],
            indicators=[
                "Missing input validation on tool parameters",
                "No authorization checks before privileged operations",
                "Tool executes actions based solely on LLM-provided context",
            ],
            mitigation=(
                "Implement strict input validation, enforce authorization checks, "
                "and require user confirmation for privileged operations."
            ),
            references=["OWASP Agentic Top 10: A01"],
            tags=["owasp", "authorization", "deputy"],
            rule_ids=["AUTH-001", "AUTH-002", "AUTH-003", "MCP-AUTH-001"],
        ),
        ThreatEntry(
            id="THREAT-002",
            name="Prompt Injection via Tool Output",
            description=(
                "Malicious data returned by an MCP tool is interpreted as "
                "instructions by the LLM, causing unintended behavior."
            ),
            severity=ThreatSeverity.CRITICAL,
            affected_tools=["tools returning user-generated content"],
            indicators=[
                "Tool output includes unescaped user content",
                "No output sanitization before returning to LLM",
                "Tool returns HTML or markdown with embedded instructions",
            ],
            mitigation=(
                "Sanitize all tool outputs, use structured data formats, "
                "and implement output length limits."
            ),
            references=["OWASP Agentic Top 10: A02"],
            tags=["owasp", "injection", "prompt"],
            rule_ids=["INJ-001", "INJ-002", "INJ-003", "MCP-INJ-001", "MCP-INJ-002"],
        ),
        ThreatEntry(
            id="THREAT-003",
            name="Tool Poisoning",
            description=(
                "An MCP tool is modified or replaced with a malicious version "
                "that exfiltrates data or runs unauthorized commands."
            ),
            severity=ThreatSeverity.CRITICAL,
            affected_tools=["dynamically loaded tools", "plugin systems"],
            indicators=[
                "Dynamic tool loading without integrity verification",
                "Tool definitions fetched from untrusted sources",
                "No hash or signature verification of tool code",
            ],
            mitigation=(
                "Pin tool versions, verify integrity with checksums or signatures, "
                "and restrict dynamic tool loading."
            ),
            references=["OWASP Agentic Top 10: A03"],
            tags=["owasp", "supply-chain", "poisoning"],
            rule_ids=["TOOL-001", "TOOL-002", "MCP-TOOL-001"],
        ),
        ThreatEntry(
            id="THREAT-004",
            name="Data Exfiltration via MCP Tools",
            description=(
                "An MCP tool sends sensitive data to external endpoints, either "
                "through explicit network calls or by encoding data in tool outputs."
            ),
            severity=ThreatSeverity.CRITICAL,
            affected_tools=["tools with network access", "file system tools"],
            indicators=[
                "Outbound HTTP requests in tool handlers",
                "DNS lookups with encoded data",
                "File writes to world-readable locations",
            ],
            mitigation=(
                "Restrict network access, implement egress filtering, "
                "and audit all outbound connections."
            ),
            references=["CWE-200", "OWASP Agentic Top 10: A06"],
            tags=["exfiltration", "network", "data-loss"],
            rule_ids=["NET-001", "NET-002", "EXFIL-001", "MCP-NET-001"],
        ),
        ThreatEntry(
            id="THREAT-005",
            name="Command Injection in Tool Handlers",
            description=(
                "Tool handlers run system commands with unsanitized input, "
                "allowing arbitrary command execution."
            ),
            severity=ThreatSeverity.CRITICAL,
            affected_tools=["tools running system commands"],
            indicators=[
                "subprocess.call with shell=True",
                "os.system() calls with user input",
                "eval() or exec() on tool parameters",
            ],
            mitigation=(
                "Never use shell=True, use subprocess with argument lists, "
                "validate and sanitize all inputs before command execution."
            ),
            references=["CWE-78"],
            tags=["injection", "command", "rce"],
            rule_ids=["CMD-001", "CMD-002", "CMD-003"],
        ),
        ThreatEntry(
            id="THREAT-006",
            name="Insecure Deserialization",
            description=(
                "MCP tools deserialize untrusted data using pickle, yaml.load, "
                "or similar unsafe methods, enabling remote code execution."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["tools processing serialized data"],
            indicators=[
                "pickle.loads() on untrusted input",
                "yaml.load() without SafeLoader",
                "marshal.loads() on external data",
            ],
            mitigation=(
                "Use JSON for serialization, use yaml.safe_load(), "
                "never deserialize untrusted data with pickle."
            ),
            references=["CWE-502"],
            tags=["deserialization", "rce"],
            rule_ids=["SER-001", "SER-002"],
        ),
        ThreatEntry(
            id="THREAT-007",
            name="Path Traversal in File Operations",
            description=(
                "MCP tools perform file operations with user-controlled paths "
                "without proper sanitization, allowing access to arbitrary files."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["file reader tools", "file writer tools"],
            indicators=[
                "File paths constructed from user input without validation",
                "No path normalization or chroot enforcement",
                "Symlink following without checks",
            ],
            mitigation=(
                "Normalize paths with os.path.realpath(), enforce a base directory, "
                "and reject paths containing '..'."
            ),
            references=["CWE-22"],
            tags=["path-traversal", "file-system"],
            rule_ids=["PATH-001", "PATH-002", "FS-001"],
        ),
        ThreatEntry(
            id="THREAT-008",
            name="Missing Rate Limiting",
            description=(
                "MCP server does not implement rate limiting, allowing "
                "resource exhaustion through rapid tool invocations."
            ),
            severity=ThreatSeverity.MEDIUM,
            affected_tools=["all tool handlers"],
            indicators=[
                "No request throttling mechanism",
                "No per-client rate limits",
                "No resource consumption monitoring",
            ],
            mitigation=(
                "Implement per-client rate limiting, add request throttling, "
                "and monitor resource consumption."
            ),
            references=["CWE-770"],
            tags=["dos", "rate-limit", "availability"],
            rule_ids=["RATE-001", "RATE-002"],
        ),
        ThreatEntry(
            id="THREAT-009",
            name="Insufficient Logging and Monitoring",
            description=(
                "MCP server does not log tool invocations, errors, or security "
                "events, making incident detection and forensics impossible."
            ),
            severity=ThreatSeverity.MEDIUM,
            affected_tools=["all tool handlers"],
            indicators=[
                "No logging in tool handlers",
                "Security events not recorded",
                "No audit trail for tool invocations",
            ],
            mitigation=(
                "Log all tool invocations with parameters, implement security "
                "event logging, and set up monitoring alerts."
            ),
            references=["CWE-778", "OWASP Agentic Top 10: A09"],
            tags=["logging", "monitoring", "observability"],
            rule_ids=["LOG-001", "LOG-002", "MCP-LOG-001"],
        ),
        ThreatEntry(
            id="THREAT-010",
            name="Hardcoded Secrets",
            description=(
                "MCP server contains hardcoded API keys, passwords, or tokens "
                "that can be extracted by anyone with access to the source."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["tools with API integrations"],
            indicators=[
                "String literals matching API key patterns",
                "Hardcoded passwords in configuration",
                "Embedded tokens in source code",
            ],
            mitigation=(
                "Use environment variables or secret managers, never hardcode "
                "credentials, and implement secret scanning in CI."
            ),
            references=["CWE-798"],
            tags=["secrets", "credentials"],
            rule_ids=["SEC-001", "SEC-002", "CRED-001"],
        ),
        ThreatEntry(
            id="THREAT-011",
            name="SQL Injection via Tool Parameters",
            description=(
                "MCP tools construct SQL queries from user input without "
                "parameterization, enabling database manipulation."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["database query tools"],
            indicators=[
                "String concatenation in SQL queries",
                "f-string or format() in SQL construction",
                "No use of parameterized queries",
            ],
            mitigation=(
                "Always use parameterized queries, never concatenate user "
                "input into SQL strings."
            ),
            references=["CWE-89"],
            tags=["injection", "sql", "database"],
            rule_ids=["SQL-001", "SQL-002"],
        ),
        ThreatEntry(
            id="THREAT-012",
            name="Server-Side Request Forgery (SSRF)",
            description=(
                "MCP tools make HTTP requests to URLs provided by users without "
                "validation, enabling access to internal services."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["URL fetching tools", "webhook tools"],
            indicators=[
                "HTTP requests to user-provided URLs",
                "No URL allowlist enforcement",
                "Access to internal network addresses",
            ],
            mitigation=(
                "Validate URLs against an allowlist, block internal IP ranges, "
                "and use a proxy for outbound requests."
            ),
            references=["CWE-918"],
            tags=["ssrf", "network"],
            rule_ids=["NET-001", "SSRF-001"],
        ),
        ThreatEntry(
            id="THREAT-013",
            name="Excessive Tool Permissions",
            description=(
                "MCP tools request or have more permissions than needed, "
                "violating the principle of least privilege."
            ),
            severity=ThreatSeverity.MEDIUM,
            affected_tools=["all tool handlers"],
            indicators=[
                "Tools with write access that only need read",
                "Full filesystem access instead of restricted paths",
                "Admin-level API keys for read-only operations",
            ],
            mitigation=(
                "Apply principle of least privilege, restrict tool permissions "
                "to the minimum required for their function."
            ),
            references=["CWE-250", "OWASP Agentic Top 10: A04"],
            tags=["permissions", "least-privilege"],
            rule_ids=["AUTH-001", "PERM-001"],
        ),
        ThreatEntry(
            id="THREAT-014",
            name="Insecure TLS Configuration",
            description=(
                "MCP server or tools disable TLS verification, use outdated "
                "protocols, or accept invalid certificates."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["tools with HTTPS connections"],
            indicators=[
                "verify=False in HTTP requests",
                "SSL certificate verification disabled",
                "TLSv1.0 or TLSv1.1 in use",
            ],
            mitigation=(
                "Always verify TLS certificates, use TLS 1.2+ minimum, "
                "and pin certificates for critical connections."
            ),
            references=["CWE-295"],
            tags=["tls", "crypto", "network"],
            rule_ids=["TLS-001", "CRYPTO-001"],
        ),
        ThreatEntry(
            id="THREAT-015",
            name="Information Disclosure in Error Messages",
            description=(
                "MCP tools expose internal details (stack traces, file paths, "
                "database schemas) in error responses."
            ),
            severity=ThreatSeverity.MEDIUM,
            affected_tools=["all tool handlers"],
            indicators=[
                "Full stack traces in error responses",
                "Internal file paths in error messages",
                "Database connection strings in errors",
            ],
            mitigation=(
                "Return generic error messages to clients, log detailed errors "
                "server-side only, and implement error handling middleware."
            ),
            references=["CWE-209"],
            tags=["info-disclosure", "error-handling"],
            rule_ids=["ERR-001", "INFO-001"],
        ),
        ThreatEntry(
            id="THREAT-016",
            name="Cross-Tool Data Leakage",
            description=(
                "Data from one tool invocation leaks to another through shared "
                "state, global variables, or caching mechanisms."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["tools sharing state", "stateful servers"],
            indicators=[
                "Global mutable state between tool invocations",
                "Shared caches without isolation",
                "Thread-local storage misuse",
            ],
            mitigation=(
                "Isolate tool invocations, use immutable data structures, "
                "and implement per-request contexts."
            ),
            references=["CWE-212"],
            tags=["data-leak", "isolation"],
            rule_ids=["LEAK-001", "STATE-001"],
        ),
        ThreatEntry(
            id="THREAT-017",
            name="Unvalidated Redirects",
            description=(
                "MCP tools follow redirects to untrusted destinations, "
                "potentially leaking authentication tokens."
            ),
            severity=ThreatSeverity.MEDIUM,
            affected_tools=["URL fetching tools"],
            indicators=[
                "HTTP client following redirects without validation",
                "Authorization headers sent to redirect targets",
                "No redirect limit enforcement",
            ],
            mitigation=(
                "Disable automatic redirects, validate redirect targets, "
                "and strip auth headers on cross-origin redirects."
            ),
            references=["CWE-601"],
            tags=["redirect", "network"],
            rule_ids=["NET-001", "REDIR-001"],
        ),
        ThreatEntry(
            id="THREAT-018",
            name="Weak Cryptography",
            description=(
                "MCP tools use weak or broken cryptographic algorithms "
                "(MD5, SHA1 for security, DES, RC4) for sensitive operations."
            ),
            severity=ThreatSeverity.MEDIUM,
            affected_tools=["tools with crypto operations"],
            indicators=[
                "MD5 or SHA1 used for password hashing",
                "DES or RC4 encryption",
                "ECB mode in AES",
            ],
            mitigation=(
                "Use SHA-256+ for hashing, AES-GCM for encryption, "
                "and bcrypt/scrypt/argon2 for passwords."
            ),
            references=["CWE-327"],
            tags=["crypto", "weak-cipher"],
            rule_ids=["CRYPTO-001", "CRYPTO-002"],
        ),
        ThreatEntry(
            id="THREAT-019",
            name="Denial of Service via Resource Exhaustion",
            description=(
                "MCP tools can be exploited to consume excessive CPU, memory, "
                "or disk resources through crafted inputs."
            ),
            severity=ThreatSeverity.MEDIUM,
            affected_tools=["tools processing user data"],
            indicators=[
                "No input size limits",
                "Recursive processing without depth limits",
                "Unbounded memory allocation based on user input",
            ],
            mitigation=(
                "Enforce input size limits, set recursion depth bounds, "
                "implement timeouts for all operations."
            ),
            references=["CWE-400"],
            tags=["dos", "resource-exhaustion"],
            rule_ids=["DOS-001", "RATE-001"],
        ),
        ThreatEntry(
            id="THREAT-020",
            name="Rug Pull via Dynamic Tool Modification",
            description=(
                "An MCP server modifies tool definitions or behavior at runtime "
                "after initial trust establishment, changing what tools do."
            ),
            severity=ThreatSeverity.CRITICAL,
            affected_tools=["dynamic tool registries"],
            indicators=[
                "Tool definitions change between invocations",
                "Runtime modification of tool handlers",
                "Dynamic code loading or eval in tool definitions",
            ],
            mitigation=(
                "Lock tool definitions after registration, hash and verify "
                "tool implementations, alert on definition changes."
            ),
            references=["OWASP Agentic Top 10: A05"],
            tags=["rug-pull", "dynamic", "trust"],
            rule_ids=["TOOL-001", "TOOL-002", "MCP-TOOL-001"],
        ),
        ThreatEntry(
            id="THREAT-021",
            name="Privilege Escalation via Tool Chaining",
            description=(
                "Combining multiple low-privilege tools to achieve actions "
                "that should require higher privileges."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["tool chains", "multi-tool workflows"],
            indicators=[
                "No composite permission checks",
                "Individual tools don't verify cumulative impact",
                "Tool chains bypass authorization boundaries",
            ],
            mitigation=(
                "Implement workflow-level authorization, track cumulative "
                "tool actions, and enforce permission boundaries."
            ),
            references=["CWE-269", "OWASP Agentic Top 10: A07"],
            tags=["privilege-escalation", "chaining"],
            rule_ids=["AUTH-001", "AUTH-002", "CHAIN-001"],
        ),
        ThreatEntry(
            id="THREAT-022",
            name="Insecure Direct Object Reference",
            description=(
                "MCP tools expose internal object identifiers (file paths, "
                "database IDs) without access control checks."
            ),
            severity=ThreatSeverity.HIGH,
            affected_tools=["resource access tools"],
            indicators=[
                "Direct use of user-provided IDs for resource access",
                "No ownership verification before data access",
                "Predictable resource identifiers",
            ],
            mitigation=(
                "Implement access control checks on all resource access, "
                "use indirect references, verify ownership."
            ),
            references=["CWE-639"],
            tags=["idor", "access-control"],
            rule_ids=["AUTH-001", "IDOR-001"],
        ),
        ThreatEntry(
            id="THREAT-023",
            name="Unsafe Regex in Input Validation",
            description=(
                "MCP tools use regex patterns vulnerable to ReDoS "
                "(Regular Expression Denial of Service) for input validation."
            ),
            severity=ThreatSeverity.LOW,
            affected_tools=["tools with regex-based validation"],
            indicators=[
                "Nested quantifiers in regex patterns",
                "Catastrophic backtracking possible",
                "No regex evaluation timeout",
            ],
            mitigation=(
                "Audit regex patterns for ReDoS, use atomic groups or "
                "possessive quantifiers, set evaluation timeouts."
            ),
            references=["CWE-1333"],
            tags=["redos", "regex", "dos"],
            rule_ids=["DOS-001", "REGEX-001"],
        ),
    ]


class ThreatIntelDB:
    """Local threat intelligence database for MCP security threats."""

    def __init__(self, threats: list[ThreatEntry] | None = None) -> None:
        """Initialize with threats. Uses built-in DB if none provided."""
        self.threats: list[ThreatEntry] = (
            threats if threats is not None else _build_threat_db()
        )
        self._by_id: dict[str, ThreatEntry] = {t.id: t for t in self.threats}
        self._by_rule: dict[str, list[ThreatEntry]] = {}
        for t in self.threats:
            for rid in t.rule_ids:
                self._by_rule.setdefault(rid, []).append(t)

    def get_threat(self, threat_id: str) -> ThreatEntry | None:
        """Look up a threat by ID."""
        return self._by_id.get(threat_id)

    def list_threats(self) -> list[ThreatEntry]:
        """Return all threats in the database."""
        return list(self.threats)

    def filter_by_severity(self, severity: ThreatSeverity) -> list[ThreatEntry]:
        """Return threats matching the given severity."""
        return [t for t in self.threats if t.severity == severity]

    def filter_by_tag(self, tag: str) -> list[ThreatEntry]:
        """Return threats matching the given tag."""
        tag_lower = tag.lower()
        return [
            t for t in self.threats
            if tag_lower in [tg.lower() for tg in t.tags]
        ]

    def search(self, query: str) -> list[ThreatEntry]:
        """Search threats by name, description, or tags."""
        q = query.lower()
        results = []
        for t in self.threats:
            if (
                q in t.name.lower()
                or q in t.description.lower()
                or any(q in tag.lower() for tag in t.tags)
                or q in t.id.lower()
            ):
                results.append(t)
        return results

    def threats_for_rule(self, rule_id: str) -> list[ThreatEntry]:
        """Return threats associated with a specific rule ID."""
        return self._by_rule.get(rule_id, [])

    @property
    def count(self) -> int:
        """Total number of threats in the database."""
        return len(self.threats)


def match_threats(
    findings: list[dict[str, Any]],
    db: ThreatIntelDB | None = None,
) -> list[ThreatMatch]:
    """Map scan findings to known threats.

    Args:
        findings: List of finding dicts (with rule_id, title, file, line).
        db: Optional ThreatIntelDB. Uses default DB if not provided.

    Returns:
        List of ThreatMatch objects linking findings to known threats.
    """
    if db is None:
        db = ThreatIntelDB()

    matches: list[ThreatMatch] = []
    for finding in findings:
        rule_id = finding.get("rule_id", "")
        related_threats = db.threats_for_rule(rule_id)
        for threat in related_threats:
            matches.append(ThreatMatch(
                threat=threat,
                finding_rule_id=rule_id,
                finding_title=finding.get("title", ""),
                finding_file=finding.get("file", ""),
                finding_line=finding.get("line", 0),
                confidence=_compute_confidence(finding, threat),
            ))
    return matches


def _compute_confidence(finding: dict[str, Any], threat: ThreatEntry) -> str:
    """Compute confidence of a threat match based on indicators."""
    rule_id = finding.get("rule_id", "")
    title = finding.get("title", "").lower()
    desc = finding.get("description", "").lower()

    if rule_id not in threat.rule_ids:
        return "LOW"

    # Check indicator overlap for higher confidence
    indicator_hits = 0
    for indicator in threat.indicators:
        words = indicator.lower().split()[:3]
        if any(word in title or word in desc for word in words):
            indicator_hits += 1

    if indicator_hits >= 2:
        return "HIGH"
    if indicator_hits >= 1:
        return "HIGH"
    return "MEDIUM"


def match_threats_from_scan(
    scan_result: dict[str, Any],
    db: ThreatIntelDB | None = None,
) -> list[ThreatMatch]:
    """Match threats from a full scan result dict.

    Args:
        scan_result: Scan result dict with 'results' list of file results.
        db: Optional ThreatIntelDB.

    Returns:
        List of ThreatMatch objects.
    """
    findings: list[dict[str, Any]] = []
    results = scan_result.get("results", [])
    for result in results:
        for f in result.get("findings", []):
            findings.append(f)
    return match_threats(findings, db)


def generate_threat_report(
    matches: list[ThreatMatch],
    db: ThreatIntelDB | None = None,
) -> ThreatReport:
    """Generate a threat intelligence report.

    Args:
        matches: List of ThreatMatch objects.
        db: Optional ThreatIntelDB for total threat count.

    Returns:
        ThreatReport with summary and details.
    """
    if db is None:
        db = ThreatIntelDB()

    matched_ids = {m.threat.id for m in matches}
    all_ids = {t.id for t in db.threats}
    unmatched = sorted(all_ids - matched_ids)

    unique_findings = {
        (m.finding_rule_id, m.finding_file, m.finding_line) for m in matches
    }

    return ThreatReport(
        matches=matches,
        total_threats=db.count,
        total_findings_analyzed=len(unique_findings),
        unmatched_threats=unmatched,
    )


def format_threat_text(report: ThreatReport) -> str:
    """Format a threat report as human-readable text."""
    lines: list[str] = []
    lines.append("=" * 60)
    lines.append("MCP Threat Intelligence Report")
    lines.append("=" * 60)
    lines.append(f"Threats in database: {report.total_threats}")
    lines.append(f"Findings analyzed:   {report.total_findings_analyzed}")
    lines.append(f"Threats matched:     {len(report.matches)}")
    lines.append("")

    counts = report.severity_counts
    lines.append("Severity breakdown:")
    for sev in ThreatSeverity:
        count = counts.get(sev.value, 0)
        if count > 0:
            lines.append(f"  {sev.value:<10} {count}")
    lines.append("")

    if report.matches:
        lines.append("-" * 60)
        lines.append("Matched Threats:")
        lines.append("-" * 60)
        seen: set[str] = set()
        for m in report.matches:
            if m.threat.id not in seen:
                seen.add(m.threat.id)
                lines.append("")
                lines.append(
                    f"  [{m.threat.severity.value}] "
                    f"{m.threat.id}: {m.threat.name}"
                )
                desc = m.threat.description
                if len(desc) > 100:
                    desc = desc[:100] + "..."
                lines.append(f"  {desc}")
                mit = m.threat.mitigation
                if len(mit) > 100:
                    mit = mit[:100] + "..."
                lines.append(f"  Mitigation: {mit}")
        lines.append("")

    if report.unmatched_threats:
        lines.append(f"Unmatched threats: {len(report.unmatched_threats)}")

    return "\n".join(lines)


def format_threat_json(report: ThreatReport) -> str:
    """Format a threat report as JSON."""
    return json.dumps(report.to_dict(), indent=2)


def format_threats_list_text(threats: list[ThreatEntry]) -> str:
    """Format a list of threats as text table."""
    lines: list[str] = []
    lines.append(f"{'ID':<14} {'Severity':<10} {'Name'}")
    lines.append(f"{'---' * 5:<14} {'---' * 4:<10} {'---' * 14}")
    for t in threats:
        lines.append(f"{t.id:<14} {t.severity.value:<10} {t.name}")
    lines.append(f"\n{len(threats)} threats in database")
    return "\n".join(lines)
