"""SARIF 2.1.0 report generator.

SARIF (Static Analysis Results Interchange Format) is the standard output
format for ecosystem interoperability with:
  - GitHub Code Scanning / GitHub Advanced Security
  - VS Code Problems panel (via SARIF Viewer extension)
  - Azure DevOps pipeline gating
  - Tekton task result consumers

Reference schema: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
JSON schema:      https://json.schemastore.org/sarif-2.1.0.json
"""

from __future__ import annotations

from sanicode.report.persist import ScanResult
from sanicode.scanner.patterns import KNOWN_BAD_PATTERNS

# SARIF 2.1.0 schema URI — must appear in the $schema field of every SARIF document.
SARIF_SCHEMA_URI = "https://json.schemastore.org/sarif-2.1.0.json"
SARIF_VERSION = "2.1.0"

_TOOL_NAME = "sanicode"
_TOOL_INFO_URI = "https://github.com/rdwj/sanicode"
_CWE_URI_TEMPLATE = "https://cwe.mitre.org/data/definitions/{cwe_id}.html"

# Maps sanicode severity levels to SARIF result levels.
_SEVERITY_TO_SARIF_LEVEL: dict[str, str] = {
    "critical": "error",
    "high": "error",
    "medium": "warning",
    "low": "note",
    "info": "note",
}


def _sarif_level(severity: str) -> str:
    """Map a sanicode severity string to a SARIF result level."""
    return _SEVERITY_TO_SARIF_LEVEL.get(severity.lower(), "warning")


def _build_rule_descriptor(rule_id: str, cwe_id: int | None) -> dict:
    """Build a SARIF reportingDescriptor for a single rule."""
    short_description = KNOWN_BAD_PATTERNS.get(rule_id, rule_id)
    descriptor: dict = {
        "id": rule_id,
        "shortDescription": {"text": short_description},
    }
    if cwe_id is not None:
        descriptor["helpUri"] = _CWE_URI_TEMPLATE.format(cwe_id=cwe_id)
    return descriptor


def _build_result(finding: dict) -> dict:
    """Build a single SARIF result object from a serialized finding dict."""
    rule_id: str = finding.get("rule_id", "UNKNOWN")
    severity: str = finding.get("derived_severity") or finding.get("severity", "medium")
    message: str = finding.get("message", "")

    # SARIF regions are 1-based; AST col_offset is 0-based.
    start_line: int = finding.get("line", 1) or 1
    start_column: int = (finding.get("column") or 0) + 1

    result: dict = {
        "ruleId": rule_id,
        "level": _sarif_level(severity),
        "message": {"text": message},
        "locations": [
            {
                "physicalLocation": {
                    "artifactLocation": {
                        "uri": finding.get("file", ""),
                        "uriBaseId": "%SRCROOT%",
                    },
                    "region": {
                        "startLine": start_line,
                        "startColumn": start_column,
                    },
                }
            }
        ],
    }

    # Attach CWE and compliance data as properties for consumers that support it.
    properties: dict = {}
    cwe_id = finding.get("cwe_id")
    if cwe_id is not None:
        properties["cwe_id"] = cwe_id
    compliance = finding.get("compliance")
    if compliance:
        properties["compliance"] = compliance
    if properties:
        result["properties"] = properties

    return result


def generate_sarif(result: ScanResult) -> dict:
    """Generate a SARIF 2.1.0-compliant report dict.

    The returned dict can be serialized with json.dumps() and consumed by any
    SARIF-compatible tool (GitHub Code Scanning, VS Code, Azure DevOps, etc.).

    Args:
        result: A ScanResult produced by build_scan_result() and optionally
                persisted via save_scan_result().

    Returns:
        A SARIF 2.1.0 document as a Python dict, ready for json.dumps().
    """
    findings: list[dict] = result.findings

    # Collect deduplicated rules, preserving first-seen order.
    seen_rule_ids: set[str] = set()
    rules: list[dict] = []
    for finding in findings:
        rule_id = finding.get("rule_id", "UNKNOWN")
        if rule_id not in seen_rule_ids:
            seen_rule_ids.add(rule_id)
            cwe_id = finding.get("cwe_id")
            rules.append(_build_rule_descriptor(rule_id, cwe_id))

    results = [_build_result(f) for f in findings]

    # Ensure the scanned_path URI ends with a slash (SARIF directory convention).
    scanned_path = result.scanned_path.rstrip("/") + "/"
    base_uri = f"file://{scanned_path}"

    return {
        "$schema": SARIF_SCHEMA_URI,
        "version": SARIF_VERSION,
        "runs": [
            {
                "tool": {
                    "driver": {
                        "name": _TOOL_NAME,
                        "version": result.sanicode_version,
                        "informationUri": _TOOL_INFO_URI,
                        "rules": rules,
                    }
                },
                "originalUriBaseIds": {
                    "%SRCROOT%": {"uri": base_uri},
                },
                "results": results,
            }
        ],
    }
