#!/usr/bin/env python3
"""Compare fdupes and fifi duplicate-finder outputs.

Approach: parse each output into a list of groups, where each group is a
sorted list of paths. Sort the outer list by the first path of each
group, then merge-walk both lists side-by-side:

  - Same first path + same members          → identical group.
  - Same first path, different members      → group differs; show diff.
  - Different first paths                   → advance the smaller side;
                                              that group only exists on
                                              one side at this anchor.

Groups whose anchors don't line up land in the "only on side X" buckets
and need a closer look (e.g. fifi adding hardlink siblings that sort
ahead of fdupes' first entry will shift the anchor).
"""
from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

# Box Drawing (U+2500–U+257F) covers every tree-prefix shape fifi uses.
PREFIX_RE = re.compile(r"^[\s─-╿]*")

TRAILER_PREFIXES = ("_", "Executed in", "usr time", "sys time")


def strip_prefix(line: str) -> str:
    return PREFIX_RE.sub("", line, count=1)


def parse_groups(path: Path) -> list[list[str]]:
    groups: list[list[str]] = []
    current: list[str] = []

    for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
        stripped = strip_prefix(raw)
        if stripped.startswith(TRAILER_PREFIXES):
            continue
        if not stripped:
            if current:
                groups.append(sorted(current))
                current = []
            continue
        current.append(stripped)

    if current:
        groups.append(sorted(current))
    groups.sort(key=lambda g: g[0])
    return groups


def fmt_group(g: list[str], max_lines: int = 6) -> str:
    if len(g) <= max_lines:
        return "\n".join(f"      {p}" for p in g)
    head = "\n".join(f"      {p}" for p in g[:max_lines - 1])
    return f"{head}\n      ... ({len(g) - max_lines + 1} more)"


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(
        "-v",
        "--verbose",
        action="store_true",
        help="show example differing groups",
    )
    parser.add_argument(
        "--limit",
        type=int,
        default=5,
        help="number of examples to print per category (default: 5)",
    )
    args = parser.parse_args()

    a = parse_groups(args.fdupes_file)
    b = parse_groups(args.fifi_file)

    print(f"fdupes: {len(a):>6} groups, {sum(len(g) for g in a):>7} paths")
    print(f"fifi:   {len(b):>6} groups, {sum(len(g) for g in b):>7} paths")
    print()

    identical = 0
    differing: list[tuple[list[str], list[str]]] = []
    only_a: list[list[str]] = []
    only_b: list[list[str]] = []

    i = j = 0
    while i < len(a) and j < len(b):
        if a[i][0] == b[j][0]:
            if a[i] == b[j]:
                identical += 1
            else:
                differing.append((a[i], b[j]))
            i += 1
            j += 1
        elif a[i][0] < b[j][0]:
            only_a.append(a[i])
            i += 1
        else:
            only_b.append(b[j])
            j += 1
    only_a.extend(a[i:])
    only_b.extend(b[j:])

    print(f"Identical groups:                  {identical}")
    print(f"Same anchor, different members:    {len(differing)}")
    print(f"fdupes-only groups (no anchor in fifi):  {len(only_a)}")
    print(f"fifi-only groups (no anchor in fdupes):  {len(only_b)}")

    if differing:
        same_size = a_larger = b_larger = 0
        a_subset = b_subset = neither = 0
        for ga, gb in differing:
            sa, sb = set(ga), set(gb)
            if len(ga) == len(gb):
                same_size += 1
            elif len(gb) > len(ga):
                b_larger += 1
            else:
                a_larger += 1
            if sa < sb:
                a_subset += 1
            elif sb < sa:
                b_subset += 1
            elif sa != sb:
                neither += 1
        print()
        print("Of differing-anchor-shared groups:")
        print(f"  fdupes ⊊ fifi:                    {a_subset}")
        print(f"  fifi ⊊ fdupes:                    {b_subset}")
        print(f"  neither is subset of the other:   {neither}")
        print(f"  (same size = {same_size}, fifi larger = {b_larger}, fdupes larger = {a_larger})")

    if args.verbose:
        n = args.limit
        if differing:
            print()
            print(f"--- up to {n} differing groups (shared anchor) ---")
            for ga, gb in differing[:n]:
                print(f"  anchor: {ga[0]}")
                print(f"    fdupes ({len(ga)}):")
                print(fmt_group(ga))
                print(f"    fifi   ({len(gb)}):")
                print(fmt_group(gb))
        if only_a:
            print()
            print(f"--- up to {n} fdupes-only groups ---")
            for g in only_a[:n]:
                print(f"  [{len(g)}] {g[0]}")
        if only_b:
            print()
            print(f"--- up to {n} fifi-only groups ---")
            for g in only_b[:n]:
                print(f"  [{len(g)}] {g[0]}")

    return 0


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