"""Output guardrails: secret detection, scope enforcement, file permissions, dangerous operations.

Runs automated pre-merge checks on git diffs produced by completed agents.
Hard-blocks on secrets and file permission violations; flags scope violations
and dangerous deletions.
"""

from __future__ import annotations

import json
import re
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from bernstein.core.license_scanner import check_license_obligations
from bernstein.core.models import GuardrailResult, Task
from bernstein.core.permissions import AgentPermissions, check_file_permissions


@dataclass(frozen=True)
class GuardrailsConfig:
    """Guardrail configuration options.

    Attributes:
        secrets: Whether to run secret detection.
        scope: Whether to run scope enforcement.
        license_scan: Whether to scan for copyleft license obligations.
        max_deletion_pct: Flag if this fraction of a file's diff lines are removals.
    """

    secrets: bool = True
    scope: bool = True
    file_permissions: bool = True
    license_scan: bool = True
    max_deletion_pct: int = 50
    permission_overrides: dict[str, AgentPermissions] | None = None


# ---------------------------------------------------------------------------
# Secret patterns
# ---------------------------------------------------------------------------

_SECRET_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
    ("aws_access_key", re.compile(r"AKIA[0-9A-Z]{16}")),
    ("github_token", re.compile(r"ghp_[a-zA-Z0-9]{36}")),
    ("github_pat", re.compile(r"github_pat_[a-zA-Z0-9_]{82}")),
    (
        "private_key",
        re.compile(r"-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"),
    ),
    (
        "jwt_token",
        re.compile(r"eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}"),
    ),
    (
        "generic_secret",
        re.compile(r"(?i)(?:password|passwd|secret|token|api_key)\s*=\s*['\"][^'\"]{8,}['\"]"),
    ),
]

# ---------------------------------------------------------------------------
# Critical files and directories
# ---------------------------------------------------------------------------

_CRITICAL_FILENAMES: frozenset[str] = frozenset(
    {
        "README.md",
        "README.rst",
        "README",
        "LICENSE",
        "LICENSE.md",
        "LICENSE.txt",
        "pyproject.toml",
        "setup.py",
        "setup.cfg",
        "Makefile",
        "Dockerfile",
        ".dockerignore",
    }
)


# ---------------------------------------------------------------------------
# Diff parsing helpers
# ---------------------------------------------------------------------------


def _parse_diff_files(diff: str) -> list[str]:
    """Extract modified file paths from a git diff (a/ prefix stripped)."""
    files: list[str] = []
    for line in diff.splitlines():
        if line.startswith("diff --git "):
            # "diff --git a/path/to/file b/path/to/file"
            parts = line.split(" ", 3)
            if len(parts) >= 3:
                path = parts[2]
                if path.startswith("a/"):
                    path = path[2:]
                files.append(path)
    return files


def _is_file_deleted(diff: str, filepath: str) -> bool:
    """Return True if *filepath* was deleted in the diff."""
    header_prefix = f"diff --git a/{filepath}"
    in_file = False
    for line in diff.splitlines():
        if line.startswith("diff --git "):
            in_file = line.startswith(header_prefix)
        elif in_file and line.startswith("deleted file mode"):
            return True
        elif in_file and line.startswith("@@"):
            # Past the extended header — no deletion marker
            break
    return False


def _parse_deletion_pct_per_file(diff: str) -> dict[str, int]:
    """Estimate deletion percentage per file from diff change lines.

    For each file, counts lines starting with '-' vs '+' (excluding headers)
    and returns the fraction that are removals as an integer 0-100.
    """
    pct: dict[str, int] = {}
    current_file: str | None = None
    added = removed = 0

    for line in diff.splitlines():
        if line.startswith("diff --git "):
            # Flush previous file stats
            if current_file is not None and (added + removed) > 0:
                pct[current_file] = int(removed / (added + removed) * 100)
            parts = line.split(" ", 3)
            if len(parts) >= 3:
                path = parts[2]
                current_file = path[2:] if path.startswith("a/") else path
            else:
                current_file = None
            added = removed = 0
        elif line.startswith("+") and not line.startswith("+++"):
            added += 1
        elif line.startswith("-") and not line.startswith("---"):
            removed += 1

    if current_file is not None and (added + removed) > 0:
        pct[current_file] = int(removed / (added + removed) * 100)

    return pct


