from __future__ import annotations

import hashlib
import os
import shutil
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Tuple

import subprocess
import yaml

from vibegate.artifacts import (
    ArtifactRecord,
    build_fixpack,
    sha256_path,
    write_fixpack,
    write_report,
    write_report_html,
)
from vibegate.checks import (
    CheckOutcome,
    detect_packaging_tool,
    run_bandit,
    run_config_sanity,
    run_dependency_hygiene,
    run_gitleaks,
    run_osv,
    run_pyright,
    run_pytest,
    run_ruff_format,
    run_ruff_lint,
    run_runtime_smoke,
)
from vibegate.config import VibeGateConfig
from vibegate.evidence import EvidenceWriter
from vibegate.findings import Finding


@dataclass(frozen=True)
class SuppressionRecord:
    fingerprint: str
    rule_id: str | None
    justification: str
    expires_at: str
    actor: str | None


def _fingerprint(finding: Finding) -> str:
    location = ""
    if finding.location and finding.location.path:
        location = f"{finding.location.path}:{finding.location.line or ''}:{finding.location.col or ''}"
    payload = f"{finding.check_id}|{finding.rule_id}|{location}|{finding.message}"
    digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
    return f"sha256:{digest}"


def _apply_fingerprints(findings: List[Finding]) -> List[Finding]:
    updated = []
    for finding in findings:
        updated.append(
            Finding(
                check_id=finding.check_id,
                finding_type=finding.finding_type,
                rule_id=finding.rule_id,
                severity=finding.severity,
                message=finding.message,
                fingerprint=_fingerprint(finding),
                tool=finding.tool,
                remediation_hint=finding.remediation_hint,
                location=finding.location,
            )
        )
    return updated


def _finding_sort_key(finding: Finding) -> tuple:
    path = finding.location.path if finding.location and finding.location.path else ""
    line = (
        finding.location.line
        if finding.location and finding.location.line is not None
        else 0
    )
    return (finding.check_id, finding.rule_id, path, line, finding.message)


def _load_suppressions(
    config: VibeGateConfig, repo_root: Path
) -> List[SuppressionRecord]:
    path = repo_root / config.suppressions.path
    if not path.exists():
        return []
    payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
    if payload.get("schema_version") != "v1alpha1":
        return []
    records = []
    now = datetime.now(timezone.utc)
    for item in payload.get("suppressions") or []:
        justification = item.get("justification")
        expires_at = item.get("expires_at")
        actor = item.get("actor")
        if config.suppressions.policy.require_justification and not justification:
            continue
        if config.suppressions.policy.require_expiry and not expires_at:
            continue
        if config.suppressions.policy.require_actor and not actor:
            continue
        try:
            expires_dt = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
        except (TypeError, ValueError):
            continue
        if expires_dt <= now:
            continue
        records.append(
            SuppressionRecord(
                fingerprint=item.get("fingerprint", ""),
                rule_id=item.get("rule_id"),
                justification=justification,
                expires_at=expires_at,
                actor=actor,
            )
        )
    return records


def _apply_suppressions(
    findings: List[Finding],
    suppressions: List[SuppressionRecord],
    evidence: EvidenceWriter,
) -> Tuple[List[Finding], List[Finding]]:
    suppressed = []
    unsuppressed = []
    suppression_map = {record.fingerprint: record for record in suppressions}
    for finding in findings:
        record = suppression_map.get(finding.fingerprint)
        if record and (record.rule_id is None or record.rule_id == finding.rule_id):
            suppressed.append(finding)
            evidence.suppression_applied(
                {
                    "rule_id": finding.rule_id,
                    "fingerprint": finding.fingerprint,
                    "justification": record.justification,
                    "expires_at": record.expires_at,
                    "actor": record.actor,
                }
            )
        else:
            unsuppressed.append(finding)
    return suppressed, unsuppressed


def _env_allowlist(config: VibeGateConfig) -> dict[str, str]:
    env = dict(os.environ)
    env.update(config.determinism.env)
    return env


def _record_tool_exec(outcome: CheckOutcome, evidence: EvidenceWriter) -> None:
    if not outcome.tool_result:
        return
    artifacts = [
        {"path": str(path), "sha256": sha256_path(path)}
        for path in outcome.tool_result.artifacts
        if path.exists()
    ]
    evidence.tool_exec(
        check_id=outcome.check_id,
        tool=outcome.tool_result.argv[0],
        tool_version=outcome.tool_result.tool_version,
        argv=outcome.tool_result.argv,
        cwd=str(evidence.repo_root),
        duration_ms=outcome.tool_result.duration_ms,
        exit_code=outcome.tool_result.exit_code,
        artifacts=artifacts,
    )


