#!/usr/bin/env python3
"""Generate docs/audit/2026-06-04-bug-scan-registry.json — hand-written source SSOT."""

from __future__ import annotations

import json
import re
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "docs/audit/2026-06-04-bug-scan-registry.json"

# Batch 1–6 coverage (path relative to ROOT)
BATCH_1_6 = {
    "server/api/install.py": {"batch_id": 1, "status": "done", "needs_full_rescan": False},
    "web/app/install.html": {"batch_id": 1, "status": "done", "needs_full_rescan": False},
    "server/application/services/script_job_callback.py": {
        "batch_id": 1,
        "status": "done",
        "needs_full_rescan": False,
    },
    "server/api/agent.py": {"batch_id": 1, "status": "partial", "needs_full_rescan": True},
    "server/api/servers.py": {"batch_id": 1, "status": "partial", "needs_full_rescan": True},
    "server/background/schedule_runner.py": {"batch_id": 2, "status": "done", "needs_full_rescan": False},
    "server/application/services/auth_service.py": {
        "batch_id": 2,
        "status": "partial",
        "needs_full_rescan": True,
    },
    "server/application/services/sync_engine_v2.py": {
        "batch_id": 2,
        "status": "partial",
        "needs_full_rescan": True,
    },
    "server/infrastructure/redis/script_execution_store.py": {
        "batch_id": 2,
        "status": "partial",
        "needs_full_rescan": True,
    },
    "frontend/src/composables/terminal/useTerminalSessions.ts": {
        "batch_id": 2,
        "status": "partial",
        "needs_full_rescan": True,
    },
    "server/background/retry_runner.py": {"batch_id": 3, "status": "done", "needs_full_rescan": False},
    "server/application/services/files_browse_service.py": {
        "batch_id": 3,
        "status": "partial",
        "needs_full_rescan": True,
    },
    "frontend/src/pages/DashboardPage.vue": {
        "batch_id": 3,
        "status": "partial",
        "needs_full_rescan": True,
    },
    "server/api/sync_v2.py": {"batch_id": 4, "status": "partial", "needs_full_rescan": True},
    "server/api/files.py": {"batch_id": 4, "status": "done", "needs_full_rescan": False},
    "server/background/heartbeat_flush.py": {
        "batch_id": 4,
        "status": "partial",
        "needs_full_rescan": True,
    },
    "frontend/src/composables/push/usePushProgress.ts": {
        "batch_id": 4,
        "status": "done",
        "needs_full_rescan": False,
    },
    "server/api/schemas.py": {"batch_id": 4, "status": "partial", "needs_full_rescan": True},
    "server/background/self_monitor.py": {"batch_id": 5, "status": "done", "needs_full_rescan": False},
    "server/api/auth_jwt.py": {"batch_id": 6, "status": "done", "needs_full_rescan": False},
    "server/background/script_execution_flush.py": {
        "batch_id": 6,
        "status": "done",
        "needs_full_rescan": False,
    },
    "frontend/src/composables/files/useFilesBrowse.ts": {
        "batch_id": 6,
        "status": "done",
        "needs_full_rescan": False,
    },
    "frontend/src/stores/files.ts": {"batch_id": 6, "status": "done", "needs_full_rescan": False},
}

EXCLUDED_JUNK = {"scripts/sedul3hFb"}
# Local secrets — not in repo SSOT; example only in docker/.env.example
EXCLUDED_LOCAL = {"docker/.env", ".env"}

LARGE_FILE_THRESHOLD = 800

# Planned batch assignments (relative path -> batch_id)
PLANNED_BATCHES: dict[str, int] = {}

def _add_batch(batch_id: int, paths: list[str]) -> None:
    for p in paths:
        PLANNED_BATCHES[p] = batch_id


# Phase A B7-B16
_add_batch(7, ["server/api/servers.py"])
_add_batch(8, ["server/api/sync_v2.py"])
_add_batch(
    9,
    [
        "server/api/webssh.py",
        "server/api/auth.py",
        "server/api/websocket.py",
        "server/api/terminal.py",
        "server/api/settings.py",
    ],
)
_add_batch(
    10,
    [
        "server/api/scripts.py",
        "server/api/search.py",
        "server/api/assets.py",
        "server/api/files.py",
        "server/api/health.py",
    ],
)
_add_batch(
    11,
    [
        "server/api/dependencies.py",
        "server/api/remote_path_validation.py",
        "server/api/schema_path_validators.py",
        "server/api/schemas.py",
        "server/api/__init__.py",
    ],
)
_add_batch(12, ["server/api/install.py"])
_add_batch(13, ["server/api/agent.py"])
_add_batch(14, ["server/api/auth_jwt.py"])

