"""Adapter LOC budget check.

Every file under `src/acaua/adapters/*.py` (excluding `__init__.py`) must
stay under `BUDGET` logical lines. "Logical" = non-blank, non-comment,
non-docstring. This guards premise 3 of the design doc — the "unification
won't leak into a framework" bet — by keeping per-backend adapter code
lean. If an adapter grows past the budget, that is a signal the abstraction
is leaking and the design needs rework, not that the budget needs raising.

Usage:
    python scripts/check_adapter_budget.py [--budget 300] [--path src/acaua/adapters]

Exit codes:
    0 — all adapters under budget (or no adapters present)
    1 — one or more adapters exceed budget
    2 — invocation error (path doesn't exist, source has syntax errors, ...)
"""

from __future__ import annotations

import argparse
import ast
import sys
from pathlib import Path

DEFAULT_BUDGET = 300


def count_logical_lines(source: str) -> int:
    """Count lines that contain real code.

    Strips: blank lines, pure-comment lines, and docstring lines (the first
    `Expr` of a module / class / function whose value is a string). Inline
    comments on a code line do not reduce the count — a `return x  # done`
    counts once.
    """
    tree = ast.parse(source)

    docstring_lines: set[int] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef):
            body = getattr(node, "body", None)
            if not body:
                continue
            first = body[0]
            if (
                isinstance(first, ast.Expr)
                and isinstance(first.value, ast.Constant)
                and isinstance(first.value.value, str)
            ):
                end = first.end_lineno or first.lineno
                for lineno in range(first.lineno, end + 1):
                    docstring_lines.add(lineno)

    count = 0
    for i, line in enumerate(source.splitlines(), start=1):
        stripped = line.strip()
        if not stripped or stripped.startswith("#") or i in docstring_lines:
            continue
        count += 1
    return count


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--path",
        default="src/acaua/adapters",
        help="Directory to scan (default: src/acaua/adapters)",
    )
    parser.add_argument(
        "--budget",
        type=int,
        default=DEFAULT_BUDGET,
        help=f"Max logical lines per adapter (default: {DEFAULT_BUDGET})",
    )
    args = parser.parse_args(argv)

    path = Path(args.path)
    if not path.is_dir():
        print(f"error: {path} is not a directory", file=sys.stderr)
        return 2

    violations: list[tuple[str, int]] = []
    checked = 0
    for adapter_file in sorted(path.glob("*.py")):
        if adapter_file.name == "__init__.py":
            continue
        checked += 1
        source = adapter_file.read_text()
        try:
            lines = count_logical_lines(source)
        except SyntaxError as e:
            print(f"error: {adapter_file}: {e}", file=sys.stderr)
            return 2
        status = "PASS" if lines <= args.budget else "FAIL"
        print(f"{status}  {adapter_file}: {lines} / {args.budget} logical lines")
        if lines > args.budget:
            violations.append((str(adapter_file), lines))

    if checked == 0:
        print(f"warning: no adapters found under {path}", file=sys.stderr)
        return 0

    if violations:
        print(
            f"\n{len(violations)} adapter(s) over budget of {args.budget} logical lines:",
            file=sys.stderr,
        )
        for f, n in violations:
            print(f"  {f}: {n} lines", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
