"""Audit the actual usage of every ``mgf-common`` public symbol across consumers.

Run::

    python tools/audit_consumers.py

Outputs a Markdown report to stdout. Redirect to
``docs/internal/CONSUMER_AUDIT_v0.5.md`` to update the committed audit.

The audit drives v0.5 reshape decisions:

  - Which public surfaces are unused after multiple consumers and could be
    deprecated cheaply (e.g. ``SchedulerError``).
  - Which v0.1 legacy patterns are still consumed (``configure()``,
    ``make_settings_config``, multi-call wiring) — those need consumer-side
    coordination before v0.5 Phase D ships deprecation warnings.
  - Which surfaces are heavily used (and therefore must not break in any
    v0.5 work).

Implementation: pure Python (no ripgrep / grep dep), reads each .py file
and applies regex matches. Counts are **lower bounds** (false positives
possible: e.g. ``CommandError`` matches Click too); the verdict column
flags ambiguities.

Re-run after each consumer migration to track progress.
"""

from __future__ import annotations

import importlib
import json
import re
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path

# ---------------------------------------------------------------------------
# Inputs.
# ---------------------------------------------------------------------------

PUBLIC_MODULES: tuple[str, ...] = (
    "mgf.common",
    "mgf.common.exceptions",
    "mgf.common.config",
    "mgf.common.observability",
    "mgf.common.vault",
    "mgf.common.cli",
    "mgf.common.registry",
    "mgf.common.typing",
    "mgf.common.progress",
    "mgf.common.progress.asyncio",
)

CONSUMERS: tuple[tuple[str, Path], ...] = (
    ("vm-vmanager-app", Path("/home/bassam/PycharmProjects/vm-vmanager-app")),
    ("PlasmaMapper", Path("/home/bassam/PycharmProjects/PlasmaMapper")),
    ("mgf-apiprobe", Path("/home/bassam/PycharmProjects/mgf-apiprobe")),
)

SCAN_SUBDIRS: tuple[str, ...] = (
    "src",
    "tests",
    "tools",
    "scripts",
    "apps",
    "config",
)

# Skip __pycache__ and .venv when walking trees.
SKIP_DIR_NAMES: frozenset[str] = frozenset(
    {"__pycache__", ".venv", "venv", ".git", "build", "dist", ".pytest_cache",
     ".mypy_cache", ".ruff_cache", "node_modules", "site-packages"}
)

# Surfaces we DO NOT count as "real usage" — re-exports + pydantic
# convenience names that aren't really mgf-common-shaped.
RE_EXPORT_NAMES: frozenset[str] = frozenset(
    {
        "AliasChoices",
        "BaseModel",
        "ConfigDict",
        "Field",
        "ValidationError",
        "field_validator",
        "model_validator",
    }
)

# v0.1 legacy patterns we want to flag specifically — these will get
# deprecation warnings in v0.5 Phase D.
LEGACY_PATTERNS: frozenset[str] = frozenset(
    {
        "configure",
        "make_settings_config",
        "setup_logging",
        "setup_otel",
        "install_excepthook",
        "install_threading_excepthook",
    }
)


@dataclass(frozen=True, slots=True)
class SymbolUsage:
    """How one public symbol shows up in one consumer."""

    consumer: str
    symbol: str
    module: str
    direct_imports: int
    qualified_uses: int
    bare_mentions: int

    @property
    def total(self) -> int:
        return self.direct_imports + self.qualified_uses + self.bare_mentions


# ---------------------------------------------------------------------------
# Surface discovery.
# ---------------------------------------------------------------------------


def discover_public_surface() -> dict[str, list[str]]:
    """Map module name → sorted list of public names from ``__all__``."""
    surface: dict[str, list[str]] = {}
    for mod_name in PUBLIC_MODULES:
        mod = importlib.import_module(mod_name)
        names = list(getattr(mod, "__all__", ()) or ())
        names = [n for n in names if n not in RE_EXPORT_NAMES]
        surface[mod_name] = sorted(names)
    return surface


