"""CLI command for comparing two datasets."""

from __future__ import annotations

import argparse
import sys
import termios
import tty
from collections.abc import Sequence
from pathlib import Path

import polars as pl

from ..api import Runtime
from ..compare import compare_paths, parse_name_list
from ..data.export import write_view_to_path
from ..logging import Recorder, RecorderConfig
from ..utils import _boot_trace, lazy_imports
from .progress import file_write_feedback


def compare_main(argv: Sequence[str], *, command_name: str = "compare") -> int:
    _boot_trace("cli:compare start")
    parser = _build_parser(command_name)
    args = parser.parse_args(argv)

    runtime = Runtime()
    try:
        keys = parse_name_list(args.keys)
        cols = parse_name_list(args.cols)
        exclude = parse_name_list(args.exclude)
        try:
            result = compare_paths(
                runtime,
                args.left_path,
                args.right_path,
                keys=keys,
                cols=cols,
                exclude=exclude,
                rtol=args.rtol,
                atol=args.atol,
            )
        except ValueError as exc:
            parser.error(str(exc))
        diff_lf = result.diff_lf
        summary = result.summary

        if args.out:
            exit_code = _write_diff_output(diff_lf, args.out)
            _print_summary(summary)
            return _final_exit_code(exit_code, summary)

        _print_summary(summary)
        if _should_open_tui():
            return _open_diff_tui(runtime, diff_lf, summary)
        return _final_exit_code(0, summary)
    finally:
        runtime.close()


def _build_parser(command_name: str) -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog=f"pulka {command_name}",
        description="Compare two datasets and open a diff view",
    )
    parser.add_argument("left_path", help="Left-hand dataset path")
    parser.add_argument("right_path", help="Right-hand dataset path")
    parser.add_argument(
        "--keys",
        help="Comma-separated key columns (default: row position)",
    )
    parser.add_argument(
        "--cols",
        help="Comma-separated columns to compare (default: all common columns)",
    )
    parser.add_argument(
        "--exclude",
        help="Comma-separated columns to exclude from comparison",
    )
    parser.add_argument(
        "--rtol",
        type=float,
        default=None,
        help="Relative tolerance for float comparisons",
    )
    parser.add_argument(
        "--atol",
        type=float,
        default=None,
        help="Absolute tolerance for float comparisons",
    )
    parser.add_argument(
        "--out",
        type=Path,
        help="Write diff output to PATH and exit",
    )
    return parser


def _write_diff_output(diff_lf: pl.LazyFrame, destination: Path) -> int:
    try:
        with file_write_feedback(destination, noun="diff file"):
            write_view_to_path(diff_lf, destination)
    except Exception as exc:
        print(f"Export failed: {exc}", file=sys.stderr)
        return 2
    print(f"Wrote {destination}")
    return 0


def _should_open_tui() -> bool:
    if not sys.stdin.isatty():
        return False
    return _prompt_yes_no("Open row-level diff sheet in Pulka? [y/n] ")


def _prompt_yes_no(prompt: str) -> bool:
    fd = sys.stdin.fileno()
    try:
        old_settings = termios.tcgetattr(fd)
    except Exception:
        try:
            response = input(prompt).strip().lower()
        except EOFError:
            return False
        return response in {"", "y", "yes"}

    sys.stdout.write("\n")
    sys.stdout.write(prompt)
    sys.stdout.flush()
    try:
        tty.setraw(fd)
        char = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

    sys.stdout.write("\n")
    sys.stdout.flush()
    if not char:
        return False
    return char.lower() in {"y", "\r", "\n"}


def _open_diff_tui(runtime: Runtime, diff_lf: pl.LazyFrame, summary: dict[str, object]) -> int:
    from ..tui.app import run_tui_app

    source_label = f"compare: {summary['left_path']} vs {summary['right_path']}"
    recorder = Recorder(RecorderConfig(enabled=False))
    session = runtime.open(
        None,
        recorder=recorder,
        lazyframe=diff_lf,
        source_label=source_label,
    )
    counts = summary["counts"]
    session.viewer.status_message = (
        f"diffs: changed={counts['changed']} left_only={counts['left_only']} "
        f"right_only={counts['right_only']}"
    )
    try:
        run_tui_app(session.viewer, recorder=session.recorder)
    finally:
        session.close()
    return _final_exit_code(0, summary)


def _final_exit_code(base: int, summary: dict[str, object]) -> int:
    if base:
        return base
    counts = summary["counts"]
    total = counts["changed"] + counts["left_only"] + counts["right_only"]
    return 0 if total == 0 else 1


