"""Command-line interface for code recorder processor."""

from __future__ import annotations

import argparse
import glob
import sys
from pathlib import Path, PurePosixPath
from typing import Any

from . import __version__
from .api.build import reconstruct_file_from_events
from .api.document import (
    filter_events_by_document_with_rename_handling,
    get_recorded_documents,
    normalize_path_string,
    resolve_document,
    resolve_template_file,
)
from .api.load import get_editors, load_jsonl
from .api.output import write_batch_json_output
from .api.verify import (
    check_time_limit,
    combine_time_info,
    compare_submitted_file,
    detect_external_copypaste,
    detect_external_copypaste_batch,
    get_recorded_initial_state,
    template_diff,
    verify_template,
)
from .display import (
    display_editors,
    display_submitted_file_comparison,
    display_suspicious_events,
    display_template_diff,
    display_time_info,
    display_warnings,
    print_batch_header,
    print_batch_summary,
)
from .playback import playback_recording


def create_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Process and verify recorder JSONL files.",
    )
    parser.add_argument(
        "--version",
        action="version",
        version=f"cr_proc {__version__}",
    )
    parser.add_argument(
        "inputs",
        nargs="+",
        help=(
            "Recording file(s) or glob patterns. Legacy mode still accepts the last "
            "positional argument as a template file."
        ),
    )
    parser.add_argument(
        "--template",
        type=Path,
        default=None,
        help=(
            "Template file or template directory. If omitted, the processor looks "
            "for a matching template next to each recording."
        ),
    )
    parser.add_argument(
        "--template-dir",
        type=Path,
        default=None,
        help=argparse.SUPPRESS,
    )
    parser.add_argument(
        "-t",
        "--time-limit",
        type=int,
        default=None,
        help="Maximum allowed active editing time in minutes.",
    )
    parser.add_argument(
        "-d",
        "--document",
        type=str,
        default=None,
        help=(
            "Select which document inside the recording to process. Use this when "
            "a recording contains multiple documents or when auto-selection picks "
            "the wrong one. This matches the recorded document path or filename; "
            "it is not a separate local input file."
        ),
    )
    parser.add_argument(
        "-o",
        "--output-json",
        type=Path,
        default=None,
        help="Write structured JSON results to this file.",
    )
    parser.add_argument(
        "--write",
        type=Path,
        default=None,
        help=(
            "Where to write reconstructed code. Use a file for single-recording runs "
            "or a directory for batch runs."
        ),
    )
    parser.add_argument(
        "-f",
        "--output-file",
        type=Path,
        default=None,
        help=argparse.SUPPRESS,
    )
    parser.add_argument(
        "--output-dir",
        type=Path,
        default=None,
        help=argparse.SUPPRESS,
    )
    parser.add_argument(
        "--submitted",
        type=Path,
        default=None,
        help=(
            "Submitted file or directory to compare against. A directory is matched "
            "per recording; a file is compared directly."
        ),
    )
    parser.add_argument(
        "--submitted-file",
        type=Path,
        default=None,
        help=argparse.SUPPRESS,
    )
    parser.add_argument(
        "--submitted-dir",
        type=Path,
        default=None,
        help=argparse.SUPPRESS,
    )
    parser.add_argument(
        "-s",
        "--show-autocomplete-details",
        action="store_true",
        help="Show detailed lines for suspicious rapid paste clusters.",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Show full suspicious event content instead of truncated output.",
    )
    parser.add_argument(
        "-g",
        "--filter-function-generation",
        action="store_true",
        help="Exclude IDE-generated boilerplate function stubs from suspicious autocomplete detections.",
    )
    parser.add_argument(
        "--filter-file",
        action="append",
        default=[],
        metavar="FILE",
        help="Exclude recordings matching this path, filename, or base name.",
    )
    parser.add_argument(
        "-p",
        "--playback",
        action="store_true",
        help="Open a browser-based playback viewer for a single recording.",
    )
    parser.add_argument(
        "--playback-speed",
        type=float,
        default=1.0,
        help="Playback speed multiplier.",
    )
    parser.add_argument(
        "--playback-start-event",
        type=int,
        default=0,
        help="Applied-event index to start playback from.",
    )
    return parser


