"""False-merge remediacja dla scen z DOKŁADNIE 2 źródłami (n=2), rozstrzygana przez
canonical duration.

Kontekst: audit_false_merges remediuje tylko n>=3 (większość źródeł = ground truth dla
outliera). Dla n=2 było "needs human review" — ale review-queue we froncie już nie ma.
Pomiar (2026-06-07): z 535 scen n=2-divergent (2 źródła, długości różnią się >3x),
WSZYSTKIE 535 mają canonical scene.duration_sec (z TPDB/StashDB), a 531 ma JEDNO źródło
pasujące do canonical (±20%) i drugie >2x odbiegające → jednoznaczny false-merge.

Logika: canonical (TPDB/StashDB) = ground-truth tożsamości sceny. Źródło pasujące do
canonical (±KEEP_TOL) = keeper; drugie, >OUTLIER_DEV× odbiegające w którąkolwiek stronę
= wrong video (teaser ALBO compilation) → dead. Działa w obie strony (w przeciwieństwie
do audit_false_merges które kasuje tylko krótsze), bo canonical jest tu SKORELOWANY z
keeperem (≠ Omar-case gdzie canonical nie pasował do niczego). Skip gdy oba pasują
(nie divergentne) lub żadne (canonical niewiarygodny — ambiguous).

Reversible (dead_at). Domyślnie dry-run; --yes zapisuje.

  python -m scripts.dedup_n2_canonical            # dry-run
  python -m scripts.dedup_n2_canonical --list 30
  python -m scripts.dedup_n2_canonical --yes
"""
from __future__ import annotations

import argparse

from sqlalchemy import text

from app.db import session_scope

MIN_DUR = 30
GAP = 90
RATIO = 3.0
KEEP_TOL = 0.20     # źródło w ±20% canonical = keeper (skorelowane → canonical wiarygodny)
OUTLIER_DEV = 2.0   # drugie źródło >2x od canonical (w którąkolwiek stronę) = wrong video

_CANDIDATES_SQL = """
WITH d AS (
    SELECT scene_id, count(*) n, min(duration_sec) mn, max(duration_sec) mx
    FROM playback_sources
    WHERE dead_at IS NULL AND duration_sec IS NOT NULL AND duration_sec > :min_dur
    GROUP BY scene_id
    HAVING count(*) = 2
       AND (max(duration_sec) - min(duration_sec)) > :gap
       AND max(duration_sec)::float / min(duration_sec) >= :ratio
)
SELECT d.scene_id, sc.duration_sec AS canonical
FROM d JOIN scenes sc ON sc.id = d.scene_id
WHERE sc.duration_sec IS NOT NULL AND sc.duration_sec > :min_dur
"""


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--list", type=int, default=0)
    ap.add_argument("--yes", action="store_true")
    args = ap.parse_args()
    params = {"min_dur": MIN_DUR, "gap": GAP, "ratio": RATIO}

    with session_scope() as s:
        cands = list(s.execute(text(_CANDIDATES_SQL), params))

    print(f"n=2 divergent scenes with canonical: {len(cands)}")
    killed = touched = skipped_ambiguous = 0
    listed = 0
    for scene_id, canonical in cands:
        with session_scope() as s:
            srcs = list(s.execute(text("""
                SELECT id, duration_sec, origin FROM playback_sources
                WHERE scene_id = :sid AND dead_at IS NULL
                  AND duration_sec IS NOT NULL AND duration_sec > 0
                ORDER BY duration_sec
            """), {"sid": scene_id}))
            if len(srcs) != 2:
                continue
            keepers = [r for r in srcs if abs(r[1] - canonical) <= canonical * KEEP_TOL]
            def _is_outlier(dur: int) -> bool:
                return canonical / dur > OUTLIER_DEV or dur / canonical > OUTLIER_DEV
            outliers = [r for r in srcs if _is_outlier(r[1]) and r not in keepers]
            # Bezpiecznie: dokładnie 1 keeper (skorelowany z canonical) + 1 outlier.
            if len(keepers) != 1 or len(outliers) != 1:
                skipped_ambiguous += 1
                continue
            keep, kill = keepers[0], outliers[0]
            touched += 1
            if listed < args.list:
                listed += 1
                print(f"  {str(scene_id)[:8]} canon={canonical}s keep={keep[2]}={keep[1]}s "
                      f"kill={kill[2]}={kill[1]}s")
            if args.yes:
                s.execute(text("""
                    UPDATE playback_sources
                    SET dead_at = now(),
                        dead_reason = 'false-merge n=2 audit: duration off canonical (other source matches)'
                    WHERE id = :id
                """), {"id": kill[0]})
                s.commit()
                killed += 1

    verb = f"killed={killed}" if args.yes else f"would_kill={touched}"
    print(f"\n{'APPLIED' if args.yes else 'DRY-RUN'}: {verb} skipped_ambiguous={skipped_ambiguous}")
    if not args.yes:
        print("(dry-run — uruchom z --yes; reversible: dead_at)")


if __name__ == "__main__":
    main()
