#!/usr/bin/env python3
"""Forge Health Check — Build verification and artifact integrity scanner.

Scans wizard environments for:
- Missing source files (.pyc without .py) — Allegro finding: GOFAI source files gone
- Burn script accumulation in /root or wizard directories
- World-readable sensitive files (keystores, tokens, configs)
- Missing required environment variables

Usage:
    python scripts/forge_health_check.py /root/wizards
    python scripts/forge_health_check.py /root/wizards --json
    python scripts/forge_health_check.py /root/wizards --fix-permissions
"""

from __future__ import annotations

import argparse
import json
import os
import stat
import sys
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Iterable


SENSITIVE_FILE_PATTERNS = (
    "keystore",
    "password",
    "private",
    "apikey",
    "api_key",
    "credentials",
)

SENSITIVE_NAME_PREFIXES = (
    "key_",
    "keys_",
    "token_",
    "tokens_",
    "secret_",
    "secrets_",
    ".env",
    "env.",
)

SENSITIVE_NAME_SUFFIXES = (
    "_key",
    "_keys",
    "_token",
    "_tokens",
    "_secret",
    "_secrets",
    ".key",
    ".env",
    ".token",
    ".secret",
)

SENSIBLE_PERMISSIONS = 0o600  # owner read/write only

REQUIRED_ENV_VARS = (
    "GITEA_URL",
    "GITEA_TOKEN",
    "GITEA_USER",
)

BURN_SCRIPT_PATTERNS = (
    "burn",
    "ignite",
    "inferno",
    "scorch",
    "char",
    "blaze",
    "ember",
)


@dataclass
class HealthFinding:
    category: str
    severity: str  # critical, warning, info
    path: str
    message: str
    suggestion: str = ""


@dataclass
class HealthReport:
    target: str
    findings: list[HealthFinding] = field(default_factory=list)
    passed: bool = True

    def add(self, finding: HealthFinding) -> None:
        self.findings.append(finding)
        if finding.severity == "critical":
            self.passed = False


EXCLUDED_PATH_SEGMENTS = frozenset({
    ".cache", "__pycache__", ".venv", "venv", "site-packages",
    ".local/share/uv", "node_modules", ".git", ".tox",
})


def _is_excluded_path(path: Path) -> bool:
    """Skip cache, venv, and package-manager directories."""
    parts = set(path.parts)
    return not parts.isdisjoint(EXCLUDED_PATH_SEGMENTS)


def scan_orphaned_bytecode(root: Path, report: HealthReport) -> None:
    """Detect .pyc files without corresponding .py source files."""
    for pyc in root.rglob("*.pyc"):
        if _is_excluded_path(pyc):
            continue
        py = pyc.with_suffix(".py")
        if not py.exists():
            # Also check __pycache__ naming convention
            if pyc.name.startswith("__") and pyc.parent.name == "__pycache__":
                stem = pyc.stem.split(".")[0]
                py = pyc.parent.parent / f"{stem}.py"
            if not py.exists():
                report.add(
                    HealthFinding(
                        category="artifact_integrity",
                        severity="critical",
                        path=str(pyc),
                        message=f"Compiled bytecode without source: {pyc}",
                        suggestion="Restore missing .py source file from version control or backup",
                    )
                )


def scan_burn_script_clutter(root: Path, report: HealthReport) -> None:
    """Detect burn scripts and other temporary artifacts outside proper staging."""
    for path in root.iterdir():
        if not path.is_file():
            continue
        lower = path.name.lower()
        if any(pat in lower for pat in BURN_SCRIPT_PATTERNS):
            report.add(
                HealthFinding(
                    category="deployment_hygiene",
                    severity="warning",
                    path=str(path),
                    message=f"Burn script or temporary artifact in production path: {path.name}",
                    suggestion="Archive to a burn/ or tmp/ directory, or remove if no longer needed",
                )
            )


