"""Compliance cross-reference mapping.

Maps CWE IDs to control identifiers across:
  - OWASP ASVS 5.0 (Chapter V1: Encoding and Sanitization)
  - NIST 800-53 rev 5
  - ASD STIG v4 r11
  - PCI DSS 4.0 (where applicable)

The authoritative cross-reference database lives in data/compliance_db.json
(inside the package) or at data/compliance_db.json relative to the project
root for editable installs. This module provides typed access and a fallback
in-memory table for the most critical sanitization CWEs.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path


def _find_db_path() -> Path | None:
    """Locate the compliance cross-reference database.

    Checks locations in priority order:
    1. Development / editable install: data/ at the project root (four levels
       up from this file inside src/sanicode/compliance/).
    2. Package-internal data: src/sanicode/data/ sibling to this package.
    3. Legacy installed package: a data/ directory alongside compliance/.
    """
    # Editable install / development: data/ is at project root
    project_root = Path(__file__).resolve().parent.parent.parent.parent
    dev_path = project_root / "data" / "compliance_db.json"
    if dev_path.is_file():
        return dev_path

    # Package-internal: sanicode/data/ is a sibling of sanicode/compliance/
    pkg_path = Path(__file__).resolve().parent.parent / "data" / "compliance_db.json"
    if pkg_path.is_file():
        return pkg_path

    # Legacy fallback: compliance/data/ (inside the compliance package)
    legacy_path = Path(__file__).resolve().parent / "data" / "compliance_db.json"
    if legacy_path.is_file():
        return legacy_path

    return None


_DB_PATH = _find_db_path()

# In-memory fallback for the core sanitization CWEs. Used when the JSON DB
# cannot be found (e.g., partially installed package). The structure mirrors
# the simple list format; map_cwe() normalises it to ComplianceMapping.
_FALLBACK_DB: dict[int, dict] = {
    # CWE-20: Improper Input Validation
    20: {
        "name": "Improper Input Validation",
        "owasp_asvs": ["v5.0.0-V1-5.1.3", "v5.0.0-V1-5.1.4"],
        "nist_800_53": ["SI-10"],
        "asd_stig": ["APSC-DV-002530"],
        "pci_dss": ["6.2.4"],
    },
    # CWE-78: OS Command Injection
    78: {
        "name": "Improper Neutralization of Special Elements used in an OS Command",
        "owasp_asvs": ["v5.0.0-V1-5.3.8"],
        "nist_800_53": ["SI-10", "SI-15"],
        "asd_stig": ["APSC-DV-002510"],
        "pci_dss": ["6.2.4"],
    },
    # CWE-79: Cross-Site Scripting
    79: {
        "name": "Cross-site Scripting",
        "owasp_asvs": ["v5.0.0-V1-5.3.3"],
        "nist_800_53": ["SI-10"],
        "asd_stig": ["APSC-DV-002520"],
        "pci_dss": ["6.2.4"],
    },
    # CWE-89: SQL Injection
    89: {
        "name": "SQL Injection",
        "owasp_asvs": ["v5.0.0-V1-5.3.4", "v5.0.0-V1-5.3.5"],
        "nist_800_53": ["SI-10"],
        "asd_stig": ["APSC-DV-002540"],
        "pci_dss": ["6.2.4"],
    },
    # CWE-94: Code Injection
    94: {
        "name": "Code Injection",
        "owasp_asvs": ["v5.0.0-V1-5.2.4"],
        "nist_800_53": ["SI-10", "SI-15"],
        "asd_stig": ["APSC-DV-002510"],
        "pci_dss": [],
    },
    # CWE-116: Improper Encoding or Escaping of Output
    116: {
        "name": "Improper Encoding or Escaping of Output",
        "owasp_asvs": ["v5.0.0-V1-5.3.1", "v5.0.0-V1-5.3.2"],
        "nist_800_53": ["SI-10"],
        "asd_stig": ["APSC-DV-002520"],
        "pci_dss": [],
    },
    # CWE-502: Deserialization of Untrusted Data
    502: {
        "name": "Deserialization of Untrusted Data",
        "owasp_asvs": ["v5.0.0-V1-5.5.3"],
        "nist_800_53": ["SI-10"],
        "asd_stig": ["APSC-DV-002550"],
        "pci_dss": ["6.2.4"],
    },
    # CWE-918: Server-Side Request Forgery
    918: {
        "name": "Server-Side Request Forgery (SSRF)",
        "owasp_asvs": ["v5.0.0-V1-5.2.7"],
        "nist_800_53": ["SI-10", "SC-7"],
        "asd_stig": [],
        "pci_dss": [],
    },
    # CWE-1333: Inefficient Regular Expression Complexity (ReDoS)
    1333: {
        "name": "Inefficient Regular Expression Complexity",
        "owasp_asvs": ["v5.0.0-V1-5.2.5"],
        "nist_800_53": ["SI-10"],
        "asd_stig": [],
        "pci_dss": [],
    },
}

# CAT level priority for severity derivation (lower index = higher severity)
_STIG_CAT_ORDER = ["CAT I", "CAT II", "CAT III"]


@dataclass
class ComplianceMapping:
    """Compliance control identifiers for a single CWE."""

    cwe_id: int
    cwe_name: str = ""
    description: str = ""
    # Rich format: list of dicts with keys "id", "title", "level"
    owasp_asvs: list[dict[str, str]] = field(default_factory=list)
    nist_800_53: list[str] = field(default_factory=list)
    # Rich format: list of dicts with keys "id", "cat", "title"
    asd_stig: list[dict[str, str]] = field(default_factory=list)
    pci_dss: list[str] = field(default_factory=list)
    # Derived summary fields
    asvs_level: str = ""  # Highest applicable level: "L1", "L2", or "L3"
    stig_category: str = ""  # Highest severity: "CAT I", "CAT II", or "CAT III"
    remediation: str = ""


def _derive_asvs_level(asvs_entries: list[dict[str, str]]) -> str:
    """Return the highest ASVS verification level required across all entries.

    ASVS levels are L1 (opportunistic) → L2 (standard) → L3 (advanced).
    Returns the most demanding level present, which represents the baseline
    assurance level the application must achieve.
    """
    order = {"L1": 1, "L2": 2, "L3": 3}
    best = 0
    for entry in asvs_entries:
        lvl = entry.get("level", "")
        if lvl in order and order[lvl] > best:
            best = order[lvl]
    return {1: "L1", 2: "L2", 3: "L3"}.get(best, "")


def _derive_stig_category(stig_entries: list[dict[str, str]]) -> str:
    """Return the highest-severity STIG category present (CAT I > CAT II > CAT III)."""
    for cat in _STIG_CAT_ORDER:
        for entry in stig_entries:
            if entry.get("cat") == cat or entry.get("cat") == cat.replace("CAT ", ""):
                return cat
    return ""


def _normalise_asvs(raw: list) -> list[dict[str, str]]:
    """Normalise an ASVS list from either rich (dict) or simple (str) format."""
    if not raw:
        return []
    if isinstance(raw[0], dict):
        return raw
    # Simple string list from fallback DB
    return [{"id": item, "title": "", "level": ""} for item in raw]


def _normalise_stig(raw: list) -> list[dict[str, str]]:
    """Normalise a STIG list from either rich (dict) or simple (str) format."""
    if not raw:
        return []
    if isinstance(raw[0], dict):
        return raw
    # Simple string list from fallback DB — CAT level unknown
    return [{"id": item, "cat": "", "title": ""} for item in raw]


def load_cross_reference_db() -> dict[int, dict]:
    """Load the compliance cross-reference database.

    Falls back to the built-in table if the JSON database is not present.

    Returns:
        Mapping from CWE ID (int) to a dict containing compliance data.
    """
    if _DB_PATH is not None:
        with open(_DB_PATH, encoding="utf-8") as fh:
            raw = json.load(fh)
        return {int(k): v for k, v in raw.items()}
    return _FALLBACK_DB


def derive_severity(mapping: ComplianceMapping) -> str:
    """Derive finding severity from the highest STIG category in the mapping.

    CAT I maps to "critical", CAT II to "high", CAT III to "medium".
    Falls back to "medium" when no STIG data is present.

    Args:
        mapping: A populated ComplianceMapping.

    Returns:
        One of "critical", "high", or "medium".
    """
    cat_to_severity = {
        "CAT I": "critical",
        "CAT II": "high",
        "CAT III": "medium",
    }
    if mapping.stig_category in cat_to_severity:
        return cat_to_severity[mapping.stig_category]
    # Fallback: inspect individual entries when stig_category not pre-computed
    for cat in _STIG_CAT_ORDER:
        for entry in mapping.asd_stig:
            entry_cat = entry.get("cat", "")
            if entry_cat == cat or f"CAT {entry_cat}" == cat:
                return cat_to_severity[cat]
    return "medium"


class ComplianceMapper:
    """Maps CWE IDs to compliance control identifiers."""

    def __init__(self, db: dict[int, dict] | None = None) -> None:
        self._db = db if db is not None else load_cross_reference_db()

    def map_cwe(self, cwe_id: int) -> ComplianceMapping:
        """Return the compliance mapping for a given CWE ID.

        Handles both the rich JSON format (dicts for ASVS/STIG entries) and
        the simple fallback format (plain strings). Unknown CWEs return an
        empty mapping with only cwe_id set.

        Args:
            cwe_id: The CWE identifier (e.g., 78 for OS Command Injection).

        Returns:
            A ComplianceMapping with all known control references.
        """
        entry = self._db.get(cwe_id)
        if entry is None:
            return ComplianceMapping(cwe_id=cwe_id)

        asvs = _normalise_asvs(entry.get("owasp_asvs", []))
        stig = _normalise_stig(entry.get("asd_stig", []))

        return ComplianceMapping(
            cwe_id=cwe_id,
            cwe_name=entry.get("name", ""),
            description=entry.get("description", ""),
            owasp_asvs=asvs,
            nist_800_53=entry.get("nist_800_53", []),
            asd_stig=stig,
            pci_dss=entry.get("pci_dss", []),
            asvs_level=_derive_asvs_level(asvs),
            stig_category=_derive_stig_category(stig),
            remediation=entry.get("remediation", ""),
        )

    def map_control(self, framework: str, control_id: str) -> list[ComplianceMapping]:
        """Reverse lookup: find all CWEs that reference a given control.

        Args:
            framework: One of "owasp_asvs", "nist_800_53", "asd_stig", "pci_dss".
            control_id: The control identifier to search for (e.g., "SI-10",
                "APSC-DV-002510", "v5.0.0-V1-5.3.8").

        Returns:
            List of ComplianceMapping objects for CWEs that reference this control.
            Returns an empty list for unknown frameworks or control IDs.
        """
        results: list[ComplianceMapping] = []
        for cwe_id, entry in self._db.items():
            raw = entry.get(framework, [])
            matched = False

            if framework in ("owasp_asvs", "asd_stig"):
                # Rich format: list of dicts; match on "id" key
                for item in raw:
                    if isinstance(item, dict):
                        if item.get("id") == control_id:
                            matched = True
                            break
                    elif item == control_id:
                        # Simple string fallback
                        matched = True
                        break
            elif framework in ("nist_800_53", "pci_dss"):
                matched = control_id in raw

            if matched:
                results.append(self.map_cwe(cwe_id))

        return results