# Phase B B17-B24
_add_batch(17, ["server/application/services/script_service.py"])
_add_batch(18, ["server/application/services/sync_engine_v2.py"])
_add_batch(19, ["server/application/services/auth_service.py"])
_add_batch(
    20,
    [
        "server/application/services/script_job_callback.py",
        "server/application/services/files_browse_service.py",
        "server/application/services/server_service.py",
        "server/application/services/sync_service.py",
        "server/application/services/script_jobs.py",
    ],
)
_add_batch(
    21,
    [
        "server/application/__init__.py",
        "server/application/services/__init__.py",
    ],
)
_add_batch(
    22,
    [
        "server/background/schedule_runner.py",
        "server/background/retry_runner.py",
        "server/background/heartbeat_flush.py",
        "server/background/self_monitor.py",
        "server/background/script_execution_flush.py",
    ],
)
_add_batch(
    23,
    [
        "server/background/ip_allowlist_refresh.py",
        "server/background/upload_staging_cleanup.py",
        "server/background/__init__.py",
    ],
)
_add_batch(24, ["server/main.py", "server/config.py", "server/__init__.py"])

# Phase C B25-B34 — filled dynamically below after collecting infra paths

INFRA_DB = sorted(
    [
        "server/infrastructure/database/admin_repo.py",
        "server/infrastructure/database/audit_log_repo.py",
        "server/infrastructure/database/crypto.py",
        "server/infrastructure/database/db_credential_repo.py",
        "server/infrastructure/database/engine_compat.py",
        "server/infrastructure/database/migrations.py",
        "server/infrastructure/database/password_preset_repo.py",
        "server/infrastructure/database/platform_node_repo.py",
        "server/infrastructure/database/push_schedule_repo.py",
        "server/infrastructure/database/script_repo.py",
        "server/infrastructure/database/server_repo.py",
        "server/infrastructure/database/session.py",
        "server/infrastructure/database/setting_repo.py",
        "server/infrastructure/database/ssh_key_preset_repo.py",
        "server/infrastructure/database/ssh_session_repo.py",
        "server/infrastructure/database/sync_log_repo.py",
        "server/infrastructure/database/__init__.py",
    ]
)
INFRA_SSH = sorted(
    [
        "server/infrastructure/ssh/asyncssh_pool.py",
        "server/infrastructure/ssh/files_capability.py",
        "server/infrastructure/ssh/remote_archive.py",
        "server/infrastructure/ssh/remote_probe.py",
        "server/infrastructure/ssh/remote_shell.py",
        "server/infrastructure/ssh/__init__.py",
    ]
)
INFRA_OTHER = sorted(
    [
        "server/infrastructure/redis/client.py",
        "server/infrastructure/redis/script_callback_rate.py",
        "server/infrastructure/redis/script_execution_store.py",
        "server/infrastructure/redis/__init__.py",
        "server/infrastructure/telegram/__init__.py",
        "server/infrastructure/agent_url.py",
        "server/infrastructure/subscription_parser.py",
        "server/infrastructure/__init__.py",
    ]
)
DOMAIN_UTILS = sorted(
    [
        "server/domain/__init__.py",
        "server/domain/models/__init__.py",
        "server/domain/repositories/__init__.py",
        "server/utils/files_chmod_policy.py",
        "server/utils/files_elevation.py",
        "server/utils/posix_paths.py",
        "server/utils/sync_error_message.py",
        "server/utils/text_io.py",
        "server/utils/unix_ls.py",
    ]
)


def _chunk(paths: list[str], size: int) -> list[list[str]]:
    return [paths[i : i + size] for i in range(0, len(paths), size)]


bid = 25
for chunk in _chunk(INFRA_DB, 5):
    _add_batch(bid, chunk)
    bid += 1
for chunk in _chunk(INFRA_SSH, 5):
    _add_batch(bid, chunk)
    bid += 1
_add_batch(bid, INFRA_OTHER)
bid += 1
for chunk in _chunk(DOMAIN_UTILS, 5):
    _add_batch(bid, chunk)
    bid += 1

# Phase D frontend — pages B35+
FRONTEND_PAGES = sorted((ROOT / "frontend/src/pages").glob("*.vue"))
FRONTEND_PAGES = [str(p.relative_to(ROOT)).replace("\\", "/") for p in FRONTEND_PAGES]
bid = 35
for chunk in _chunk(FRONTEND_PAGES, 5):
    _add_batch(bid, chunk)
    bid += 1