# ---------------------------------------------------------------------------
# Individual checks
# ---------------------------------------------------------------------------


def check_secrets(diff: str) -> list[GuardrailResult]:
    """Scan a diff for known secret patterns.

    Args:
        diff: Git diff output string.

    Returns:
        List with one GuardrailResult for the "secret_detection" check.
    """
    found: list[str] = []
    for name, pattern in _SECRET_PATTERNS:
        if pattern.search(diff):
            found.append(name)

    if found:
        return [
            GuardrailResult(
                check="secret_detection",
                passed=False,
                blocked=True,
                detail=f"Potential secrets detected: {', '.join(found)}",
            )
        ]
    return [
        GuardrailResult(
            check="secret_detection",
            passed=True,
            blocked=False,
            detail="No secrets detected",
        )
    ]


def check_scope(diff: str, task: Task) -> list[GuardrailResult]:
    """Check that all modified files are within the task's owned_files scope.

    If the task has no owned_files, scope enforcement is skipped (passes).

    Args:
        diff: Git diff output string.
        task: Task with owned_files defining the allowed scope.

    Returns:
        List with one GuardrailResult for the "scope_enforcement" check.
    """
    if not task.owned_files:
        return [
            GuardrailResult(
                check="scope_enforcement",
                passed=True,
                blocked=False,
                detail="No scope defined — skipping",
            )
        ]

    changed_files = _parse_diff_files(diff)
    out_of_scope = [
        f
        for f in changed_files
        if not any(f == owned or f.startswith(owned.rstrip("/") + "/") for owned in task.owned_files)
    ]

    if out_of_scope:
        return [
            GuardrailResult(
                check="scope_enforcement",
                passed=False,
                blocked=False,  # Soft block — flagged for review, not hard-blocked
                detail=f"{len(out_of_scope)} file(s) modified outside task scope",
                files=out_of_scope,
            )
        ]
    return [
        GuardrailResult(
            check="scope_enforcement",
            passed=True,
            blocked=False,
            detail="All modified files within scope",
        )
    ]


def check_dangerous_operations(
    diff: str,
    config: GuardrailsConfig,
) -> list[GuardrailResult]:
    """Flag dangerous operations in a git diff.

    Checks:
      - Deletion of critical project files (README, LICENSE, pyproject.toml, etc.)
      - Deletion of test files
      - Large-scale file deletions (>max_deletion_pct% of changed lines are removals)

    Args:
        diff: Git diff output string.
        config: Guardrails configuration (max_deletion_pct threshold).

    Returns:
        List with one GuardrailResult for the "dangerous_operations" check.
    """
    issues: list[str] = []
    changed_files = _parse_diff_files(diff)

    for filepath in changed_files:
        if not _is_file_deleted(diff, filepath):
            continue
        filename = Path(filepath).name
        if filename in _CRITICAL_FILENAMES:
            issues.append(f"Critical file deleted: {filepath}")
        elif "test" in filepath.lower() or filepath.startswith("tests/"):
            issues.append(f"Test file deleted: {filepath}")

    # Large-scale deletion check
    pct_by_file = _parse_deletion_pct_per_file(diff)
    for filepath, pct in pct_by_file.items():
        if pct > config.max_deletion_pct:
            issues.append(f"Large deletion in {filepath}: {pct}% of diff lines are removals")

    if issues:
        violated_files = [f for f in changed_files if any(f in issue for issue in issues)]
        return [
            GuardrailResult(
                check="dangerous_operations",
                passed=False,
                blocked=False,
                detail="; ".join(issues),
                files=violated_files,
            )
        ]
    return [
        GuardrailResult(
            check="dangerous_operations",
            passed=True,
            blocked=False,
            detail="No dangerous operations detected",
        )
    ]


# ---------------------------------------------------------------------------
# Metrics recording
# ---------------------------------------------------------------------------


