#!/usr/bin/env python3
"""Doc-impact enforcement — checks if staged code changes require doc updates.

Scans staged git changes against an impact mapping. If code in certain
directories was changed but the corresponding documentation was NOT also
staged, the script reports what's missing.

Uses generic directory patterns that work across any tech stack.

Usage:
    enforce_doc_impact.py [--staged] [--strict]

Exit codes:
    0 = no doc updates required, or all required docs are staged
    1 = required doc updates are missing (--strict mode only)
"""
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]

# Generic impact mapping — works across stacks.
# code_patterns: substrings matched against file paths
# required_docs: always required when code matches
# required_docs_on_new: only required when NEW files are added (not just modified)
IMPACT_MAP = [
    {
        "name": "Infrastructure / Docker",
        "code_patterns": [
            "docker-compose", "docker/", "Dockerfile",
            ".env", "Makefile", "nginx", "Procfile",
            "Vagrantfile", "terraform/", "k8s/", "helm/",
        ],
        "required_docs": [
            "docs/development/DEPLOYMENT_GUIDE.md",
            "docs/context/deployment.md",
        ],
    },
    {
        "name": "Backend / Server",
        "code_patterns": [
            "app/models/", "app/services/", "app/api/",
            "src/models/", "src/services/", "src/api/",
            "src/server/", "src/routes/", "src/controllers/",
            "backend/", "server/", "api/",
        ],
        "required_docs": [],
        "required_docs_on_new": [
            "docs/development/ARCHITECTURE.md",
            "docs/context/backend.md",
        ],
    },
    {
        "name": "Frontend / Client",
        "code_patterns": [
            "src/hooks/", "src/components/", "src/app/",
            "src/pages/", "src/views/", "src/stores/",
            "frontend/", "client/", "web/",
        ],
        "required_docs": [],
        "required_docs_on_new": [
            "docs/context/frontend.md",
        ],
    },
    {
        "name": "Database / Migrations",
        "code_patterns": [
            "alembic/", "migrations/", "schema.sql",
            "prisma/", "drizzle/", "knex/", "flyway/",
            "liquibase/", "db/migrate/",
        ],
        "required_docs": [
            "docs/context/database.md",
        ],
    },
    {
        "name": "Auth / Permissions",
        "code_patterns": [
            "auth", "permission", "rbac",
            "middleware/auth", "guards/", "policies/",
        ],
        "required_docs": [
            "docs/context/security.md",
        ],
    },
    {
        "name": "Test Infrastructure",
        "code_patterns": [
            "conftest.py", "vitest.config", "jest.config",
            "pytest.ini", "setup.cfg", ".mocharc",
            "karma.conf", "cypress.config", "playwright.config",
        ],
        "required_docs": [
            "docs/context/testing.md",
        ],
    },
    {
        "name": "Shared Patterns / Utilities",
        "code_patterns": [
            "src/lib/", "src/utils/", "src/helpers/",
            "app/core/", "app/utils/", "lib/",
            "shared/", "common/", "pkg/",
        ],
        "required_docs": [],
        "required_docs_on_new": [
            "docs/KEY_PATTERNS.md",
        ],
    },
]


def get_staged_files():
    result = subprocess.run(
        ["git", "diff", "--cached", "--name-status"],
        capture_output=True, text=True, cwd=ROOT,
    )
    modified, new = [], []
    for line in result.stdout.strip().splitlines():
        if not line:
            continue
        parts = line.split("\t", 1)
        if len(parts) < 2:
            continue
        status, filepath = parts[0], parts[1]
        if status.startswith("A"):
            new.append(filepath)
        else:
            modified.append(filepath)
    return modified, new


def get_changed_files():
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        capture_output=True, text=True, cwd=ROOT,
    )
    modified, new = [], []
    for line in result.stdout.strip().splitlines():
        if not line or len(line) < 4:
            continue
        status = line[:2].strip()
        filepath = line[3:]
        if "?" in status or "A" in status:
            new.append(filepath)
        else:
            modified.append(filepath)
    return modified, new


def check(staged=False, strict=False):
    if staged:
        modified, new = get_staged_files()
    else:
        modified, new = get_changed_files()

    all_files = modified + new
    if not all_files:
        return 0

    all_files_set = set(all_files)
    new_set = set(new)
    missing = []

    for rule in IMPACT_MAP:
        matched = False
        has_new = False
        for filepath in all_files:
            for pattern in rule["code_patterns"]:
                if pattern in filepath:
                    matched = True
                    if filepath in new_set:
                        has_new = True
                    break
            if matched and has_new:
                break

        if not matched:
            continue

        for doc in rule.get("required_docs", []):
            if doc not in all_files_set:
                missing.append((rule["name"], doc))

        if has_new:
            for doc in rule.get("required_docs_on_new", []):
                if doc not in all_files_set:
                    missing.append((rule["name"], doc))

    if missing:
        print("Documentation Impact Check")
        print("=" * 50)
        print()
        print("The following documentation updates may be required:")
        print()
        for area, doc in missing:
            print(f"  [{area}] → {doc}")
        print()
        if strict:
            return 1
    return 0


if __name__ == "__main__":
    sys.exit(check(
        staged="--staged" in sys.argv,
        strict="--strict" in sys.argv,
    ))