# composables — useTerminalSessions alone first (large)
_add_batch(bid, ["frontend/src/composables/terminal/useTerminalSessions.ts"])
bid += 1
COMPOSABLES = sorted(
    (ROOT / "frontend/src/composables").rglob("*"),
    key=lambda p: str(p),
)
COMPOSABLES = [
    str(p.relative_to(ROOT)).replace("\\", "/")
    for p in COMPOSABLES
    if p.is_file() and p.suffix in {".ts", ".vue"}
    and "useTerminalSessions.ts" not in p.name
]
for chunk in _chunk(COMPOSABLES, 5):
    _add_batch(bid, chunk)
    bid += 1

COMPONENTS = sorted(
    (ROOT / "frontend/src/components").rglob("*.vue"),
    key=lambda p: str(p),
)
COMPONENTS = [str(p.relative_to(ROOT)).replace("\\", "/") for p in COMPONENTS]
for chunk in _chunk(COMPONENTS, 5):
    _add_batch(bid, chunk)
    bid += 1

_add_batch(
    bid,
    [
        "frontend/src/api/index.ts",
        "frontend/src/stores/auth.ts",
        "frontend/src/stores/files.ts",
        "frontend/src/router/index.ts",
        "frontend/src/App.vue",
    ],
)
bid += 1

FRONTEND_REST = sorted(
    p
    for p in (ROOT / "frontend/src").rglob("*")
    if p.is_file()
    and p.suffix in {".ts", ".scss"}
    and "assets" not in p.parts
    and p not in {ROOT / f for f in [
        "frontend/src/api/index.ts",
        "frontend/src/stores/auth.ts",
        "frontend/src/stores/files.ts",
        "frontend/src/router/index.ts",
        "frontend/src/App.vue",
        "frontend/src/main.ts",
    ]}
)
FRONTEND_REST_PATHS = []
for p in FRONTEND_REST:
    rel = str(p.relative_to(ROOT)).replace("\\", "/")
    if rel.startswith("frontend/src/pages/"):
        continue
    if rel.startswith("frontend/src/composables/"):
        continue
    if rel.startswith("frontend/src/components/"):
        continue
    if rel.endswith((".png", ".svg", ".jpg", ".ico")):
        continue
    FRONTEND_REST_PATHS.append(rel)
for chunk in _chunk(sorted(FRONTEND_REST_PATHS), 5):
    _add_batch(bid, chunk)
    bid += 1
if (ROOT / "frontend/src/main.ts").exists():
    PLANNED_BATCHES.setdefault("frontend/src/main.ts", bid)

# Phase E
_add_batch(
    bid,
    [
        "web/app/index.html",
        "web/app/install.html",
        "web/app/api.js",
        "web/app/layout.js",
        "web/app/preview.html",
    ],
)
bid += 1
_add_batch(
    bid,
    [
        "web/app/preview-v3.html",
        "web/app/preview-v4.html",
        "web/app/preview-v5.html",
        "web/agent/agent.py",
        "web/agent/agent.sh",
    ],
)
bid += 1
_add_batch(
    bid,
    [
        "web/agent/install.sh",
        "web/agent/uninstall.sh",
        "web/agent/config.example.json",
    ],
)

# Phase F tests
TESTS = sorted((ROOT / "tests").rglob("*.py"))
TESTS = [str(p.relative_to(ROOT)).replace("\\", "/") for p in TESTS]
for chunk in _chunk(TESTS, 5):
    _add_batch(bid, chunk)
    bid += 1

DEPLOY = sorted(
    p
    for p in (ROOT / "deploy").rglob("*")
    if p.is_file() and p.name != "gate_log.jsonl"
)
DEPLOY = [str(p.relative_to(ROOT)).replace("\\", "/") for p in DEPLOY]
for chunk in _chunk(DEPLOY, 5):
    _add_batch(bid, chunk)
    bid += 1

SCRIPTS = sorted(
    p
    for p in (ROOT / "scripts").rglob("*")
    if p.is_file() and p.name not in EXCLUDED_JUNK
)
SCRIPTS = [str(p.relative_to(ROOT)).replace("\\", "/") for p in SCRIPTS]
for chunk in _chunk(SCRIPTS, 5):
    _add_batch(bid, chunk)
    bid += 1

DOCKER_DEPLOY_ROOT = sorted(
    [
        "docker/generate_env.py",
        "docker/sync_root_env.py",
        "docker/entrypoint.sh",
        "docker/.env.example",
        "docker/README.md",
        "Dockerfile",
        ".dockerignore",
    ]
)
_add_batch(bid, [p for p in DOCKER_DEPLOY_ROOT if (ROOT / p).exists()])
bid += 1
_add_batch(
    bid,
    [
        "frontend/vite.config.mts",
        "frontend/package.json",
        "requirements.txt",
        "requirements-dev.txt",
        "ruff.toml",
    ],
)


