"""SARIF CI Integration — Enhanced SARIF output for CI/CD platforms.

Generates SARIF 2.1.0, GitHub Code Scanning annotations, GitLab SAST,
and Azure DevOps compatible output from mcpaudit scan results.
Zero external dependencies — pure Python 3.11+ stdlib.
"""

from __future__ import annotations

import hashlib
import json
from typing import Any
from urllib.parse import quote

from . import __version__
from .models import Finding, ScanResult, ScanSummary, Severity

SARIF_VERSION = "2.1.0"
SARIF_SCHEMA = (
    "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/"
    "main/sarif-2.1/schema/sarif-schema-2.1.0.json"
)
TOOL_NAME = "mcpaudit"
TOOL_URI = "https://github.com/cmiretf/mcpaudit"


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _severity_to_sarif_level(severity: Severity) -> str:
    """Map mcpaudit severity to SARIF level."""
    return {
        Severity.CRITICAL: "error",
        Severity.HIGH: "error",
        Severity.MEDIUM: "warning",
        Severity.LOW: "note",
        Severity.INFO: "note",
    }[severity]


def _severity_to_sarif_rank(severity: Severity) -> float:
    """Map mcpaudit severity to SARIF security-severity rank (0-10)."""
    return {
        Severity.CRITICAL: 9.5,
        Severity.HIGH: 7.5,
        Severity.MEDIUM: 5.0,
        Severity.LOW: 2.5,
        Severity.INFO: 1.0,
    }[severity]


def _confidence_to_precision(confidence: str) -> str:
    """Map confidence level to SARIF precision."""
    return {
        "HIGH": "high",
        "MEDIUM": "medium",
        "LOW": "low",
    }.get(confidence, "medium")


def _compute_fingerprint(finding: Finding) -> str:
    """Compute a stable fingerprint for dedup across runs.

    Uses rule_id + file + line + title to generate a SHA-256 hash.
    """
    key = f"{finding.rule_id}:{finding.file}:{finding.line}:{finding.title}"
    return hashlib.sha256(key.encode("utf-8")).hexdigest()[:32]


def _encode_uri(filepath: str) -> str:
    """Encode a filepath for SARIF artifact URI."""
    # Replace backslashes, URI-encode special chars except /
    return quote(filepath.replace("\\", "/"), safe="/")


def _build_reporting_descriptor(finding: Finding) -> dict[str, Any]:
    """Build a SARIF reportingDescriptor from a finding."""
    descriptor: dict[str, Any] = {
        "id": finding.rule_id,
        "name": finding.title.replace(" ", ""),
        "shortDescription": {"text": finding.title},
        "fullDescription": {"text": finding.description},
        "defaultConfiguration": {
            "level": _severity_to_sarif_level(finding.severity),
        },
        "properties": {
            "security-severity": str(_severity_to_sarif_rank(finding.severity)),
            "precision": _confidence_to_precision(finding.confidence),
            "tags": ["security", "mcp"],
        },
    }
    if finding.cwe:
        descriptor["properties"]["tags"].append(finding.cwe)
        descriptor["relationships"] = [
            {
                "target": {
                    "id": finding.cwe,
                    "guid": hashlib.sha256(finding.cwe.encode()).hexdigest()[:36],
                    "toolComponent": {"name": "CWE"},
                },
                "kinds": ["superset"],
            }
        ]
    if finding.recommendation:
        descriptor["help"] = {
            "text": finding.recommendation,
            "markdown": f"**Recommendation:** {finding.recommendation}",
        }
    return descriptor