def _print_summary(summary: dict[str, object]) -> None:
    left_path = summary["left_path"]
    right_path = summary["right_path"]
    compare_cols = summary["compare_cols"]
    left_extra = summary["left_extra"]
    right_extra = summary["right_extra"]
    counts = summary["counts"]
    total = summary["total"]
    top_changed = summary.get("top_changed") or []
    excluded_cols = summary.get("exclude") or []
    warnings = summary.get("warnings") or ()

    left_rows = summary["left_rows"]
    right_rows = summary["right_rows"]

    for warning in warnings:
        text = str(warning).strip()
        if text:
            print(f"warning: {text}", file=sys.stderr)

    overlap_rows = min(left_rows - counts["left_only"], right_rows - counts["right_only"])
    overlap_rows = max(overlap_rows, 0)
    unchanged_rows = int(counts.get("unchanged", 0))
    left_cols_total = len(compare_cols) + len(left_extra)
    right_cols_total = len(compare_cols) + len(right_extra)

    shape_rows: list[tuple[str, str, str, str]] = [
        ("Total rows", str(left_rows), str(right_rows), "All rows scanned in each dataset."),
        (
            "Matched rows",
            str(overlap_rows),
            str(overlap_rows),
            "Rows whose keys exist in both datasets.",
        ),
        (
            "Rows only in this side",
            str(counts["left_only"]),
            str(counts["right_only"]),
            "Rows whose keys exist only on that side.",
        ),
        (
            "Total columns",
            str(left_cols_total),
            str(right_cols_total),
            "Compared columns plus side-only columns.",
        ),
        (
            "Columns only in this side",
            str(len(left_extra)),
            str(len(right_extra)),
            "Columns that exist only on that side.",
        ),
    ]

    breakdown_rows: list[tuple[str, str, str, str]] = []
    unchanged_pct = unchanged_rows / overlap_rows * 100 if overlap_rows else 0.0
    unchanged_share = f"{unchanged_pct:.1f}% of matched"
    changed_def = "Rows present in both with any differing compared values."
    unchanged_def = "Rows present in both with all compared values equal."
    left_only_def = "Row key exists only in the left dataset."
    right_only_def = "Row key exists only in the right dataset."
    total_def = "Changed + only-in-left + only-in-right."
    if total:
        changed_pct = counts["changed"] / total * 100
        left_pct = counts["left_only"] / total * 100
        right_pct = counts["right_only"] / total * 100
        breakdown_rows = [
            ("Matched unchanged", str(unchanged_rows), unchanged_share, unchanged_def),
            (
                "Matched changed",
                str(counts["changed"]),
                f"{changed_pct:.1f}% of diff",
                changed_def,
            ),
            (
                "Only in left",
                str(counts["left_only"]),
                f"{left_pct:.1f}% of diff",
                left_only_def,
            ),
            (
                "Only in right",
                str(counts["right_only"]),
                f"{right_pct:.1f}% of diff",
                right_only_def,
            ),
            ("Total diff rows", str(total), "100.0% of diff", total_def),
        ]
    else:
        breakdown_rows = [
            ("Matched unchanged", str(unchanged_rows), unchanged_share, unchanged_def),
            ("Matched changed", "0", "0.0% of diff", changed_def),
            ("Only in left", "0", "0.0% of diff", left_only_def),
            ("Only in right", "0", "0.0% of diff", right_only_def),
            ("Total diff rows", "0", "0.0% of diff", total_def),
        ]

    if sys.stdout.isatty() and _print_rich_summary(
        left=str(left_path),
        right=str(right_path),
        shape_rows=shape_rows,
        breakdown_rows=breakdown_rows,
        top_changed=top_changed,
        excluded_cols=list(excluded_cols),
    ):
        return

    print("Compare summary")
    print(f"Left:  {left_path}")
    print(f"Right: {right_path}")
    if excluded_cols:
        print(f"Excluded columns: {', '.join(excluded_cols)}")
    print()
    print("Shape")
    for metric, left_val, right_val, definition in shape_rows:
        print(f"  {metric}: left={left_val} right={right_val} - {definition}")
    print()
    print("Diff breakdown")
    for label, count, share, definition in breakdown_rows:
        print(f"  {label}: {count} ({share}) - {definition}")
    if top_changed:
        print()
        print("Top changed columns")
        for name, count, pct in top_changed:
            print(f"  {name}: {count} ({pct:.1f}%)")


def _print_rich_summary(
    *,
    left: str,
    right: str,
    shape_rows: list[tuple[str, str, str, str]],
    breakdown_rows: list[tuple[str, str, str, str]],
    top_changed: list[tuple[str, int, float]],
    excluded_cols: list[str],
) -> bool:
    try:
        console_cls = lazy_imports.rich_console_class()
        table_cls = lazy_imports.rich_table_class()
    except Exception:
        return False

    console = console_cls()

    console.print("Compare summary", style="bold")
    console.print(f"Left:  {left}", style="dim")
    console.print(f"Right: {right}", style="dim")
    if excluded_cols:
        console.print(f"Excluded columns: {', '.join(excluded_cols)}", style="dim")

    shape_table = table_cls(show_lines=False, header_style="bold", border_style="dim")
    shape_table.add_column("Metric", style="bold")
    shape_table.add_column("Left")
    shape_table.add_column("Right")
    shape_table.add_column("Definition")
    for metric, left_val, right_val, definition in shape_rows:
        shape_table.add_row(metric, left_val, right_val, definition)

    console.print()
    console.print("Shape", style="dim")
    console.print(shape_table)

    breakdown_table = table_cls(show_lines=False, header_style="bold", border_style="dim")
    breakdown_table.add_column("Category", style="bold")
    breakdown_table.add_column("Rows", justify="right")
    breakdown_table.add_column("Share", justify="right")
    breakdown_table.add_column("Definition")
    for label, count, share, definition in breakdown_rows:
        breakdown_table.add_row(label, count, share, definition)

    console.print()
    console.print("Diff breakdown", style="dim")
    console.print(breakdown_table)

    if top_changed:
        console.print()
        console.print("Top changed columns", style="dim")
        top_table = table_cls(show_lines=False, header_style="bold", border_style="dim")
        top_table.add_column("Column", style="bold")
        top_table.add_column("Changed rows", justify="right")
        top_table.add_column("Rate", justify="right")
        for name, count, pct in top_changed:
            top_table.add_row(name, str(count), f"{pct:.1f}%")
        console.print(top_table)
    return True


__all__ = ["compare_main"]