def collect_must_audit() -> list[Path]:
    paths: list[Path] = []
    # server py
    paths.extend(ROOT.glob("server/**/*.py"))
    # frontend src
    for p in (ROOT / "frontend/src").rglob("*"):
        if not p.is_file():
            continue
        if p.suffix not in {".ts", ".vue", ".scss"}:
            continue
        if "assets" in p.parts and p.suffix in {".png", ".svg", ".jpg", ".ico"}:
            continue
        paths.append(p)
    # web app root html/js
    for name in ["*.html", "*.js"]:
        paths.extend((ROOT / "web/app").glob(name))
    # agent
    paths.extend((ROOT / "web/agent").rglob("*"))
    paths = [p for p in paths if p.is_file()]
    # tests
    paths.extend((ROOT / "tests").rglob("*.py"))
    # deploy scripts docker (selected)
    for p in (ROOT / "deploy").rglob("*"):
        if p.is_file() and p.name != "gate_log.jsonl":
            paths.append(p)
    for p in (ROOT / "scripts").rglob("*"):
        if p.is_file() and p.name not in EXCLUDED_JUNK:
            paths.append(p)
    for p in (ROOT / "docker").rglob("*"):
        if p.is_file():
            paths.append(p)
    for extra in [
        "Dockerfile",
        ".dockerignore",
        "frontend/vite.config.mts",
        "frontend/package.json",
        "requirements.txt",
        "requirements-dev.txt",
        "ruff.toml",
    ]:
        ep = ROOT / extra
        if ep.exists():
            paths.append(ep)
    # dedupe
    seen: set[str] = set()
    out: list[Path] = []
    for p in sorted(set(paths), key=lambda x: str(x)):
        rel = str(p.relative_to(ROOT)).replace("\\", "/")
        if rel in seen:
            continue
        seen.add(rel)
        out.append(p)
    return out


def line_count(path: Path) -> int:
    try:
        return sum(1 for _ in path.open("rb"))
    except OSError:
        return 0


def main() -> None:
    must = collect_must_audit()
    entries: list[dict] = []
    covered_assets = 0

    for path in sorted((ROOT / "web/app/assets").rglob("*"), key=lambda x: str(x)):
        if not path.is_file():
            continue
        rel = str(path.relative_to(ROOT)).replace("\\", "/")
        entries.append(
            {
                "path": rel,
                "category": "generated",
                "lines": line_count(path),
                "batch_id": None,
                "status": "covered-by-frontend-src",
                "needs_full_rescan": False,
                "planned_batch": "B-Assets",
            }
        )
        covered_assets += 1

    for path in must:
        rel = str(path.relative_to(ROOT)).replace("\\", "/")
        if rel in EXCLUDED_JUNK:
            entries.append(
                {
                    "path": rel,
                    "category": "excluded-junk",
                    "lines": line_count(path),
                    "batch_id": None,
                    "status": "excluded",
                    "needs_full_rescan": False,
                }
            )
            continue
        if rel in EXCLUDED_LOCAL:
            entries.append(
                {
                    "path": rel,
                    "category": "excluded-local",
                    "lines": line_count(path),
                    "batch_id": None,
                    "status": "excluded",
                    "needs_full_rescan": False,
                    "note": "local secrets; audit docker/.env.example instead",
                }
            )
            continue
        prev = BATCH_1_6.get(rel, {})
        planned = PLANNED_BATCHES.get(rel)
        n = line_count(path)
        large = n > LARGE_FILE_THRESHOLD
        entry = {
            "path": rel,
            "category": "must_audit",
            "lines": n,
            "large_file": large,
            "batch_id": prev.get("batch_id") or planned,
            "planned_batch": planned,
            "status": prev.get("status", "pending"),
            "needs_full_rescan": prev.get("needs_full_rescan", False),
            "read_range": None,
            "findings_count": 0,
        }
        entries.append(entry)

    must_audit = [e for e in entries if e["category"] == "must_audit"]
    pending = sum(1 for e in must_audit if e["status"] == "pending")
    partial_rescan = sum(1 for e in must_audit if e.get("needs_full_rescan"))
    done = sum(1 for e in must_audit if e["status"] == "done")

    doc = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "summary": {
            "total_must_audit": len(must_audit),
            "pending": pending,
            "done": done,
            "partial_rescan": partial_rescan,
            "covered_by_frontend_src_assets": covered_assets,
            "excluded_junk": len(EXCLUDED_JUNK),
            "max_batch_id": max(PLANNED_BATCHES.values()) if PLANNED_BATCHES else 0,
        },
        "entries": entries,
    }
    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text(json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    print(json.dumps(doc["summary"], indent=2))


if __name__ == "__main__":
    main()