# ---------------------------------------------------------------------------
# File walking.
# ---------------------------------------------------------------------------


def _iter_py_files(root: Path) -> list[Path]:
    """Yield every .py file under ``root``, skipping cache + venv dirs."""
    if not root.exists():
        return []
    paths: list[Path] = []
    candidates: list[Path] = []
    for sub in SCAN_SUBDIRS:
        candidate = root / sub
        if candidate.exists():
            candidates.append(candidate)
    if not candidates:
        candidates = [root]
    for base in candidates:
        for path in base.rglob("*.py"):
            # Skip dirs in the SKIP set anywhere in the path.
            if any(part in SKIP_DIR_NAMES for part in path.parts):
                continue
            paths.append(path)
    return paths


# ---------------------------------------------------------------------------
# Pattern matching.
# ---------------------------------------------------------------------------


_FILE_TEXT_CACHE: dict[Path, str] = {}


def _read(path: Path) -> str:
    cached = _FILE_TEXT_CACHE.get(path)
    if cached is not None:
        return cached
    try:
        text = path.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError):
        text = ""
    _FILE_TEXT_CACHE[path] = text
    return text


def measure_usage(consumer: str, root: Path, module: str, symbol: str) -> SymbolUsage:
    """Count how often ``symbol`` from ``module`` appears in ``root``.

    - ``direct_imports``: # of files where ``from <module> import ...``
      includes ``<symbol>`` (multiline-aware via re.DOTALL on the import
      block).
    - ``qualified_uses``: # of line-level matches for ``<module>.<symbol>``.
    - ``bare_mentions``: bare ``<symbol>`` matches in files that import
      from ``mgf.common`` somewhere (limits false positives from
      same-named symbols in other libs).
    """
    if not root.exists():
        return SymbolUsage(consumer, symbol, module, 0, 0, 0)

    files = _iter_py_files(root)
    if not files:
        return SymbolUsage(consumer, symbol, module, 0, 0, 0)

    module_re = re.escape(module)
    symbol_re = re.escape(symbol)

    # Multiline-aware import: matches both
    #   from <module> import <symbol>
    # AND
    #   from <module> import (
    #       ...,
    #       <symbol>,
    #       ...,
    #   )
    direct_pattern = re.compile(
        rf"from {module_re} import\s*\(?[^)\n]*?\b{symbol_re}\b",
        re.DOTALL,
    )
    # For multi-line parenthesized imports — match the full block.
    direct_block_pattern = re.compile(
        rf"from {module_re} import\s*\([^)]*?\b{symbol_re}\b[^)]*?\)",
        re.DOTALL,
    )
    qualified_pattern = re.compile(rf"\b{module_re}\.{symbol_re}\b")
    bare_pattern = re.compile(rf"\b{symbol_re}\b")
    has_mgf_import = re.compile(r"from mgf\.common")

    direct = 0
    qualified = 0
    bare = 0

    for path in files:
        text = _read(path)
        if not text:
            continue

        # Direct imports — file count, not line count.
        if direct_block_pattern.search(text) or direct_pattern.search(text):
            direct += 1

        # Qualified uses — line count.
        qualified += len(qualified_pattern.findall(text))

        # Bare mentions — only in files that import from mgf.common,
        # excluding qualified uses (already counted above) and the
        # symbol-on-import line (we want USE not declaration).
        if has_mgf_import.search(text):
            all_bare = len(bare_pattern.findall(text))
            # Subtract qualified-use occurrences (they include the symbol).
            qualified_in_text = len(qualified_pattern.findall(text))
            bare += max(0, all_bare - qualified_in_text)

    return SymbolUsage(
        consumer=consumer,
        symbol=symbol,
        module=module,
        direct_imports=direct,
        qualified_uses=qualified,
        bare_mentions=bare,
    )