def _run_all_checks(
    config: VibeGateConfig, repo_root: Path, evidence: EvidenceWriter
) -> Tuple[List[Finding], List[dict[str, str]]]:
    env = _env_allowlist(config)
    findings: List[Finding] = []
    skipped_checks: List[dict[str, str]] = []
    checks = [
        ("formatting", config.checks.formatting, run_ruff_format),
        ("lint", config.checks.lint, run_ruff_lint),
        ("typecheck", config.checks.typecheck, run_pyright),
        ("tests", config.checks.tests, run_pytest),
        (
            "dependency_hygiene",
            config.checks.dependency_hygiene,
            run_dependency_hygiene,
        ),
        ("sast", config.checks.sast, run_bandit),
        ("secrets", config.checks.secrets, run_gitleaks),
        ("vulnerability", config.checks.vulnerability, run_osv),
        ("config_sanity", config.checks.config_sanity, run_config_sanity),
        ("runtime_smoke", config.checks.runtime_smoke, run_runtime_smoke),
    ]

    for _, check, runner in checks:
        if not check.enabled:
            skipped_checks.append({"check_id": check.id, "reason": "disabled"})
            continue
        if runner is run_dependency_hygiene:
            outcome = runner(check, repo_root, env, config)
        elif runner is run_config_sanity:
            outcome = runner(check, repo_root)
        else:
            outcome = runner(check, repo_root, env)
        if outcome.skipped_reason:
            skipped_checks.append(
                {
                    "check_id": outcome.check_id,
                    "reason": f"SKIPPED: {outcome.skipped_reason}",
                }
            )
        if outcome.tool_result:
            _record_tool_exec(outcome, evidence)
        findings.extend(outcome.findings)

    return findings, skipped_checks


def _selected_packaging(config: VibeGateConfig) -> dict[str, object]:
    tool = detect_packaging_tool(config)
    lockfiles = config.packaging.lockfiles
    if not lockfiles:
        from vibegate.checks import _default_lockfiles

        lockfiles = _default_lockfiles(tool)
    return {"tool": tool, "lockfiles": lockfiles}


def _toolchain_versions() -> List[dict[str, str]]:
    tools = ["ruff", "pyright", "pytest", "bandit", "gitleaks", "osv-scanner", "uv"]
    versions = []
    for tool in tools:
        if not shutil.which(tool):
            continue
        try:
            result = subprocess.run(
                [tool, "--version"],
                capture_output=True,
                text=True,
                timeout=2,
                check=False,
            )
        except (OSError, subprocess.SubprocessError):
            continue
        if result.returncode != 0:
            continue
        output = (result.stdout or result.stderr or "").strip()
        if not output:
            continue
        versions.append({"tool": tool, "version": output.splitlines()[0].strip()})
    return versions


def run_check(
    config: VibeGateConfig, repo_root: Path
) -> tuple[List[ArtifactRecord], str]:
    evidence = EvidenceWriter(config, repo_root)
    evidence.start_run(_selected_packaging(config), _toolchain_versions())

    started = time.monotonic()
    findings, skipped_checks = _run_all_checks(config, repo_root, evidence)
    findings = _apply_fingerprints(findings)
    findings = sorted(findings, key=_finding_sort_key)

    for finding in findings:
        evidence.finding(finding.event_payload())

    suppressions = _load_suppressions(config, repo_root)
    suppressed, unsuppressed = _apply_suppressions(findings, suppressions, evidence)

    status = "FAIL" if unsuppressed else "PASS"
    report_artifacts = [
        write_report(config.outputs, status, unsuppressed, skipped_checks)
    ]
    if config.outputs.emit_html:
        report_artifacts.append(write_report_html(config.outputs))

    duration_ms = int((time.monotonic() - started) * 1000)
    counts = {
        "findings_total": len(findings),
        "findings_unsuppressed": len(unsuppressed),
        "suppressed_total": len(suppressed),
    }
    evidence.run_summary(status, duration_ms, counts, skipped_checks)

    evidence_sha = (
        sha256_path(config.outputs.evidence_jsonl)
        if config.outputs.evidence_jsonl.exists()
        else ""
    )
    contract_sha = (
        sha256_path(config.contract_path)
        if config.contract_path and config.contract_path.exists()
        else ""
    )
    fixpack_payload = build_fixpack(
        evidence.run_id, unsuppressed, contract_sha, evidence_sha
    )
    fixpack_artifacts = write_fixpack(config.outputs, fixpack_payload, status)

    return report_artifacts + fixpack_artifacts, status


def run_fixpack(
    config: VibeGateConfig, repo_root: Path
) -> tuple[List[ArtifactRecord], str]:
    evidence = EvidenceWriter(config, repo_root)
    evidence.start_run(_selected_packaging(config), _toolchain_versions())

    started = time.monotonic()
    findings, skipped_checks = _run_all_checks(config, repo_root, evidence)
    findings = _apply_fingerprints(findings)
    findings = sorted(findings, key=_finding_sort_key)

    for finding in findings:
        evidence.finding(finding.event_payload())

    suppressions = _load_suppressions(config, repo_root)
    suppressed, unsuppressed = _apply_suppressions(findings, suppressions, evidence)

    status = "FAIL" if unsuppressed else "PASS"
    duration_ms = int((time.monotonic() - started) * 1000)
    counts = {
        "findings_total": len(findings),
        "findings_unsuppressed": len(unsuppressed),
        "suppressed_total": len(suppressed),
    }
    evidence.run_summary(status, duration_ms, counts, skipped_checks)

    evidence_sha = (
        sha256_path(config.outputs.evidence_jsonl)
        if config.outputs.evidence_jsonl.exists()
        else ""
    )
    contract_sha = (
        sha256_path(config.contract_path)
        if config.contract_path and config.contract_path.exists()
        else ""
    )
    fixpack_payload = build_fixpack(
        evidence.run_id, unsuppressed, contract_sha, evidence_sha
    )
    fixpack_artifacts = write_fixpack(config.outputs, fixpack_payload, status)

    return fixpack_artifacts, status
