from __future__ import annotations

import re
import subprocess
from dataclasses import dataclass
from pathlib import Path

FORBIDDEN_SUFFIXES = {".db", ".sqlite", ".sqlite3", ".dump", ".parquet", ".xlsx", ".xls", ".pdf", ".docx", ".key", ".pem", ".p12", ".pfx"}
FORBIDDEN_NAMES = {".env", ".env.local", "credentials.json", "token.json"}
FORBIDDEN_NAME_PATTERNS = [re.compile(r"github[_-]?token", re.I), re.compile(r".*secret.*", re.I), re.compile(r".*credential.*", re.I)]
FORBIDDEN_TOP_LEVEL_DIRS = {"data", "imports", "exports", "reports", "backups", "secrets", "logs", "local_runtime", "runtime", "db", "databases"}
FORBIDDEN_RUNTIME_PARTS = {".pytest_cache", ".mypy_cache", ".ruff_cache", "node_modules", ".vite", "dist"}
SECRET_PATTERNS = [
    ("github_pat", re.compile(r"github_pat_[A-Za-z0-9_]{20,}")),
    ("github_ghp", re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}")),
    ("private_key", re.compile(r"-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----")),
    ("aws_access_key", re.compile(r"AKIA[0-9A-Z]{16}")),
    ("api_key_assignment", re.compile(r"(?i)(api[_-]?key|token|secret)\s*=\s*['\"]?[A-Za-z0-9_\-]{20,}")),
    ("gog_password", re.compile(r"GOG_KEYRING_PASSWORD\s*=")),
    ("bearer_token", re.compile(r"(?i)authorization\s*[:=]\s*['\"]?bearer\s+[A-Za-z0-9._~+/=-]{20,}")),
    ("openai_key", re.compile(r"sk-(?:proj-)?[A-Za-z0-9_-]{20,}")),
    ("slack_token", re.compile(r"xox[baprs]-[A-Za-z0-9-]{20,}")),
]
ALLOW_PREFIXES = ("examples/synthetic/", "tests/fixtures/", "config.example/")
ALLOW_SECRET_MARKER_PATHS = {
    "src/jarvis_finance/quality/git_safety.py",
    "tests/unit/test_git_safety.py",
    "tests/integration/test_dummy_data_load.py",
}

@dataclass(frozen=True)
class SafetyFinding:
    path: str
    reason: str

def _rel(path: Path, root: Path) -> str:
    return path.relative_to(root).as_posix()

def is_allowed_exception(rel: str) -> bool:
    return rel.startswith(ALLOW_PREFIXES) or rel in {"frontend/package.json", "frontend/package-lock.json", "frontend/tsconfig.json"}


def _candidate_files(root: Path) -> list[Path]:
    """Return commit candidates in Git repos, or all files for standalone fixtures."""
    try:
        probe = subprocess.run(
            ["git", "-C", str(root), "rev-parse", "--show-toplevel"],
            check=True,
            capture_output=True,
            text=True,
        )
        if Path(probe.stdout.strip()).resolve() == root:
            tracked = subprocess.run(
                ["git", "-C", str(root), "ls-files", "--cached", "--others", "--exclude-standard", "-z"],
                check=True,
                capture_output=True,
            )
            return [root / rel.decode("utf-8") for rel in tracked.stdout.split(b"\0") if rel]
    except (OSError, subprocess.CalledProcessError, UnicodeDecodeError):
        pass
    return [path for path in root.rglob("*") if path.is_symlink() or path.is_file()]


def scan_path(root: str | Path) -> list[SafetyFinding]:
    root = Path(root).resolve()
    findings: list[SafetyFinding] = []
    for path in _candidate_files(root):
        if ".git" in path.parts or "__pycache__" in path.parts:
            continue
        rel = _rel(path, root)
        rel_parts = Path(rel).parts
        if path.is_symlink():
            findings.append(SafetyFinding(rel, "symlink_blocked"))
            continue
        if path.is_dir():
            continue
        if any(part in FORBIDDEN_RUNTIME_PARTS for part in rel_parts):
            findings.append(SafetyFinding(rel, "runtime_cache_file_blocked"))
            continue
        if rel_parts and rel_parts[0] in FORBIDDEN_TOP_LEVEL_DIRS and not is_allowed_exception(rel):
            findings.append(SafetyFinding(rel, "runtime_or_real_data_directory_blocked"))
        lower_name = path.name.lower()
        if lower_name in FORBIDDEN_NAMES or (lower_name.startswith(".env.") and lower_name != ".env.example"):
            findings.append(SafetyFinding(rel, "forbidden_secret_filename"))
        if any(p.search(path.name) for p in FORBIDDEN_NAME_PATTERNS):
            findings.append(SafetyFinding(rel, "forbidden_secret_like_filename"))
        if path.suffix.lower() in {".csv", ".json"} and not is_allowed_exception(rel):
            findings.append(SafetyFinding(rel, "csv_json_only_allowed_in_examples_synthetic_or_tests_fixtures"))
        if path.suffix.lower() in FORBIDDEN_SUFFIXES and not is_allowed_exception(rel):
            findings.append(SafetyFinding(rel, f"forbidden_file_type:{path.suffix.lower()}"))
        if rel in ALLOW_SECRET_MARKER_PATHS:
            continue
        try:
            text = path.read_text(encoding="utf-8", errors="ignore")
        except OSError:
            continue
        for label, pattern in SECRET_PATTERNS:
            if pattern.search(text):
                findings.append(SafetyFinding(rel, f"secret_pattern:{label}"))
    return findings

def assert_safe(root: str | Path) -> None:
    findings = scan_path(root)
    if findings:
        details = "\n".join(f"{f.path}: {f.reason}" for f in findings)
        raise ValueError(f"Git safety scan failed:\n{details}")