def expand_file_patterns(patterns: list[str]) -> list[Path]:
    """Expand glob patterns into existing recording files."""
    paths: list[Path] = []
    for pattern in patterns:
        matches = [Path(match) for match in glob.glob(pattern)]
        if matches:
            paths.extend(matches)
        else:
            paths.append(Path(pattern))

    existing_paths = [path for path in paths if path.exists()]
    missing_paths = [path for path in paths if not path.exists()]
    for path in missing_paths:
        print(f"Warning: File not found: {path}", file=sys.stderr)

    if not existing_paths:
        raise FileNotFoundError("No recording files found")

    return existing_paths


def _recording_filter_candidates(path_value: str | Path) -> set[str]:
    normalized = normalize_path_string(str(path_value)).casefold()
    path = PurePosixPath(normalized)
    candidates = {
        normalized,
        path.name,
        path.stem,
    }
    if path.name.endswith(".recording.jsonl.gz"):
        candidates.add(path.name[: -len(".recording.jsonl.gz")])
    return candidates


def _looks_like_recording_input(value: str) -> bool:
    normalized = normalize_path_string(value).casefold()
    if any(char in value for char in "*?["):
        return True
    return normalized.endswith((".recording.jsonl.gz", ".jsonl.gz", ".jsonl"))


def _choose_single_path_option(
    parser: argparse.ArgumentParser,
    option_label: str,
    *values: Path | None,
) -> Path | None:
    selected = [value for value in values if value is not None]
    if len(selected) > 1:
        parser.error(f"Use only one of {option_label}")
    return selected[0] if selected else None


def filter_recording_files(
    jsonl_files: list[Path],
    filter_values: list[str],
) -> tuple[list[Path], list[Path]]:
    """Exclude recording files matching any filter value."""
    if not filter_values:
        return jsonl_files, []

    normalized_filters = [
        _recording_filter_candidates(filter_value)
        for filter_value in filter_values
    ]

    included: list[Path] = []
    excluded: list[Path] = []
    for jsonl_file in jsonl_files:
        candidates = _recording_filter_candidates(jsonl_file)
        if any(candidates & filter_candidates for filter_candidates in normalized_filters):
            excluded.append(jsonl_file)
        else:
            included.append(jsonl_file)

    return included, excluded


def find_submitted_file(
    jsonl_file: Path,
    submitted_dir: Path,
    target_document: str | None,
) -> Path | None:
    extension = ".py"
    if target_document:
        extension = Path(target_document).suffix or ".py"

    submitted_name = jsonl_file.name.replace(".recording.jsonl.gz", extension)
    submitted_file = submitted_dir / submitted_name
    if submitted_file.exists():
        return submitted_file
    return None


def write_reconstructed_file(output_path: Path, content: str) -> None:
    output_path.parent.mkdir(parents=True, exist_ok=True)
    text = content if content.endswith("\n") else f"{content}\n"
    output_path.write_text(text)
    print(f"Written to: {output_path}", file=sys.stderr)


def _split_inputs(
    inputs: list[str],
    template_target: Path | None,
) -> tuple[list[str], Path | None]:
    if template_target is not None:
        return inputs, template_target

    if len(inputs) >= 2 and not _looks_like_recording_input(inputs[-1]):
        return inputs[:-1], Path(inputs[-1])

    return inputs, None


def _resolve_template_sources(
    jsonl_file: Path,
    template_target: Path | None,
) -> tuple[Path | None, Path | None]:
    effective_target = template_target or jsonl_file.parent
    if effective_target.is_dir():
        return None, effective_target
    return effective_target, None


def _resolve_submitted_sources(
    submitted_target: Path | None,
) -> tuple[Path | None, Path | None]:
    if submitted_target is None:
        return None, None
    if submitted_target.is_dir():
        return None, submitted_target
    return submitted_target, None


def _load_recording_context(
    jsonl_file: Path,
    args: argparse.Namespace,
    template_target: Path | None,
) -> tuple[tuple[dict[str, Any], ...], str | None, tuple[dict[str, Any], ...]]:
    json_data = load_jsonl(jsonl_file)
    recorded_docs = get_recorded_documents(json_data)
    effective_template_target = template_target or jsonl_file.parent
    target_document = resolve_document(
        recorded_docs,
        effective_template_target,
        args.document,
    )
    doc_events = filter_events_by_document_with_rename_handling(json_data, target_document)
    return json_data, target_document, doc_events


