#!/usr/bin/env python3
"""Report Solidity contracts that are not reachable from current tests or scripts."""

from __future__ import annotations

import argparse
import json
import re
import sys
from collections import Counter
from pathlib import Path


EXCLUDED_PARTS = {"artifacts", "broadcast", "cache", "lib", "node_modules", "out"}
IMPORT_RE = re.compile(r'^\s*import\s+(?:\{[^}]+\}\s+from\s+)?["\']([^"\']+)["\'];', re.M)


def discover_sources(repo_root: Path) -> dict[str, Path]:
    return {
        path.relative_to(repo_root).as_posix(): path
        for path in repo_root.rglob("*.sol")
        if path.is_file() and not any(part in EXCLUDED_PARTS for part in path.parts)
    }


def resolve_import(importer: Path, import_path: str, repo_root: Path) -> str | None:
    if import_path.startswith(("./", "../")):
        target = (importer.parent / import_path).resolve()
        try:
            return target.relative_to(repo_root).as_posix()
        except ValueError:
            return None

    if import_path.startswith("@emoney/"):
        return f"contracts/emoney/{import_path[len('@emoney/'):]}"

    return None


def build_graph(repo_root: Path, files: dict[str, Path]) -> tuple[dict[str, list[str]], dict[str, set[str]]]:
    imports: dict[str, list[str]] = {}
    inbound: dict[str, set[str]] = {rel: set() for rel in files}

    for rel_path, file_path in files.items():
        text = file_path.read_text(errors="ignore")
        resolved_targets: list[str] = []
        for import_path in IMPORT_RE.findall(text):
            target = resolve_import(file_path, import_path, repo_root)
            if target and target in files:
                resolved_targets.append(target)
                inbound[target].add(rel_path)
        imports[rel_path] = resolved_targets

    return imports, inbound


def walk_reachable(imports: dict[str, list[str]], roots: list[str]) -> set[str]:
    seen: set[str] = set()
    stack = list(roots)
    while stack:
        current = stack.pop()
        if current in seen:
            continue
        seen.add(current)
        stack.extend(imports.get(current, []))
    return seen


def create_report(repo_root: Path) -> dict[str, object]:
    files = discover_sources(repo_root)
    imports, inbound = build_graph(repo_root, files)

    contract_files = sorted(rel for rel in files if rel.startswith("contracts/"))
    root_files = sorted(rel for rel in files if rel.startswith(("test/", "script/")))
    reachable = walk_reachable(imports, root_files)

    unreachable = [rel for rel in contract_files if rel not in reachable]
    no_inbound = [rel for rel in contract_files if not inbound[rel]]

    unreachable_by_bucket = Counter(path.split("/")[1] for path in unreachable)
    no_inbound_by_bucket = Counter(path.split("/")[1] for path in no_inbound)

    return {
        "repoRoot": repo_root.as_posix(),
        "summary": {
            "contractsTotal": len(contract_files),
            "rootsTotal": len(root_files),
            "contractsReachableFromTestsOrScripts": len(contract_files) - len(unreachable),
            "contractsUnreachableFromTestsOrScripts": len(unreachable),
            "contractsWithNoLocalInboundRefs": len(no_inbound),
        },
        "unreachableByBucket": dict(unreachable_by_bucket.most_common()),
        "noInboundByBucket": dict(no_inbound_by_bucket.most_common()),
        "unreachableContracts": unreachable,
        "contractsWithNoLocalInboundRefs": no_inbound,
    }


def print_text(report: dict[str, object]) -> None:
    summary = report["summary"]
    print(f"repo root: {report['repoRoot']}")
    print(f"contracts total: {summary['contractsTotal']}")
    print(f"tests/scripts roots total: {summary['rootsTotal']}")
    print(f"contracts reachable from tests or scripts: {summary['contractsReachableFromTestsOrScripts']}")
    print(f"contracts unreachable from tests or scripts: {summary['contractsUnreachableFromTestsOrScripts']}")
    print(f"contracts with no local inbound refs: {summary['contractsWithNoLocalInboundRefs']}")
    print()
    print("Unreachable by top-level bucket:")
    for bucket, count in report["unreachableByBucket"].items():
        print(f"  {bucket}: {count}")
    print()
    print("Archive candidates (unreachable from current tests/scripts):")
    for contract in report["unreachableContracts"]:
        print(f"  {contract}")
    print()
    print("Note: unreachable does not prove safe deletion; it only means this repo's current Solidity tests/scripts do not import the file.")


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
    return parser.parse_args(argv)


def main(argv: list[str]) -> int:
    args = parse_args(argv)
    repo_root = Path(__file__).resolve().parents[2]
    report = create_report(repo_root)

    if args.json:
        json.dump(report, sys.stdout, indent=2)
        sys.stdout.write("\n")
        return 0

    print_text(report)
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