# ---------------------------------------------------------------------------
# Report rendering.
# ---------------------------------------------------------------------------


def render_report(
    surface: dict[str, list[str]],
    by_module_consumer: dict[tuple[str, str], dict[str, SymbolUsage]],
) -> str:
    """Render the audit as Markdown."""
    lines: list[str] = []
    lines.append("# Consumer Audit — v0.5 reshape pre-flight")
    lines.append("")
    lines.append("**Generated by:** `python tools/audit_consumers.py`")
    lines.append("**Re-run after every consumer migration.**")
    lines.append("")
    lines.append(
        "This report counts textual occurrences of every `mgf-common` "
        "public symbol across consumer source trees. Counts are **lower "
        "bounds** (false positives possible: e.g. `CommandError` matches "
        "Click too)."
    )
    lines.append("")

    # ── §1 — High-level summary ───────────────────────────────────────────
    lines.append("## §1 Summary")
    lines.append("")
    lines.append("**Surfaces consumed (≥1 direct import or qualified use):**")
    lines.append("")
    lines.append("| Module | Public names | VM | PM | apiprobe |")
    lines.append("|---|---:|---:|---:|---:|")
    for module, names in surface.items():
        consumed = {}
        for consumer, _ in CONSUMERS:
            count = sum(
                1
                for n in names
                if (
                    by_module_consumer.get((module, consumer), {})
                    .get(n, SymbolUsage(consumer, n, module, 0, 0, 0))
                    .direct_imports
                    + by_module_consumer.get((module, consumer), {})
                    .get(n, SymbolUsage(consumer, n, module, 0, 0, 0))
                    .qualified_uses
                )
                > 0
            )
            consumed[consumer] = count
        lines.append(
            f"| `{module}` | {len(names)} | "
            f"{consumed['vm-vmanager-app']} | "
            f"{consumed['PlasmaMapper']} | "
            f"{consumed['mgf-apiprobe']} |"
        )
    lines.append("")

    # ── §2 — Per-symbol usage table ────────────────────────────────────────
    lines.append("## §2 Per-symbol usage")
    lines.append("")
    lines.append("Format: `direct_imports / qualified_uses / bare_mentions`.")
    lines.append("")
    lines.append("| Module | Symbol | VM | PM | apiprobe | Verdict |")
    lines.append("|---|---|---|---|---|---|")
    for module, names in surface.items():
        for symbol in names:
            cells = []
            total_across = 0
            for consumer, _ in CONSUMERS:
                usage = by_module_consumer.get((module, consumer), {}).get(
                    symbol,
                    SymbolUsage(consumer, symbol, module, 0, 0, 0),
                )
                cell = (
                    f"{usage.direct_imports}/{usage.qualified_uses}/"
                    f"{usage.bare_mentions}"
                )
                cells.append(cell)
                total_across += usage.direct_imports + usage.qualified_uses
            verdict = _verdict(symbol, total_across)
            lines.append(
                f"| `{module}` | `{symbol}` | "
                f"{cells[0]} | {cells[1]} | {cells[2]} | {verdict} |"
            )
    lines.append("")

    # ── §3 — Unused public symbols ────────────────────────────────────────
    lines.append("## §3 Unused public symbols (deprecation candidates)")
    lines.append("")
    unused: list[tuple[str, str]] = []
    for module, names in surface.items():
        for symbol in names:
            total = sum(
                by_module_consumer.get((module, consumer), {})
                .get(symbol, SymbolUsage(consumer, symbol, module, 0, 0, 0))
                .direct_imports
                + by_module_consumer.get((module, consumer), {})
                .get(symbol, SymbolUsage(consumer, symbol, module, 0, 0, 0))
                .qualified_uses
                for consumer, _ in CONSUMERS
            )
            if total == 0:
                unused.append((module, symbol))
    if not unused:
        lines.append("(none)")
    else:
        lines.append(
            "These have **zero direct imports + qualified uses** in any "
            "audited consumer. Some are essential primitives nobody touches "
            "directly (`AppContext`, `current_context`); others may be "
            "genuinely unused."
        )
        lines.append("")
        for module, symbol in unused:
            note = ""
            if symbol == "SchedulerError":
                note = " — **open question #7 in plan**: drop?"
            lines.append(f"- `{module}.{symbol}`{note}")
    lines.append("")

    # ── §4 — Legacy v0.1 patterns ─────────────────────────────────────────
    lines.append("## §4 Legacy v0.1 patterns still in active use")
    lines.append("")
    lines.append(
        "These will get deprecation warnings in v0.5 Phase D. Each "
        "consumer-side migration must be coordinated before that ships."
    )
    lines.append("")
    lines.append("| Symbol | VM | PM | apiprobe |")
    lines.append("|---|---:|---:|---:|")
    for symbol in sorted(LEGACY_PATTERNS):
        cells = []
        for consumer, _ in CONSUMERS:
            total = sum(
                by_module_consumer.get((module, consumer), {})
                .get(symbol, SymbolUsage(consumer, symbol, module, 0, 0, 0))
                .direct_imports
                + by_module_consumer.get((module, consumer), {})
                .get(symbol, SymbolUsage(consumer, symbol, module, 0, 0, 0))
                .qualified_uses
                for module in surface
            )
            cells.append(str(total))
        lines.append(f"| `{symbol}` | {cells[0]} | {cells[1]} | {cells[2]} |")
    lines.append("")

    # ── §5 — Reproduction ─────────────────────────────────────────────────
    lines.append("## §5 Reproduction")
    lines.append("")
    lines.append("```bash")
    lines.append(
        "python tools/audit_consumers.py > docs/internal/CONSUMER_AUDIT_v0.5.md"
    )
    lines.append("```")
    lines.append("")
    lines.append("Generated against:")
    for consumer, path in CONSUMERS:
        exists = "OK" if path.exists() else "MISSING"
        lines.append(f"- {consumer}: `{path}` ({exists})")
    lines.append("")

    return "\n".join(lines)