def _process_recording(
    jsonl_file: Path,
    args: argparse.Namespace,
    template_target: Path | None,
    submitted_target: Path | None,
    *,
    context: tuple[tuple[dict[str, Any], ...], str | None, tuple[dict[str, Any], ...]] | None = None,
    precomputed_suspicious_events: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
    warnings: list[str] = []
    if context is None:
        json_data, target_document, doc_events = _load_recording_context(
            jsonl_file,
            args,
            template_target,
        )
    else:
        json_data, target_document, doc_events = context

    template_path, template_dir = _resolve_template_sources(jsonl_file, template_target)

    template_text, resolved_template = resolve_template_file(
        template_path,
        template_dir,
        target_document,
    )
    if template_dir and resolved_template is None:
        warnings.append(
            f"Warning: No matching template found for {target_document} in {template_dir}. "
            "Reconstruction will use the recording snapshot."
        )

    if target_document and not doc_events:
        warnings.append(
            f"Warning: No events found for document '{target_document}' in {jsonl_file}"
        )

    editors = get_editors(doc_events or json_data)
    time_info = check_time_limit(doc_events, args.time_limit)
    verified = True
    diff_text = ""
    suspicious_events: list[dict[str, Any]] = []

    reconstruction_seed = template_text
    if doc_events:
        try:
            reconstruction_seed = verify_template(template_text, doc_events)
            suspicious_events = (
                precomputed_suspicious_events
                if precomputed_suspicious_events is not None
                else detect_external_copypaste(
                    doc_events,
                    filter_function_generation=args.filter_function_generation,
                )
            )
        except ValueError as exc:
            verified = False
            warnings.append(f"Warning: Verification failed for {jsonl_file}: {exc}")
            suspicious_events = (
                precomputed_suspicious_events
                if precomputed_suspicious_events is not None
                else detect_external_copypaste(
                    doc_events,
                    filter_function_generation=args.filter_function_generation,
                )
            )
            diff_text = template_diff(template_text, doc_events)
            recorded_initial = get_recorded_initial_state(doc_events)
            if recorded_initial is not None:
                reconstruction_seed = recorded_initial

    reconstructed = (
        reconstruct_file_from_events(
            doc_events,
            reconstruction_seed,
            document_path=target_document,
        )
        if doc_events
        else template_text
    )

    submitted_file, submitted_dir = _resolve_submitted_sources(submitted_target)
    if submitted_dir and submitted_file is None:
        submitted_file = find_submitted_file(jsonl_file, submitted_dir, target_document)

    submitted_comparison = None
    if submitted_file is not None:
        submitted_comparison = compare_submitted_file(reconstructed, submitted_file)

    return {
        "jsonl_file": jsonl_file,
        "target_document": target_document,
        "verified": verified,
        "reconstructed": reconstructed,
        "suspicious_events": suspicious_events,
        "time_info": time_info,
        "template_diff": diff_text,
        "submitted_comparison": submitted_comparison,
        "editors": sorted(editors),
        "warnings": warnings,
    }


def main(argv: list[str] | None = None) -> int:
    parser = create_parser()
    args = parser.parse_args(argv)

    template_target = _choose_single_path_option(
        parser,
        "--template and --template-dir",
        args.template,
        args.template_dir,
    )
    submitted_target = _choose_single_path_option(
        parser,
        "--submitted, --submitted-file, and --submitted-dir",
        args.submitted,
        args.submitted_file,
        args.submitted_dir,
    )
    write_target = _choose_single_path_option(
        parser,
        "--write, --output-file, and --output-dir",
        args.write,
        args.output_file,
        args.output_dir,
    )

    try:
        recording_patterns, template_target = _split_inputs(args.inputs, template_target)
        jsonl_files = expand_file_patterns(recording_patterns)
        jsonl_files, excluded_files = filter_recording_files(jsonl_files, args.filter_file)
        for excluded_file in excluded_files:
            print(f"Skipping filtered recording: {excluded_file}", file=sys.stderr)
        if not jsonl_files:
            print("No recording files remain after applying --filter-file", file=sys.stderr)
            return 1

        batch_mode = len(jsonl_files) > 1
        if args.playback and batch_mode:
            print("Playback mode only supports a single recording file.", file=sys.stderr)
            return 1

        if template_target is not None and not template_target.exists():
            raise FileNotFoundError(f"Template path not found: {template_target}")
        if submitted_target is not None and not submitted_target.exists():
            raise FileNotFoundError(f"Submitted path not found: {submitted_target}")
        if batch_mode and write_target is not None and write_target.exists() and write_target.is_file():
            parser.error("--write must be a directory when processing multiple recordings")

        if args.playback:
            json_data = load_jsonl(jsonl_files[0])
            recorded_docs = get_recorded_documents(json_data)
            effective_template_target = template_target or jsonl_files[0].parent
            target_document = resolve_document(
                recorded_docs,
                effective_template_target,
                args.document,
            )
            template_path, template_dir = _resolve_template_sources(jsonl_files[0], template_target)
            template_text, _ = resolve_template_file(
                template_path,
                template_dir,
                target_document,
            )
            playback_recording(
                json_data,
                target_document,
                template_text,
                speed=args.playback_speed,
                start_event=args.playback_start_event,
            )
            return 0

        recording_contexts: dict[
            Path,
            tuple[tuple[dict[str, Any], ...], str | None, tuple[dict[str, Any], ...]],
        ] = {}
        batch_suspicious_events: dict[Path, list[dict[str, Any]]] = {}
        if batch_mode:
            for jsonl_file in jsonl_files:
                recording_contexts[jsonl_file] = _load_recording_context(
                    jsonl_file,
                    args,
                    template_target,
                )
            batch_suspicious_events = detect_external_copypaste_batch(
                {
                    jsonl_file: context[2]
                    for jsonl_file, context in recording_contexts.items()
                },
                filter_function_generation=args.filter_function_generation,
            )

        results: list[dict[str, Any]] = []
        for index, jsonl_file in enumerate(jsonl_files, start=1):
            if batch_mode:
                print_batch_header(index, len(jsonl_files), jsonl_file.name)

            result = _process_recording(
                jsonl_file,
                args,
                template_target,
                submitted_target,
                context=recording_contexts.get(jsonl_file),
                precomputed_suspicious_events=batch_suspicious_events.get(jsonl_file),
            )
            results.append(result)

            display_warnings(result["warnings"])
            display_time_info(result["time_info"])
            display_editors(set(result["editors"]))
            display_suspicious_events(
                result["suspicious_events"],
                args.show_autocomplete_details,
                args.verbose,
            )
            display_template_diff(result["template_diff"])
            display_submitted_file_comparison(result["submitted_comparison"])

            if write_target is not None and not batch_mode:
                output_path = (
                    write_target / jsonl_file.name.replace(".recording.jsonl.gz", ".py")
                    if write_target.exists() and write_target.is_dir()
                    else write_target
                )
                write_reconstructed_file(output_path, result["reconstructed"])

            if write_target is not None and batch_mode:
                output_name = jsonl_file.name.replace(".recording.jsonl.gz", ".py")
                write_reconstructed_file(
                    write_target / output_name,
                    result["reconstructed"],
                )

        combined_time_info = combine_time_info(
            [result["time_info"] for result in results],
            args.time_limit,
        )
        all_verified = all(result["verified"] for result in results)

        submitted_comparisons = [
            result["submitted_comparison"]
            for result in results
            if result["submitted_comparison"] is not None
        ]
        submitted_all_passed: bool | None = None
        if submitted_comparisons:
            submitted_all_passed = all(
                comparison.get("matches") or comparison.get("whitespace_only")
                for comparison in submitted_comparisons
                if "error" not in comparison
            ) and all("error" not in comparison for comparison in submitted_comparisons)

        if batch_mode:
            print_batch_summary(
                len(results),
                sum(1 for result in results if result["verified"]),
                combined_time_info,
                submitted_all_passed=submitted_all_passed,
            )

        if args.output_json:
            write_batch_json_output(
                args.output_json,
                results,
                combined_time_info,
                all_verified,
                batch_mode=batch_mode,
                submitted_all_passed=submitted_all_passed,
                version=__version__,
            )

        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
