#!/usr/bin/env python3
"""maketool-refscan
 
Option B: String reference scan

- Default: scan current working directory; show ONLY unused candidates.
- Use --show-used to print used files too.
- Optional --csv to write a report.

This is intentionally heuristic: it searches for filename/path *tokens* inside
project text sources. It's best for catching unused assets (.ui/.ico/.png/.qss/etc.)
and "orphan" files. For definitive Python reachability, use an import/AST walk.
"""

from __future__ import annotations

import argparse
import csv
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional


DEFAULT_IGNORE_DIRS = {
    ".git", ".hg", ".svn",
    "__pycache__", ".mypy_cache", ".pytest_cache",
    ".venv", "venv", "env",
    "build", "dist", ".tox",
}

# Text-like files we scan for references
DEFAULT_SCAN_EXTS = {
    ".py", ".ui", ".qrc", ".qss",
    ".bat", ".cmd", ".ps1",
    ".txt", ".md", ".rst",
    ".ini", ".cfg", ".toml",
    ".json", ".yaml", ".yml",
    ".xml", ".html", ".htm", ".css",
}

# Binary-ish files we should not attempt to read as text sources
DEFAULT_SKIP_CONTENT_EXTS = {
    ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg",
    ".ico", ".icns",
    ".pdf", ".zip", ".7z", ".rar",
    ".db", ".sqlite", ".dll", ".exe", ".pyd",
}


@dataclass(frozen=True)
class Candidate:
    path: Path
    rel: str
    tokens: tuple[str, ...]


def iter_files(root: Path, ignore_dirs: set[str]) -> Iterable[Path]:
    for p in root.rglob("*"):
        if p.is_dir():
            continue
        if any(part in ignore_dirs for part in p.parts):
            continue
        yield p


def safe_read_text(p: Path) -> Optional[str]:
    try:
        return p.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return None


def norm(s: str) -> str:
    return s.replace("\\", "/").lower()


def build_candidates(root: Path, files: list[Path]) -> list[Candidate]:
    out: list[Candidate] = []
    for p in files:
        rel = str(p.relative_to(root))
        rel_norm = norm(rel)
        base = p.name.lower()
        stem = p.stem.lower()

        tokens = {
            base,               # e.g. rat.ico
            stem,               # e.g. common (helps Python imports like 'import common')
            rel_norm,           # e.g. icons/rat.ico
            rel_norm.replace("/", "\\"),  # windows style
        }

        out.append(Candidate(p, rel, tuple(t for t in tokens if t)))
    return out


def is_text_source(p: Path, scan_exts: set[str], skip_exts: set[str]) -> bool:
    ext = p.suffix.lower()
    if ext in skip_exts:
        return False
    return ext in scan_exts


def _print_group(title: str, rows: list[tuple[Candidate, list[str]]]) -> None:
    print(title)
    print("-" * len(title))
    for c, refs in rows:
        if refs:
            preview = ", ".join(refs[:3]) + (" ..." if len(refs) > 3 else "")
            print(f"{c.rel}   <-- {preview}")
        else:
            print(c.rel)
    print()


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--root", default=".", help="Project root (default: current directory)")
    ap.add_argument("--show-used", action="store_true", help="Show used files as well")
    ap.add_argument("--csv", default="", help="Write CSV report (optional path)")
    ap.add_argument("--ignore-dir", action="append", default=[], help="Additional directory names to ignore")
    ap.add_argument("--scan-ext", action="append", default=[], help="Additional extensions to scan as text sources (e.g. .ts)")
    ap.add_argument("--no-default-scan-exts", action="store_true", help="Do not include the default scan extensions")
    args = ap.parse_args()

    root = Path(args.root).resolve()
    ignore_dirs = set(DEFAULT_IGNORE_DIRS) | set(args.ignore_dir)

    if args.no_default_scan_exts:
        scan_exts = set(e.lower() for e in args.scan_ext if e.startswith("."))
    else:
        scan_exts = set(DEFAULT_SCAN_EXTS) | set(e.lower() for e in args.scan_ext if e.startswith("."))

    files = sorted(iter_files(root, ignore_dirs))
    candidates = build_candidates(root, files)
    text_sources = [p for p in files if is_text_source(p, scan_exts, DEFAULT_SKIP_CONTENT_EXTS)]

    references: dict[str, list[str]] = {c.rel: [] for c in candidates}

    for src in text_sources:
        src_rel = str(src.relative_to(root))
        text = safe_read_text(src)
        if not text:
            continue
        hay = text.lower()

        for c in candidates:
            if src == c.path:
                continue
            if any(tok in hay for tok in c.tokens):
                references[c.rel].append(src_rel)

    used: list[tuple[Candidate, list[str]]] = []
    unused: list[tuple[Candidate, list[str]]] = []

    for c in candidates:
        refs = references[c.rel]
        if refs:
            used.append((c, refs))
        else:
            unused.append((c, refs))

    _print_group("UNUSED files (no filename/path tokens found)", unused)
    if args.show_used:
        _print_group("USED files (token found somewhere)", used)

    if args.csv:
        csv_path = Path(args.csv).resolve()
        with csv_path.open("w", newline="", encoding="utf-8") as f:
            w = csv.writer(f)
            w.writerow(["file", "status", "ref_count", "ref_sources_sample"])
            for c, _ in unused:
                w.writerow([c.rel, "UNUSED", 0, ""])
            for c, refs in used:
                w.writerow([c.rel, "USED", len(refs), "; ".join(refs[:10])])
        print(f"CSV written: {csv_path}")

    return 0


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