def _verdict(symbol: str, total: int) -> str:
    """Bucket the symbol's usage."""
    if symbol == "SchedulerError" and total == 0:
        return "**candidate-drop (#7)**"
    if symbol in LEGACY_PATTERNS and total > 0:
        return "**deprecate-v0.5**"
    if total == 0:
        return "unused"
    if total < 5:
        return "light"
    if total < 30:
        return "moderate"
    return "heavy"


# ---------------------------------------------------------------------------
# Entry point.
# ---------------------------------------------------------------------------


def run() -> str:
    """Run the audit and return the rendered Markdown."""
    surface = discover_public_surface()
    by_module_consumer: dict[tuple[str, str], dict[str, SymbolUsage]] = defaultdict(
        dict
    )

    for consumer, root in CONSUMERS:
        if not root.exists():
            continue
        for module, names in surface.items():
            for symbol in names:
                usage = measure_usage(consumer, root, module, symbol)
                by_module_consumer[(module, consumer)][symbol] = usage

    return render_report(surface, by_module_consumer)


def emit_json_summary(path: Path) -> None:
    """Emit a JSON summary of usage counts (for CI / regression tracking)."""
    surface = discover_public_surface()
    summary: dict[str, dict[str, dict[str, int]]] = {}
    for module, names in surface.items():
        summary[module] = {}
        for symbol in names:
            summary[module][symbol] = {}
            for consumer, root in CONSUMERS:
                if not root.exists():
                    summary[module][symbol][consumer] = 0
                    continue
                usage = measure_usage(consumer, root, module, symbol)
                summary[module][symbol][consumer] = (
                    usage.direct_imports + usage.qualified_uses
                )
    path.write_text(json.dumps(summary, indent=2, sort_keys=True))


if __name__ == "__main__":
    print(run())
