"""
enqueue.py — Daemon qui peuple band_locations depuis bands.location_text.

Deux déclencheurs :
  - manuel : job_triggers de type 'geocoder_enqueue'
    (POST /admin/api/job-triggers {job_type: "geocoder_enqueue"})
  - automatique : toutes les ENQUEUE_AUTO_INTERVAL_MIN minutes, en rattrapage.
    Le trigger DB trg_bands_geocode_dirty (migration 013) supprime les
    band_locations d'un band dès que son location_text change ; ce scan
    périodique idempotent (ON CONFLICT DO NOTHING) les retrouve et les
    reparse sans canal de notification supplémentaire.

Chaque exécution (manuelle ou auto) crée une ligne crawl_run pour apparaître
dans l'activité admin avec ses logs, en miroir de crawler/src/db.py.

Tourne en continu (restart: unless-stopped), poll toutes les 15s.
"""

import os
import time

import psycopg2
from parser import (
    COUNTRY_NAME_TO_ISO2,
    country_centroid,
    parse_location_text,
)

POLL_INTERVAL     = int(os.environ.get("GEOCODE_ENQUEUE_POLL", "15"))
BATCH             = int(os.environ.get("GEOCODE_ENQUEUE_BATCH", "100000"))
AUTO_INTERVAL_MIN = int(os.environ.get("ENQUEUE_AUTO_INTERVAL_MIN", "60"))


def resolve_iso2(location_raw: str, band_country: str | None) -> str | None:
    t = location_raw.strip()
    if len(t) == 2 and t.upper().isalpha():
        return t.upper()
    iso = COUNTRY_NAME_TO_ISO2.get(t.lower())
    if iso:
        return iso
    if band_country:
        return band_country.strip().upper()
    return None


def run_enqueue(cur) -> tuple[int, int, int, int]:
    """Parse tous les bands et peuple band_locations.
    Retourne (inserted, country_fixed, skipped, failed)."""
    cur.execute(
        "SELECT ma_id, country, location_text FROM bands "
        "WHERE location_text IS NOT NULL AND location_text <> '' "
        "ORDER BY ma_id ASC LIMIT %s",
        (BATCH,),
    )
    bands = cur.fetchall()
    print(f"[enqueue] {len(bands)} bands à parser")

    inserted = country_fixed = skipped = failed = 0
    err_samples: list[str] = []

    for ma_id, country, location_text in bands:
        steps = parse_location_text(location_text, country)
        if not steps:
            skipped += 1
            continue

        for step in steps:
            try:
                cur.execute(
                    """
                    INSERT INTO band_locations
                      (ma_id, step_order, step_label, location_raw,
                       is_country_only, geocode_status, geocode_next_at)
                    VALUES (%s, %s, %s, %s, %s, 'queued', now())
                    ON CONFLICT (ma_id, step_order, location_raw) DO NOTHING
                    """,
                    (ma_id, step['step_order'], step['step_label'],
                     step['location_raw'], step['is_country_only']),
                )
                if cur.rowcount > 0:
                    inserted += 1
            except Exception as exc:
                failed += 1
                if len(err_samples) < 5:
                    err_samples.append(f"ma_id={ma_id}: {exc}")
                print(f"[enqueue] WARN insert ma_id={ma_id}: {exc}")

    if failed:
        print(f"[enqueue] {failed} inserts échoués — échantillon: {err_samples}")

    # Résoudre immédiatement les entrées pays-seulement
    cur.execute(
        """
        SELECT bl.id, bl.location_raw, b.country
        FROM band_locations bl
        JOIN bands b ON b.ma_id = bl.ma_id
        WHERE bl.geocode_status = 'queued'
          AND bl.is_country_only = TRUE
        """
    )
    for loc_id, location_raw, band_country in cur.fetchall():
        iso2   = resolve_iso2(location_raw, band_country)
        coords = country_centroid(iso2) if iso2 else None
        if coords:
            lat, lon = coords
            cur.execute(
                """
                UPDATE band_locations
                SET lat=%s, lon=%s,
                    geocode_status='country_only',
                    geocode_provider='country_centroid',
                    geocode_confidence=0.1,
                    geocode_query=%s,
                    updated_at=now()
                WHERE id=%s
                """,
                (lat, lon, iso2, loc_id),
            )
            country_fixed += 1
        else:
            print(f"[enqueue] no centroid id={loc_id} raw='{location_raw}'")

    return inserted, country_fixed, skipped, failed