def _build_sarif_result(finding: Finding) -> dict[str, Any]:
    """Build a SARIF result object from a finding."""
    result: dict[str, Any] = {
        "ruleId": finding.rule_id,
        "ruleIndex": 0,  # Will be set by caller
        "level": _severity_to_sarif_level(finding.severity),
        "message": {"text": finding.description},
        "locations": [
            {
                "physicalLocation": {
                    "artifactLocation": {
                        "uri": _encode_uri(finding.file),
                        "uriBaseId": "%SRCROOT%",
                    },
                    "region": {
                        "startLine": max(1, finding.line),
                        "startColumn": 1,
                    },
                },
            }
        ],
        "fingerprints": {
            "primaryLocationLineHash": _compute_fingerprint(finding),
        },
        "partialFingerprints": {
            "primaryLocationLineHash/v1": _compute_fingerprint(finding),
        },
        "properties": {
            "security-severity": str(_severity_to_sarif_rank(finding.severity)),
            "confidence": finding.confidence,
        },
    }

    if finding.code_snippet:
        result["locations"][0]["physicalLocation"]["region"]["snippet"] = {
            "text": finding.code_snippet,
        }
        # Code flow support for data flow findings
        result["codeFlows"] = [
            {
                "threadFlows": [
                    {
                        "locations": [
                            {
                                "location": {
                                    "physicalLocation": {
                                        "artifactLocation": {
                                            "uri": _encode_uri(finding.file),
                                            "uriBaseId": "%SRCROOT%",
                                        },
                                        "region": {
                                            "startLine": max(1, finding.line),
                                            "snippet": {"text": finding.code_snippet},
                                        },
                                    },
                                    "message": {"text": finding.description},
                                },
                            }
                        ],
                    }
                ],
            }
        ]

    if finding.recommendation:
        result["fixes"] = [
            {
                "description": {"text": finding.recommendation},
            }
        ]

    return result


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def generate_sarif_upload(
    scan_results: list[ScanResult],
    tool_name: str = TOOL_NAME,
) -> dict[str, Any]:
    """Generate SARIF 2.1.0 JSON for upload to GitHub Code Scanning / SARIF consumers.

    Full SARIF 2.1.0 spec compliance with:
    - Tool driver with all rules as reportingDescriptors
    - Artifact locations with URI encoding
    - Code flow support for data flow findings
    - Fingerprints for dedup across runs

    Args:
        scan_results: List of ScanResult from mcpaudit scanner.
        tool_name: Name of the tool (default: "mcpaudit").

    Returns:
        SARIF JSON dict (not serialized).
    """
    all_findings: list[Finding] = []
    for r in scan_results:
        all_findings.extend(r.findings)

    # Build rules (reportingDescriptors), dedup by rule_id
    rules_map: dict[str, dict[str, Any]] = {}
    rule_index: dict[str, int] = {}
    for f in all_findings:
        if f.rule_id not in rules_map:
            rule_index[f.rule_id] = len(rules_map)
            rules_map[f.rule_id] = _build_reporting_descriptor(f)

    # Build results
    sarif_results: list[dict[str, Any]] = []
    for f in all_findings:
        result = _build_sarif_result(f)
        result["ruleIndex"] = rule_index.get(f.rule_id, 0)
        sarif_results.append(result)

    # Build artifact list
    artifact_uris: set[str] = set()
    for r in scan_results:
        artifact_uris.add(_encode_uri(r.file))
    artifacts = [
        {"location": {"uri": uri, "uriBaseId": "%SRCROOT%"}}
        for uri in sorted(artifact_uris)
    ]

    summary = ScanSummary(results=scan_results)

    sarif: dict[str, Any] = {
        "$schema": SARIF_SCHEMA,
        "version": SARIF_VERSION,
        "runs": [
            {
                "tool": {
                    "driver": {
                        "name": tool_name,
                        "version": __version__,
                        "semanticVersion": __version__,
                        "informationUri": TOOL_URI,
                        "rules": list(rules_map.values()),
                        "properties": {
                            "tags": [
                                "mcp",
                                "security",
                                "static-analysis",
                                "owasp-agentic",
                            ],
                        },
                    },
                },
                "artifacts": artifacts,
                "results": sarif_results,
                "invocations": [
                    {
                        "executionSuccessful": True,
                        "toolExecutionNotifications": [],
                        "properties": {
                            "filesScanned": summary.total_files,
                            "linesScanned": summary.total_lines,
                            "overallGrade": summary.overall_grade,
                            "scanTimeMs": round(summary.total_scan_time_ms, 2),
                        },
                    }
                ],
                "columnKind": "utf16CodeUnits",
            }
        ],
    }

    return sarif


def generate_github_annotation(finding: Finding) -> dict[str, Any]:
    """Generate a GitHub Actions annotation format for a single finding.

    Compatible with ``::error file=...`` format for GitHub Code Scanning.

    Args:
        finding: A single security finding.

    Returns:
        Dict with annotation fields (annotation_level, path, start_line, etc.).
    """
    level_map = {
        Severity.CRITICAL: "failure",
        Severity.HIGH: "failure",
        Severity.MEDIUM: "warning",
        Severity.LOW: "notice",
        Severity.INFO: "notice",
    }

    return {
        "annotation_level": level_map.get(finding.severity, "warning"),
        "path": finding.file,
        "start_line": max(1, finding.line),
        "end_line": max(1, finding.line),
        "title": f"[{finding.rule_id}] {finding.title}",
        "message": finding.description,
        "raw_details": finding.code_snippet or "",
    }


