#!/usr/bin/env python3
"""Deduplicate a CSV by one or more named columns using only the stdlib."""

from __future__ import annotations

import argparse
import csv
import json
from pathlib import Path
from typing import Any, Iterable


def _key_for(
    row: dict[str | None, str | list[str] | None],
    columns: tuple[str, ...],
    *,
    ignore_case: bool,
    strip_key_whitespace: bool,
) -> tuple[str, ...]:
    values: list[str] = []
    for column in columns:
        value = row.get(column)
        if not isinstance(value, str):
            raise ValueError(f"row has no scalar value for key column: {column}")
        if strip_key_whitespace:
            value = value.strip()
        if ignore_case:
            value = value.casefold()
        values.append(value)
    return tuple(values)


def deduplicate_rows(
    rows: Iterable[dict[str | None, str | list[str] | None]],
    key_columns: Iterable[str],
    *,
    keep: str = "first",
    ignore_case: bool = False,
    strip_key_whitespace: bool = False,
) -> tuple[list[dict[str | None, str | list[str] | None]], int]:
    """Return retained rows plus the number of duplicates removed."""
    columns = tuple(key_columns)
    if not columns or any(not column for column in columns):
        raise ValueError("at least one non-empty key column is required")
    if len(set(columns)) != len(columns):
        raise ValueError("key columns must be unique")
    if keep not in {"first", "last"}:
        raise ValueError("keep must be 'first' or 'last'")

    materialized = list(rows)
    ordered = materialized if keep == "first" else list(reversed(materialized))
    seen: set[tuple[str, ...]] = set()
    retained: list[dict[str | None, str | list[str] | None]] = []
    for row in ordered:
        key = _key_for(
            row,
            columns,
            ignore_case=ignore_case,
            strip_key_whitespace=strip_key_whitespace,
        )
        if key in seen:
            continue
        seen.add(key)
        retained.append(row)
    if keep == "last":
        retained.reverse()
    return retained, len(materialized) - len(retained)


def deduplicate_csv(
    input_path: str | Path,
    output_path: str | Path,
    key_columns: Iterable[str],
    *,
    keep: str = "first",
    ignore_case: bool = False,
    strip_key_whitespace: bool = False,
) -> dict[str, Any]:
    """Read, validate, deduplicate, and write a CSV file."""
    source = Path(input_path).resolve()
    destination = Path(output_path).resolve()
    if source == destination:
        raise ValueError("input and output paths must be different")

    with source.open("r", encoding="utf-8-sig", newline="") as handle:
        reader = csv.DictReader(handle)
        fieldnames = reader.fieldnames
        if not fieldnames:
            raise ValueError("input CSV must have a header row")
        if len(set(fieldnames)) != len(fieldnames):
            raise ValueError("input CSV contains duplicate header names")
        columns = tuple(key_columns)
        missing = [column for column in columns if column not in fieldnames]
        if missing:
            raise ValueError(f"missing key column(s): {', '.join(missing)}")
        rows = list(reader)
    if any(None in row for row in rows):
        raise ValueError("input CSV contains a row with more values than headers")

    retained, duplicates_removed = deduplicate_rows(
        rows,
        columns,
        keep=keep,
        ignore_case=ignore_case,
        strip_key_whitespace=strip_key_whitespace,
    )
    destination.parent.mkdir(parents=True, exist_ok=True)
    with destination.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fieldnames, extrasaction="raise", lineterminator="\n")
        writer.writeheader()
        writer.writerows(retained)
    return {
        "input_rows": len(rows),
        "output_rows": len(retained),
        "duplicates_removed": duplicates_removed,
        "key_columns": list(columns),
        "keep": keep,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input_csv")
    parser.add_argument("output_csv")
    parser.add_argument("--key", action="append", required=True, dest="key_columns")
    parser.add_argument("--keep", choices=("first", "last"), default="first")
    parser.add_argument("--ignore-case", action="store_true")
    parser.add_argument("--strip-key-whitespace", action="store_true")
    args = parser.parse_args()
    try:
        summary = deduplicate_csv(
            args.input_csv,
            args.output_csv,
            args.key_columns,
            keep=args.keep,
            ignore_case=args.ignore_case,
            strip_key_whitespace=args.strip_key_whitespace,
        )
    except (OSError, csv.Error, ValueError) as exc:
        parser.exit(2, f"csv_dedupe: {exc}\n")
    print(json.dumps(summary, sort_keys=True))
    return 0


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