"""Command-line entry point for delta-reduce.

Usage:
    delta-reduce --test <script> --input <file> [-n <threads>]
    python -m delta_reduce --test <script> --input <file> [-n <threads>]
"""
from __future__ import annotations

import argparse
import pathlib
import subprocess
import tempfile
import threading
from typing import Callable

_IsCancel = Callable[[], bool]

from delta_reduce.core import run_to_fixpoint

_thread_local = threading.local()


def _get_thread_tmpdir() -> pathlib.Path:
    """Return (lazily creating) a per-thread temporary directory."""
    if not hasattr(_thread_local, "tmpdir"):
        _thread_local.tmpdir = pathlib.Path(tempfile.mkdtemp())
    return _thread_local.tmpdir


def _make_test_fn(
    test_script: pathlib.Path,
    input_name: str,
) -> Callable[[list[str], _IsCancel], bool]:
    """Build the test_fn callable that writes a candidate file and runs the script."""
    test_script_abs = str(test_script.resolve())

    def test_fn(candidate: list[str], is_cancelled: Callable[[], bool]) -> bool:
        tmpdir = _get_thread_tmpdir()
        candidate_file = tmpdir / input_name
        candidate_file.write_text("".join(candidate))
        proc = subprocess.Popen(
            [test_script_abs],
            cwd=str(tmpdir),
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        while True:
            try:
                proc.wait(timeout=0.05)
                return proc.returncode == 0
            except subprocess.TimeoutExpired:
                if is_cancelled():
                    proc.kill()
                    proc.wait()
                    return False

    return test_fn


def main(argv: list[str] | None = None) -> None:
    parser = argparse.ArgumentParser(
        prog="delta-reduce",
        description="Reduce a file to a minimal version that still satisfies a test.",
    )
    parser.add_argument("--test", required=True, metavar="SCRIPT", help="Test script")
    parser.add_argument("--input", required=True, metavar="FILE", help="Input file to reduce")
    parser.add_argument(
        "-n", type=int, default=4, metavar="N", help="Number of parallel threads (default: 4)"
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="Print each deletion attempt"
    )
    args = parser.parse_args(argv)

    input_path = pathlib.Path(args.input).resolve()
    input_name = input_path.name
    lines = input_path.read_text().splitlines(keepends=True)

    # Backup original file (only on first run)
    backup_path = input_path.with_name(input_name + ".orig")
    if not backup_path.exists():
        backup_path.write_text("".join(lines))

    print(f"{len(lines)} lines remaining ({args.n} threads)", flush=True)

    def on_progress(current_lines: list[str]) -> None:
        input_path.write_text("".join(current_lines))
        print(f"{len(current_lines)} lines remaining", flush=True)

    test_fn = _make_test_fn(pathlib.Path(args.test), input_name)

    reduced = run_to_fixpoint(lines, test_fn, n_threads=args.n, on_progress=on_progress, verbose=args.verbose)

    # Final write (in case the last pass made progress but on_progress already wrote it)
    input_path.write_text("".join(reduced))


if __name__ == "__main__":
    main()