def generate_gitlab_sast(scan_results: list[ScanResult]) -> dict[str, Any]:
    """Generate GitLab SAST JSON report format.

    Compatible with GitLab Security Dashboard (gl-sast-report.json).

    Args:
        scan_results: List of ScanResult from mcpaudit scanner.

    Returns:
        GitLab SAST report dict.
    """
    vulnerabilities: list[dict[str, Any]] = []

    severity_map = {
        Severity.CRITICAL: "Critical",
        Severity.HIGH: "High",
        Severity.MEDIUM: "Medium",
        Severity.LOW: "Low",
        Severity.INFO: "Info",
    }

    confidence_map = {
        "HIGH": "High",
        "MEDIUM": "Medium",
        "LOW": "Low",
    }

    for sr in scan_results:
        for f in sr.findings:
            vuln: dict[str, Any] = {
                "id": _compute_fingerprint(f),
                "category": "sast",
                "name": f.title,
                "message": f.description,
                "description": f.description,
                "severity": severity_map.get(f.severity, "Unknown"),
                "confidence": confidence_map.get(f.confidence, "Medium"),
                "scanner": {
                    "id": "mcpaudit",
                    "name": "mcpaudit",
                },
                "location": {
                    "file": f.file,
                    "start_line": max(1, f.line),
                },
                "identifiers": [
                    {
                        "type": "mcpaudit_rule_id",
                        "name": f.rule_id,
                        "value": f.rule_id,
                    },
                ],
            }
            if f.cwe:
                vuln["identifiers"].append({
                    "type": "cwe",
                    "name": f.cwe,
                    "value": f.cwe.replace("CWE-", ""),
                    "url": f"https://cwe.mitre.org/data/definitions/{f.cwe.replace('CWE-', '')}.html",
                })
            if f.recommendation:
                vuln["solution"] = f.recommendation
            vulnerabilities.append(vuln)

    return {
        "version": "15.0.0",
        "vulnerabilities": vulnerabilities,
        "scan": {
            "scanner": {
                "id": "mcpaudit",
                "name": "mcpaudit",
                "version": __version__,
                "vendor": {"name": "Carlos Miret"},
            },
            "type": "sast",
            "status": "success",
            "start_time": "",
            "end_time": "",
        },
    }


def format_sarif_summary(sarif: dict[str, Any]) -> str:
    """Format a SARIF report as a human-readable summary.

    Args:
        sarif: SARIF JSON dict.

    Returns:
        Formatted text summary.
    """
    lines: list[str] = []
    lines.append("")
    lines.append("=" * 60)
    lines.append("  SARIF Report Summary")
    lines.append("=" * 60)

    for run in sarif.get("runs", []):
        tool = run.get("tool", {}).get("driver", {})
        lines.append(f"  Tool: {tool.get('name', 'unknown')} v{tool.get('version', '?')}")

        rules = tool.get("rules", [])
        lines.append(f"  Rules: {len(rules)}")

        results = run.get("results", [])
        lines.append(f"  Findings: {len(results)}")

        # Count by level
        level_counts: dict[str, int] = {}
        for result in results:
            level = result.get("level", "note")
            level_counts[level] = level_counts.get(level, 0) + 1

        if level_counts:
            lines.append("  By level:")
            for level, count in sorted(level_counts.items()):
                lines.append(f"    {level}: {count}")

        # Invocation info
        for inv in run.get("invocations", []):
            props = inv.get("properties", {})
            if props:
                lines.append(f"  Files scanned: {props.get('filesScanned', 'N/A')}")
                lines.append(f"  Grade: {props.get('overallGrade', 'N/A')}")

    lines.append("=" * 60)
    return "\n".join(lines)


def export_sarif_json(
    scan_results: list[ScanResult],
    tool_name: str = TOOL_NAME,
) -> str:
    """Export SARIF report as formatted JSON string.

    Args:
        scan_results: List of ScanResult from mcpaudit scanner.
        tool_name: Name of the tool (default: "mcpaudit").

    Returns:
        SARIF JSON string.
    """
    sarif = generate_sarif_upload(scan_results, tool_name=tool_name)
    return json.dumps(sarif, indent=2, ensure_ascii=False)


def export_github_annotations(scan_results: list[ScanResult]) -> str:
    """Export GitHub Actions annotations as JSON.

    Args:
        scan_results: List of ScanResult.

    Returns:
        JSON string with all annotations.
    """
    annotations: list[dict[str, Any]] = []
    for sr in scan_results:
        for f in sr.findings:
            annotations.append(generate_github_annotation(f))
    return json.dumps(annotations, indent=2, ensure_ascii=False)


def export_gitlab_sast_json(scan_results: list[ScanResult]) -> str:
    """Export GitLab SAST report as JSON.

    Args:
        scan_results: List of ScanResult.

    Returns:
        GitLab SAST JSON string.
    """
    report = generate_gitlab_sast(scan_results)
    return json.dumps(report, indent=2, ensure_ascii=False)
