"""
dedup_check.py — Dubletten-Erkennung für den NotebookLM-Kurator.

Pflicht-Vorstufe vor jedem Scoring-Lauf.
Erkennt Duplikate über drei Stufen:
  1. Exakter Titel-Match
  2. Hohe Titel-Ähnlichkeit (Jaccard ≥ 0.7)
  3. Gleiche YouTube-Video-ID (aus url-Feld oder Fulltext)

Haupt-API:
    find_duplicates(sources) -> list[dict]
    suggest_keeper(pair, scores) -> dict
"""

import re
from urllib.parse import urlparse, parse_qs


def _tokenize(title: str) -> set[str]:
    """Zerlegt einen Titel in lowercase Wort-Tokens."""
    return set(re.findall(r"[a-z0-9äöüß]+", title.lower()))


def _jaccard(a: set[str], b: set[str]) -> float:
    """Jaccard-Ähnlichkeit zweier Token-Mengen."""
    if not a and not b:
        return 1.0
    if not a or not b:
        return 0.0
    return len(a & b) / len(a | b)


def _extract_youtube_id(url: str | None) -> str | None:
    """Extrahiert die YouTube-Video-ID aus einer URL. Gibt None zurück bei Nicht-YouTube."""
    if not url:
        return None
    try:
        parsed = urlparse(url)
    except Exception:
        return None

    host = parsed.netloc.lower()
    path = parsed.path

    yt_hosts = {"youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be", "www.youtu.be"}
    if host not in yt_hosts:
        return None

    if host == "youtu.be":
        segment = path.lstrip("/").split("/")[0].split("?")[0]
        return segment if segment else None

    if "/watch" in path:
        qs = parse_qs(parsed.query)
        ids = qs.get("v", [])
        return ids[0] if ids else None

    for prefix in ("/shorts/", "/embed/"):
        if path.startswith(prefix):
            segment = path[len(prefix):].split("/")[0].split("?")[0]
            return segment if segment else None

    return None


def find_duplicates(sources: list[dict]) -> list[dict]:
    """
    Findet Dubletten-Paare in einer Quellenliste.

    Args:
        sources: Liste von Dicts aus `source list --json` mit mindestens
                 "id", "title", optional "url".

    Returns:
        Liste von Dubletten-Paaren:
        [
            {
                "source_a": {"id": ..., "title": ...},
                "source_b": {"id": ..., "title": ...},
                "confidence": "exact_title" | "high_similarity" | "same_video_id",
                "jaccard": float | None,
                "video_id": str | None,
            },
            ...
        ]
        Sortiert: exact_title zuerst, dann same_video_id, dann high_similarity.
    """
    pairs: list[dict] = []
    seen_pair_ids: set[tuple[str, str]] = set()

    # Vorberechnung: Tokens und Video-IDs
    token_cache: dict[str, set[str]] = {}
    vid_cache: dict[str, str | None] = {}

    for s in sources:
        sid = s["id"]
        token_cache[sid] = _tokenize(s.get("title", ""))
        vid_cache[sid] = _extract_youtube_id(s.get("url"))

    n = len(sources)
    for i in range(n):
        for j in range(i + 1, n):
            a = sources[i]
            b = sources[j]
            aid, bid = a["id"], b["id"]

            pair_key = (min(aid, bid), max(aid, bid))
            if pair_key in seen_pair_ids:
                continue

            title_a = (a.get("title") or "").strip()
            title_b = (b.get("title") or "").strip()

            # Stufe 1: Exakter Titel-Match
            if title_a and title_b and title_a.lower() == title_b.lower():
                seen_pair_ids.add(pair_key)
                pairs.append({
                    "source_a": {"id": aid, "title": title_a, "url": a.get("url")},
                    "source_b": {"id": bid, "title": title_b, "url": b.get("url")},
                    "confidence": "exact_title",
                    "jaccard": 1.0,
                    "video_id": vid_cache.get(aid) or vid_cache.get(bid),
                })
                continue

            # Stufe 3: Gleiche YouTube-Video-ID (vor Jaccard, da sicherer)
            vid_a = vid_cache.get(aid)
            vid_b = vid_cache.get(bid)
            if vid_a and vid_b and vid_a == vid_b:
                seen_pair_ids.add(pair_key)
                pairs.append({
                    "source_a": {"id": aid, "title": title_a, "url": a.get("url")},
                    "source_b": {"id": bid, "title": title_b, "url": b.get("url")},
                    "confidence": "same_video_id",
                    "jaccard": _jaccard(token_cache[aid], token_cache[bid]),
                    "video_id": vid_a,
                })
                continue

            # Stufe 2: Hohe Jaccard-Ähnlichkeit
            sim = _jaccard(token_cache[aid], token_cache[bid])
            if sim >= 0.7:
                seen_pair_ids.add(pair_key)
                pairs.append({
                    "source_a": {"id": aid, "title": title_a, "url": a.get("url")},
                    "source_b": {"id": bid, "title": title_b, "url": b.get("url")},
                    "confidence": "high_similarity",
                    "jaccard": sim,
                    "video_id": vid_a or vid_b,
                })

    # Sortierung: exact_title > same_video_id > high_similarity
    confidence_order = {"exact_title": 0, "same_video_id": 1, "high_similarity": 2}
    pairs.sort(key=lambda p: (confidence_order.get(p["confidence"], 9), -(p["jaccard"] or 0)))

    return pairs


def suggest_keeper(pair: dict, scores: dict | None = None) -> dict:
    """
    Empfiehlt, welche Quelle eines Paares behalten werden soll.

    Args:
        pair: Ein Eintrag aus find_duplicates().
        scores: Score-Dokument (scores["scores"][source_id]["gesamtscore"]).
                None wenn keine Scores vorliegen.

    Returns:
        {
            "keep": {"id": ..., "title": ..., "reason": ...},
            "delete": {"id": ..., "title": ...},
        }
    """
    a = pair["source_a"]
    b = pair["source_b"]
    a_id, b_id = a["id"], b["id"]

    # Scores verfügbar?
    score_a = None
    score_b = None
    if scores and "scores" in scores:
        s = scores["scores"]
        if a_id in s and isinstance(s[a_id], dict):
            score_a = s[a_id].get("gesamtscore")
        if b_id in s and isinstance(s[b_id], dict):
            score_b = s[b_id].get("gesamtscore")

    # Entscheidungslogik
    if score_a is not None and score_b is not None:
        if score_a >= score_b:
            return {
                "keep": {"id": a_id, "title": a["title"], "reason": f"Höherer Score ({score_a:.3f} vs {score_b:.3f})"},
                "delete": {"id": b_id, "title": b["title"]},
            }
        else:
            return {
                "keep": {"id": b_id, "title": b["title"], "reason": f"Höherer Score ({score_b:.3f} vs {score_a:.3f})"},
                "delete": {"id": a_id, "title": a["title"]},
            }

    if score_a is not None and score_b is None:
        return {
            "keep": {"id": a_id, "title": a["title"], "reason": "Einzige gescorte Quelle"},
            "delete": {"id": b_id, "title": b["title"]},
        }
    if score_b is not None and score_a is None:
        return {
            "keep": {"id": b_id, "title": b["title"], "reason": "Einzige gescorte Quelle"},
            "delete": {"id": a_id, "title": a["title"]},
        }

    # Kein Score: ältere Quelle behalten (a kommt in source list vor b → a ist älter)
    return {
        "keep": {"id": a_id, "title": a["title"], "reason": "Ältere Quelle (kein Score vorliegend)"},
        "delete": {"id": b_id, "title": b["title"]},
    }
