"""Backfill inferred tags dla scen bez tagów.

Forward-fix (`normalize_scene._merge_inferred_tags`) taguje tylko nowe/zmienione
sceny — istniejące tube-only sceny bez tagów (16% tag coverage, ~868k grywalnych)
nie są re-resolvowane (skip przy niezmienionym raw_hash). Ten skrypt jednorazowo
przelatuje sceny bez `scene_tags`, infer_tag_slugs(title) → INSERT SceneTag.

Bezpieczeństwo:
  - Keyset pagination po `scenes.id` (stały RAM niezależnie od rozmiaru tabeli).
  - Batch commit co `--batch` scen.
  - `pg_insert(...).on_conflict_do_nothing` na (scene_id, tag_id) — idempotentne,
    można puścić wielokrotnie.
  - `--dry-run` liczy wpływ bez zapisu.
  - `source_id=NULL` (SceneTag.source_id nullable) — tag z inferencji nie ma źródła.

Użycie:
  python scripts/backfill_inferred_tags.py --dry-run                # cały katalog, bez zapisu
  python scripts/backfill_inferred_tags.py --only-playable --dry-run
  python scripts/backfill_inferred_tags.py --batch 1000             # realny zapis
  python scripts/backfill_inferred_tags.py --limit 500              # test na 500 scenach
"""
from __future__ import annotations

import argparse
import logging
import sys
import time

from sqlalchemy import select, text
from sqlalchemy.dialects.postgresql import insert as pg_insert

from app.db import session_scope
from app.models.scene import SceneTag
from app.models.tag import Tag
from app.normalize.tag_inference import infer_tag_slugs

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("backfill_tags")


def _load_tag_cache(session) -> dict[str, "uuid.UUID"]:  # type: ignore[name-defined]
    return {slug: tid for slug, tid in session.execute(select(Tag.slug, Tag.id)).all()}


def _fetch_batch(session, *, last_id, batch: int, only_playable: bool):
    """Keyset page: sceny bez scene_tags, z tytułem, id > last_id."""
    where_playable = (
        "AND EXISTS (SELECT 1 FROM playback_sources p WHERE p.scene_id = sc.id AND p.dead_at IS NULL)"
        if only_playable
        else ""
    )
    sql = text(
        f"""
        SELECT sc.id, sc.title
        FROM scenes sc
        WHERE sc.id > :last_id
          AND sc.title IS NOT NULL AND sc.title <> ''
          AND NOT EXISTS (SELECT 1 FROM scene_tags st WHERE st.scene_id = sc.id)
          {where_playable}
        ORDER BY sc.id
        LIMIT :batch
        """
    )
    return session.execute(sql, {"last_id": last_id, "batch": batch}).all()


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--dry-run", action="store_true", help="Licz wpływ, nie zapisuj")
    ap.add_argument("--only-playable", action="store_true", help="Tylko sceny z żywym playbackiem")
    ap.add_argument("--batch", type=int, default=1000, help="Scen na transakcję (default 1000)")
    ap.add_argument("--limit", type=int, default=None, help="Max scen do przetworzenia (test)")
    args = ap.parse_args()

    t0 = time.time()
    scanned = 0
    scenes_tagged = 0
    links_added = 0
    last_id = "00000000-0000-0000-0000-000000000000"

    with session_scope() as session:
        tag_cache = _load_tag_cache(session)
    log.info("tag cache: %d slugs; dry_run=%s only_playable=%s", len(tag_cache), args.dry_run, args.only_playable)

    while True:
        with session_scope() as session:
            rows = _fetch_batch(session, last_id=last_id, batch=args.batch, only_playable=args.only_playable)
            if not rows:
                break

            pending: list[dict] = []
            for scene_id, title in rows:
                last_id = str(scene_id)
                scanned += 1
                slugs = infer_tag_slugs(title)
                if not slugs:
                    continue
                got = 0
                for slug in slugs:
                    tag_id = tag_cache.get(slug)
                    if tag_id is None:
                        # Slug nie istnieje jako Tag (rzadkie — inference slugi są DB-aligned).
                        # Tworzymy, by nie zgubić sygnału; cache by uniknąć powtórek.
                        if args.dry_run:
                            continue
                        tag = Tag(name=slug.replace("-", " ").title(), slug=slug)
                        session.add(tag)
                        session.flush()
                        tag_id = tag.id
                        tag_cache[slug] = tag_id
                    pending.append({"scene_id": scene_id, "tag_id": tag_id, "source_id": None})
                    got += 1
                if got:
                    scenes_tagged += 1
                    links_added += got

            if pending and not args.dry_run:
                session.execute(
                    pg_insert(SceneTag.__table__).values(pending).on_conflict_do_nothing(
                        index_elements=["scene_id", "tag_id"]
                    )
                )

        if scanned % 20000 < args.batch:
            rate = scanned / max(time.time() - t0, 0.1)
            log.info("scanned=%d tagged=%d links=%d (%.0f scenes/s) last_id=%s",
                     scanned, scenes_tagged, links_added, rate, last_id)

        if args.limit and scanned >= args.limit:
            log.info("limit %d reached", args.limit)
            break

    log.info(
        "DONE %s: scanned=%d scenes_tagged=%d links_added=%d elapsed=%.1fs",
        "(dry-run)" if args.dry_run else "(written)",
        scanned, scenes_tagged, links_added, time.time() - t0,
    )
    return 0


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