def _is_sensitive_filename(name: str) -> bool:
    """Check if a filename indicates it may contain secrets."""
    lower = name.lower()
    if lower == ".env.example":
        return False
    # Skip stylesheet and documentation artifacts
    if lower.endswith(".css"):
        return False
    # Skip scanner tooling — these are detectors, not secrets
    if lower in {"secret_scan.py", "secret_scanner.py"}:
        return False
    if any(pat in lower for pat in SENSITIVE_FILE_PATTERNS):
        return True
    if any(lower.startswith(pref) for pref in SENSITIVE_NAME_PREFIXES):
        return True
    if any(lower.endswith(suff) for suff in SENSITIVE_NAME_SUFFIXES):
        return True
    return False


def scan_sensitive_file_permissions(root: Path, report: HealthReport, fix: bool = False) -> None:
    """Detect world-readable sensitive files."""
    for fpath in root.rglob("*"):
        if not fpath.is_file():
            continue
        if _is_excluded_path(fpath):
            continue
        # Skip test files — real secrets should never live in tests/
        if "/tests/" in str(fpath) or str(fpath).startswith(str(root / "tests")):
            continue
        if not _is_sensitive_filename(fpath.name):
            continue

        try:
            mode = fpath.stat().st_mode
        except OSError:
            continue

        # Readable by group or other
        if mode & stat.S_IRGRP or mode & stat.S_IROTH:
            was_fixed = False
            if fix:
                try:
                    fpath.chmod(SENSIBLE_PERMISSIONS)
                    was_fixed = True
                except OSError:
                    pass

            report.add(
                HealthFinding(
                    category="security",
                    severity="critical",
                    path=str(fpath),
                    message=(
                        f"Sensitive file world-readable: {fpath.name} "
                        f"(mode={oct(mode & 0o777)})"
                    ),
                    suggestion=(
                        f"Fixed permissions to {oct(SENSIBLE_PERMISSIONS)}"
                        if was_fixed
                        else f"Run 'chmod {oct(SENSIBLE_PERMISSIONS)[2:]} {fpath}'"
                    ),
                )
            )


def scan_environment_variables(report: HealthReport) -> None:
    """Check for required environment variables."""
    for var in REQUIRED_ENV_VARS:
        if not os.environ.get(var):
            report.add(
                HealthFinding(
                    category="configuration",
                    severity="warning",
                    path="$" + var,
                    message=f"Required environment variable {var} is missing or empty",
                    suggestion="Export the variable in your shell profile or secrets manager",
                )
            )


def run_health_check(target: Path, fix_permissions: bool = False) -> HealthReport:
    report = HealthReport(target=str(target.resolve()))
    if target.exists():
        scan_orphaned_bytecode(target, report)
        scan_burn_script_clutter(target, report)
        scan_sensitive_file_permissions(target, report, fix=fix_permissions)
    scan_environment_variables(report)
    return report


def print_report(report: HealthReport) -> None:
    status = "PASS" if report.passed else "FAIL"
    print(f"Forge Health Check: {status}")
    print(f"Target: {report.target}")
    print(f"Findings: {len(report.findings)}\n")

    by_category: dict[str, list[HealthFinding]] = {}
    for f in report.findings:
        by_category.setdefault(f.category, []).append(f)

    for category, findings in by_category.items():
        print(f"[{category.upper()}]")
        for f in findings:
            print(f"  {f.severity.upper()}: {f.message}")
            if f.suggestion:
                print(f"    -> {f.suggestion}")
        print()


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Forge Health Check")
    parser.add_argument("target", nargs="?", default="/root/wizards", help="Root path to scan")
    parser.add_argument("--json", action="store_true", help="Output JSON report")
    parser.add_argument("--fix-permissions", action="store_true", help="Auto-fix file permissions")
    args = parser.parse_args(argv)

    target = Path(args.target)
    report = run_health_check(target, fix_permissions=args.fix_permissions)

    if args.json:
        print(json.dumps(asdict(report), indent=2))
    else:
        print_report(report)

    return 0 if report.passed else 1


if __name__ == "__main__":
    raise SystemExit(main())
