#!/usr/bin/env python3
"""Investigate why fifi reports more duplicates than fdupes.

For each fdupes group that fifi reports as a strict superset, take an
"original" from the fdupes group and the "extras" that fifi adds, then
check whether each extra is:
  - a hardlink (same inode as the original),
  - byte-identical (genuine duplicate fdupes missed),
  - or different (likely fifi false positive).
"""
from __future__ import annotations

import argparse
import filecmp
import os
import random
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from compare import parse_groups  # noqa: E402


def categorize(extra: str, group_members: list[str]) -> str:
    """Classify an extra path against every other member of its group.

    A path is a "hardlink" if it shares an inode with any other group
    member; otherwise we compare bytes against the first comparable member.
    """
    try:
        st_e = os.stat(extra)
    except OSError as e:
        return f"stat-error: {e}"

    comparable = None
    for member in group_members:
        if member == extra:
            continue
        try:
            st_m = os.stat(member)
        except OSError:
            continue
        if (st_e.st_dev, st_e.st_ino) == (st_m.st_dev, st_m.st_ino):
            return "hardlink"
        if comparable is None and st_m.st_size == st_e.st_size:
            comparable = member

    if comparable is None:
        return "no-comparable-member"
    try:
        if filecmp.cmp(comparable, extra, shallow=False):
            return "byte-identical"
        return "DIFFERENT-BYTES"
    except OSError as e:
        return f"read-error: {e}"


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("fdupes_file", type=Path)
    parser.add_argument("fifi_file", type=Path)
    parser.add_argument(
        "-n",
        "--samples",
        type=int,
        default=50,
        help="number of extended fdupes groups to sample (default: 50)",
    )
    parser.add_argument(
        "--seed", type=int, default=42, help="RNG seed for sampling"
    )
    parser.add_argument(
        "--show-different",
        action="store_true",
        help="print details of any DIFFERENT-BYTES findings (potential false positives)",
    )
    args = parser.parse_args()

    fdupes = [frozenset(g) for g in parse_groups(args.fdupes_file)]
    fifi = [frozenset(g) for g in parse_groups(args.fifi_file)]
    fifi_index = {p: g for g in fifi for p in g}

    fdupes_set = set(fdupes)
    fifi_set = set(fifi)
    only_fdupes = fdupes_set - fifi_set

    # Find "extended" groups: all fdupes paths land in one fifi group F, F ⊋ G.
    extended_pairs: list[tuple[frozenset[str], frozenset[str]]] = []
    for g in only_fdupes:
        targets = {fifi_index.get(p) for p in g}
        if None in targets or len(targets) != 1:
            continue
        f = next(iter(targets))
        if f and len(f) > len(g):
            extended_pairs.append((g, f))

    print(f"Found {len(extended_pairs)} extended group pairs.")

    rng = random.Random(args.seed)
    sample = rng.sample(extended_pairs, min(args.samples, len(extended_pairs)))

    counts: dict[str, int] = {}
    different_examples: list[tuple[str, str]] = []
    pairs_checked = 0

    for g, f in sample:
        members = sorted(f)
        extras = sorted(f - g)
        for extra in extras:
            pairs_checked += 1
            verdict = categorize(extra, members)
            counts[verdict] = counts.get(verdict, 0) + 1
            if verdict == "DIFFERENT-BYTES":
                different_examples.append((sorted(g)[0], extra))

    print(f"Checked {pairs_checked} (original, extra) pairs from {len(sample)} groups.")
    print()
    print("Verdicts:")
    for k in sorted(counts, key=lambda x: -counts[x]):
        print(f"  {k:<20} {counts[k]:>6}")

    if args.show_different and different_examples:
        print()
        print("--- DIFFERENT-BYTES pairs ---")
        for o, e in different_examples[:20]:
            print(f"  original: {o}")
            print(f"  extra:    {e}")

    return 0


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