def record_guardrail_event(
    task_id: str,
    check: str,
    result: str,
    workdir: Path,
    *,
    files: list[str] | None = None,
) -> None:
    """Append a guardrail event to .sdd/metrics/guardrails.jsonl.

    Args:
        task_id: ID of the task being checked.
        check: Check name (e.g. "secret_detection").
        result: Outcome string: "pass", "blocked", or "flagged".
        workdir: Project root directory.
        files: Files involved in any violation (omitted from JSON if empty).
    """
    metrics_dir = workdir / ".sdd" / "metrics"
    metrics_dir.mkdir(parents=True, exist_ok=True)
    event: dict[str, Any] = {
        "timestamp": datetime.now(UTC).isoformat(),
        "task_id": task_id,
        "check": check,
        "result": result,
    }
    if files:
        event["files"] = files
    with open(metrics_dir / "guardrails.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps(event) + "\n")


def get_guardrail_stats(workdir: Path) -> dict[str, Any]:
    """Read .sdd/metrics/guardrails.jsonl and return aggregate stats.

    Returns a dict with:
      - total: total events recorded
      - blocked: events with result "blocked"
      - flagged: events with result "flagged"
      - by_check: per-check breakdown {check: {pass: N, blocked: N, flagged: N}}

    Args:
        workdir: Project root directory.
    """
    metrics_file = workdir / ".sdd" / "metrics" / "guardrails.jsonl"
    if not metrics_file.exists():
        return {"total": 0, "blocked": 0, "flagged": 0, "by_check": {}}

    total = blocked = flagged = 0
    by_check: dict[str, dict[str, int]] = {}

    for raw_line in metrics_file.read_text(encoding="utf-8").splitlines():
        raw_line = raw_line.strip()
        if not raw_line:
            continue
        try:
            event = json.loads(raw_line)
        except json.JSONDecodeError:
            continue

        check = str(event.get("check", "unknown"))
        result_val = str(event.get("result", "pass"))
        total += 1
        if result_val == "blocked":
            blocked += 1
        elif result_val == "flagged":
            flagged += 1

        counts = by_check.setdefault(check, {"pass": 0, "blocked": 0, "flagged": 0})
        if result_val in counts:
            counts[result_val] += 1
        else:
            counts[result_val] = 1

    return {"total": total, "blocked": blocked, "flagged": flagged, "by_check": by_check}


# ---------------------------------------------------------------------------
# Main entrypoint
# ---------------------------------------------------------------------------


def run_guardrails(
    diff: str,
    task: Task,
    config: GuardrailsConfig,
    workdir: Path,
) -> list[GuardrailResult]:
    """Run all enabled guardrail checks on an agent's diff and record events.

    Args:
        diff: Git diff output string.
        task: The task that produced the diff.
        config: Which checks to run and their thresholds.
        workdir: Project root for writing metrics.

    Returns:
        List of GuardrailResult, one per enabled check.
    """
    results: list[GuardrailResult] = []

    if config.secrets:
        for r in check_secrets(diff):
            results.append(r)
            _record_result(task.id, r, workdir)

    if config.scope:
        for r in check_scope(diff, task):
            results.append(r)
            _record_result(task.id, r, workdir)

    if config.file_permissions:
        for r in check_file_permissions(diff, task.role, config.permission_overrides):
            results.append(r)
            _record_result(task.id, r, workdir)

    for r in check_dangerous_operations(diff, config):
        results.append(r)
        _record_result(task.id, r, workdir)

    if config.license_scan:
        for r in check_license_obligations(diff):
            results.append(r)
            _record_result(task.id, r, workdir)

    return results


def _record_result(task_id: str, result: GuardrailResult, workdir: Path) -> None:
    """Translate a GuardrailResult to a metrics event string and record it."""
    if result.passed:
        result_str = "pass"
    elif result.blocked:
        result_str = "blocked"
    else:
        result_str = "flagged"
    record_guardrail_event(
        task_id,
        result.check,
        result_str,
        workdir,
        files=result.files if result.files else None,
    )