# ------------------------------------------------------------------
# crawl_run wrapping — même table/forme que apps/crawler/src/db.py, pour que
# chaque exécution de l'enqueue apparaisse dans l'activité admin avec ses logs.
# ------------------------------------------------------------------

def _start_crawl_run(cur) -> int:
    cur.execute(
        "INSERT INTO crawl_run (run_type) VALUES ('geocoder_enqueue') RETURNING id"
    )
    return cur.fetchone()[0]


def _finish_crawl_run(cur, run_id: int, stats: dict, error: str | None):
    status = "error" if error else "done"
    cur.execute(
        """
        UPDATE crawl_run SET
          status=%s, finished_at=now(),
          bands_seen=%s, bands_new=%s, bands_updated=%s, error=%s
        WHERE id=%s
        """,
        (status, stats.get("seen", 0), stats.get("new", 0),
         stats.get("skipped", 0), error, run_id),
    )


def _log(cur, run_id: int, level: str, message: str):
    cur.execute(
        "INSERT INTO crawl_log (run_id, level, message) VALUES (%s, %s, %s)",
        (run_id, level, message[:2000]),
    )


def _execute_run(cur, trigger_label: str, job_id: int | None = None) -> None:
    """Exécute run_enqueue() une fois, entourée d'un crawl_run + logs.
    trigger_label : 'manuel' ou 'auto' (juste pour le message de log)."""
    run_id = _start_crawl_run(cur)
    _log(cur, run_id, "info", f"[geocoder_enqueue] démarré ({trigger_label})")
    print(f"[enqueue] run #{run_id} démarré ({trigger_label})")

    error = None
    try:
        inserted, country_fixed, skipped, failed = run_enqueue(cur)
        summary = (
            f"[geocoder_enqueue] terminé ({trigger_label}) — "
            f"inserted={inserted} country_fixed={country_fixed} "
            f"skipped={skipped} failed={failed}"
        )
        _log(cur, run_id, "info", summary)
        print(f"[enqueue] {summary}")
        _finish_crawl_run(cur, run_id, {
            "seen": inserted + country_fixed, "new": inserted, "skipped": skipped,
        }, None)
    except Exception as exc:
        error = str(exc)[:400]
        _log(cur, run_id, "error", f"[geocoder_enqueue] erreur ({trigger_label}): {error}")
        print(f"[enqueue] run #{run_id} erreur: {error}")
        _finish_crawl_run(cur, run_id, {}, error)

    if job_id is not None:
        if error:
            cur.execute(
                "UPDATE job_triggers SET status='error', finished_at=now(), error=%s WHERE id=%s",
                (error, job_id),
            )
        else:
            cur.execute(
                "UPDATE job_triggers SET status='done', finished_at=now() WHERE id=%s",
                (job_id,),
            )


def main():
    dsn  = os.environ["DATABASE_URL"]
    conn = psycopg2.connect(dsn)
    conn.autocommit = True

    print(f"[enqueue] daemon démarré, poll {POLL_INTERVAL}s, auto toutes les {AUTO_INTERVAL_MIN}min")

    last_auto = time.monotonic()

    with conn.cursor() as cur:
        while True:
            # Rattrapage automatique (bands rendus 'dirty' par le trigger DB)
            if time.monotonic() - last_auto >= AUTO_INTERVAL_MIN * 60:
                _execute_run(cur, "auto")
                last_auto = time.monotonic()

            # Déclenchement manuel via l'admin
            cur.execute(
                """
                SELECT id FROM job_triggers
                WHERE job_type = 'geocoder_enqueue' AND status = 'pending'
                ORDER BY created_at ASC
                LIMIT 1
                FOR UPDATE SKIP LOCKED
                """
            )
            row = cur.fetchone()

            if not row:
                time.sleep(POLL_INTERVAL)
                continue

            job_id = row[0]
            cur.execute(
                "UPDATE job_triggers SET status='running', started_at=now() WHERE id=%s",
                (job_id,),
            )
            _execute_run(cur, "manuel", job_id=job_id)

    conn.close()


if __name__ == "__main__":
    main()
