"""pvlint — the ParVagues track linter.

    python3 tools/pvlint live/**/*.tidal        # check
    python3 tools/pvlint --fix live/foo.tidal   # black-style autofix in place
    python3 tools/pvlint --setlist live/a.tidal live/b.tidal   # + orphan check

Exit codes: 0 clean, 1 errors found, 2 warnings only (with --strict), 3 usage.

Design note — why autofix is conservative
-----------------------------------------
This runs on files that are performed live. A fix that is *usually* right is
worse than no fix, because the failure mode is silence in front of an audience.
So only rules that carry an exact replacement offer `--fix`, and a rule offers
one only when the correct output is unambiguous (delete a shadowing `let`, add
the missing parens around an operator section). Everything else reports and
lets a human decide.
"""

from __future__ import annotations

import argparse
import os
import sys

try:  # `python3 -m tools.pvlint` / imported as a package
    from .core import Track, load
    from .rules import check
except ImportError:  # `python3 tools/pvlint …` — dir-as-script, no parent package
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    from pvlint.core import Track, load
    from pvlint.rules import check

COLOR = {"error": "\033[31m", "warning": "\033[33m", "info": "\033[36m"}
RESET = "\033[0m"
BOLD = "\033[1m"


def render(track: Track, findings: list, show_info: bool, use_color: bool) -> None:
    shown = [f for f in findings if show_info or f.severity != "info"]
    if not shown:
        return
    c = (lambda s, k: f"{COLOR[k]}{s}{RESET}") if use_color else (lambda s, k: s)
    b = (lambda s: f"{BOLD}{s}{RESET}") if use_color else (lambda s: s)
    print(b(track.path))
    for f in shown:
        loc = f"{f.line}:"
        sev = c(f.severity.upper().ljust(7), f.severity)
        print(f"  {loc:>6} {sev} {f.rule}  {f.message}")
        if f.detail:
            for ln in f.detail.split("\n"):
                print(f"         {ln}")
    print()


def apply_fixes(track: Track, findings: list) -> tuple[str, int]:
    """Apply line-replacement fixes bottom-up so earlier line numbers stay valid."""
    lines = track.text.splitlines()
    fixable = [f for f in findings if f.fix is not None]
    applied = 0
    for f in sorted(fixable, key=lambda f: -f.line):
        idx = f.line - 1
        if idx < 0 or idx >= len(lines):
            continue
        if f.fix == "":
            del lines[idx]
        else:
            lines[idx] = f.fix
        applied += 1
    text = "\n".join(lines)
    if track.text.endswith("\n"):
        text += "\n"
    return text, applied


def orphan_report(tracks: list[Track]) -> int:
    """Cross-track check: what does each transition leave playing?"""
    print(f"{BOLD}orphan-orbit transitions{RESET}")
    worst = 0
    for a, b in zip(tracks, tracks[1:]):
        left = sorted(a.declared_orbits() - b.declared_orbits())
        if left:
            worst = max(worst, len(left))
            names = ", ".join(f"d{o}" for o in left)
            print(f"  {a.path.split('/')[-1]} -> {b.path.split('/')[-1]}: "
                  f"{COLOR['warning']}{names} keep playing{RESET}")
        else:
            print(f"  {a.path.split('/')[-1]} -> {b.path.split('/')[-1]}: clean")
    print()
    return worst


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(prog="pvlint", description=__doc__.split("\n")[0])
    ap.add_argument("paths", nargs="+", help=".tidal files to check")
    ap.add_argument("--fix", action="store_true", help="apply safe autofixes in place")
    ap.add_argument("--info", action="store_true", help="also show info findings")
    ap.add_argument("--strict", action="store_true", help="warnings fail too")
    ap.add_argument("--setlist", action="store_true",
                    help="treat paths as an ordered setlist and report orphan orbits")
    ap.add_argument("--quiet", action="store_true", help="only print a summary")
    ap.add_argument("--no-color", action="store_true")
    args = ap.parse_args(argv)

    use_color = not args.no_color and sys.stdout.isatty()
    tracks: list[Track] = []
    errors = warnings = fixed = 0

    for path in args.paths:
        try:
            track = load(path)
        except OSError as e:
            print(f"{path}: cannot read ({e})", file=sys.stderr)
            errors += 1
            continue
        tracks.append(track)
        findings = check(track)

        if args.fix:
            text, n = apply_fixes(track, findings)
            if n:
                with open(path, "w", encoding="utf-8") as fh:
                    fh.write(text)
                fixed += n
                track = load(path)
                tracks[-1] = track
                findings = check(track)

        if not args.quiet:
            render(track, findings, args.info, use_color)
        errors += sum(1 for f in findings if f.severity == "error")
        warnings += sum(1 for f in findings if f.severity == "warning")

    if args.setlist and len(tracks) > 1:
        orphan_report(tracks)

    n = len(tracks)
    summary = f"pvlint: {n} track(s), {errors} error(s), {warnings} warning(s)"
    if args.fix:
        summary += f", {fixed} autofix(es) applied"
    print(summary)

    if errors:
        return 1
    if warnings and args.strict:
        return 2
    return 0


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