"""
worker.py — Géocodeur Geoapify pour band_locations.

Boucle infinie, traite geocode_status='queued' un par un avec SKIP LOCKED.
Stratégie :
  0. Fast-path dedup : si un autre band_location du MÊME (lieu, pays) est déjà
     'done', on copie son résultat sans appel API.
  1. Sinon, essayer les requêtes fallback du parser (précis → vague).
     Pour chaque requête : cache d'abord (geocode_cache), puis Geoapify.
  2. Un résultat n'est accepté (done) que s'il est FIABLE :
       confiance >= MIN_CONFIDENCE  ET  granularité non-grossière.
     Sinon on continue les fallbacks.
  3. Après MAX_GEO_TRIES sans résultat fiable → status='llm_needed'.
  4. Succès → band_locations + sync bands.lat/lon (origine).
"""

import os
import time
import random
import json
import requests
import psycopg2

from parser import build_fallback_queries

GEOAPIFY_API_KEY = os.environ.get("GEOAPIFY_API_KEY", "").strip()
GEOAPIFY_BASE    = "https://api.geoapify.com/v1/geocode/search"

MIN_DELAY      = float(os.environ.get("GEOCODER_MIN_DELAY", "0.22"))
JITTER         = float(os.environ.get("GEOCODER_JITTER", "0.10"))
MAX_PER_RUN    = int(os.environ.get("GEOCODE_MAX_PER_RUN", "100000"))
MAX_GEO_TRIES  = int(os.environ.get("GEOCODE_MAX_GEO_TRIES", "3"))
MIN_CONFIDENCE = float(os.environ.get("GEOCODE_MIN_CONFIDENCE", "0.7"))

# Granularités jugées trop grossières pour une carte de villes : on refuse (→ LLM).
COARSE_TYPES = {"country", "state", "county", "region", "province", "political"}

_last_call = 0.0


def _polite_sleep():
    global _last_call
    elapsed = time.monotonic() - _last_call
    wait    = max(0.0, MIN_DELAY - elapsed) + random.uniform(0.0, JITTER)
    if wait > 0:
        time.sleep(wait)
    _last_call = time.monotonic()


def is_reliable(confidence, granularity, is_country_only) -> bool:
    """Un géocodage est fiable s'il est assez confiant ET assez précis.

    On exige STRICTEMENT plus que le seuil : 0.7 et en-dessous → rejeté (LLM).
    """
    if is_country_only:
        return True  # résolu par centroïde à l'enqueue, granularité pays assumée
    if confidence is None or confidence <= MIN_CONFIDENCE:
        return False
    if granularity and granularity.lower() in COARSE_TYPES:
        return False
    return True


def geoapify_search(query: str) -> dict | None:
    if not GEOAPIFY_API_KEY:
        raise RuntimeError("GEOAPIFY_API_KEY not set")
    params = {"text": query, "apiKey": GEOAPIFY_API_KEY, "limit": 1}
    _polite_sleep()
    r = requests.get(GEOAPIFY_BASE, params=params, timeout=30)
    if r.status_code == 429:
        raise RuntimeError("geoapify_throttle:429")
    r.raise_for_status()
    data     = r.json()
    features = data.get("features") or []
    if not features:
        return None
    props  = features[0].get("properties", {})
    coords = features[0].get("geometry", {}).get("coordinates", [])
    if len(coords) < 2:
        return None
    rank        = props.get("rank") or {}
    confidence  = rank.get("confidence") if isinstance(rank, dict) else None
    granularity = props.get("result_type")
    return {
        "lat":         props.get("lat") or coords[1],
        "lon":         props.get("lon") or coords[0],
        "confidence":  confidence,
        "granularity": granularity,
        "raw":         data,
    }


def sync_bands_primary(cur, ma_id: int):
    """Met à jour bands.lat/lon avec le lieu d'ORIGINE géocodé (step_order le plus bas).

    Carte « metal from Europe » : on veut le point de formation du groupe, pas
    sa dernière localisation.
    """
    cur.execute(
        """
        SELECT lat, lon FROM band_locations
        WHERE ma_id = %s
          AND geocode_status IN ('done', 'country_only')
          AND lat IS NOT NULL AND lon IS NOT NULL
        ORDER BY step_order ASC, id ASC
        LIMIT 1
        """,
        (ma_id,),
    )
    row = cur.fetchone()
    if not row:
        return
    lat, lon = row
    cur.execute(
        """
        UPDATE bands
        SET lat=%s, lon=%s,
            geom=ST_SetSRID(ST_MakePoint(%s,%s),4326)::geography,
            geocoded_at=now(),
            geocode_provider='band_locations',
            geocode_error=NULL,
            geocode_error_at=NULL
        WHERE ma_id=%s
        """,
        (lat, lon, lon, lat, ma_id),
    )


def mark_done(cur, loc_id, ma_id, lat, lon, query, provider, confidence, granularity):
    cur.execute(
        """
        UPDATE band_locations
        SET lat=%s, lon=%s,
            geocode_status='done',
            geocode_query=%s,
            geocode_provider=%s,
            geocode_confidence=%s,
            geocode_granularity=%s,
            geocode_error=NULL,
            updated_at=now()
        WHERE id=%s
        """,
        (lat, lon, query, provider, confidence, granularity, loc_id),
    )
    sync_bands_primary(cur, ma_id)


def try_dedup(cur, loc_id, ma_id, location_raw, country) -> bool:
    """Fast-path : copier le résultat d'un band_location identique déjà 'done'."""
    cur.execute(
        """
        SELECT bl2.lat, bl2.lon, bl2.geocode_confidence, bl2.geocode_granularity,
               bl2.geocode_query
        FROM band_locations bl2
        JOIN bands b2 ON b2.ma_id = bl2.ma_id
        WHERE lower(bl2.location_raw) = lower(%s)
          AND COALESCE(b2.country,'') = COALESCE(%s,'')
          AND bl2.geocode_status = 'done'
          AND bl2.lat IS NOT NULL
        LIMIT 1
        """,
        (location_raw, country),
    )
    hit = cur.fetchone()
    if not hit:
        return False
    lat, lon, conf, gran, query = hit
    mark_done(cur, loc_id, ma_id, lat, lon, query, 'dedup', conf, gran)
    print(f"[worker] id={loc_id} DEDUP '{location_raw}' ({country})")
    return True


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

    print(f"[worker] démarré — seuil confiance={MIN_CONFIDENCE}, max_tries={MAX_GEO_TRIES}")

    processed = 0
    with conn.cursor() as cur:
        while processed < MAX_PER_RUN:
            cur.execute(
                """
                SELECT bl.id, bl.ma_id, bl.location_raw, bl.is_country_only,
                       bl.geocode_tries_geo, bl.geocode_query, b.country
                FROM band_locations bl
                JOIN bands b ON b.ma_id = bl.ma_id
                WHERE bl.geocode_status = 'queued'
                  AND bl.geocode_next_at <= now()
                ORDER BY bl.geocode_next_at ASC, bl.id ASC
                LIMIT 1
                FOR UPDATE OF bl SKIP LOCKED
                """
            )
            row = cur.fetchone()
            if not row:
                print("[worker] rien à traiter, attente 60s")
                time.sleep(60)
                continue

            loc_id, ma_id, location_raw, is_country_only, tries, llm_query, country = row

            cur.execute(
                "UPDATE band_locations SET geocode_status='processing', updated_at=now() WHERE id=%s",
                (loc_id,),
            )

            # 0. Fast-path dedup (aucun appel API)
            if try_dedup(cur, loc_id, ma_id, location_raw, country):
                processed += 1
                continue

            # Requêtes à essayer : requête LLM en premier si disponible
            fallbacks = build_fallback_queries(location_raw, country)
            if llm_query and llm_query not in fallbacks:
                queries = [llm_query] + fallbacks
            else:
                queries = fallbacks

            seen: set[str]    = set()
            unique: list[str] = []
            for q in queries:
                if q not in seen:
                    seen.add(q)
                    unique.append(q)

            success     = False
            rate_limit  = False
            used_query  = None
            lat = lon   = None
            confidence  = None
            granularity = None
            provider    = 'geoapify'
            last_err    = None

            for query in unique:
                # Cache hit ? (avec confiance/granularité recalculées)
                cur.execute(
                    "SELECT lat, lon, confidence, granularity FROM geocode_cache WHERE query=%s",
                    (query,),
                )
                cached = cur.fetchone()
                if cached and cached[0] is not None:
                    c_lat, c_lon, c_conf, c_gran = cached
                    if is_reliable(c_conf, c_gran, is_country_only):
                        lat, lon    = c_lat, c_lon
                        confidence  = c_conf
                        granularity = c_gran
                        used_query  = query
                        provider    = 'geoapify-cache'
                        success     = True
                        print(f"[worker] id={loc_id} cache HIT '{query}' conf={c_conf}")
                        break
                    # cache présent mais peu fiable → inutile de rappeler l'API pour cette requête
                    last_err = f"low_conf_cache:{c_conf}:'{query}'"
                    continue

                try:
                    res = geoapify_search(query)
                    if res:
                        r_lat  = float(res["lat"])
                        r_lon  = float(res["lon"])
                        r_conf = res.get("confidence")
                        r_gran = res.get("granularity")
                        # Toujours mettre en cache (même peu fiable) pour éviter de rappeler
                        cur.execute(
                            """
                            INSERT INTO geocode_cache
                              (query, provider, lat, lon, geom, raw, confidence, granularity, updated_at)
                            VALUES (%s,'geoapify',%s,%s,
                                    ST_SetSRID(ST_MakePoint(%s,%s),4326)::geography,
                                    %s, %s, %s, now())
                            ON CONFLICT (query) DO UPDATE
                              SET lat=EXCLUDED.lat, lon=EXCLUDED.lon,
                                  geom=EXCLUDED.geom, raw=EXCLUDED.raw,
                                  confidence=EXCLUDED.confidence,
                                  granularity=EXCLUDED.granularity,
                                  provider=EXCLUDED.provider, updated_at=now()
                            """,
                            (query, r_lat, r_lon, r_lon, r_lat,
                             json.dumps(res["raw"]), r_conf, r_gran),
                        )
                        if is_reliable(r_conf, r_gran, is_country_only):
                            lat, lon    = r_lat, r_lon
                            confidence  = r_conf
                            granularity = r_gran
                            used_query  = query
                            success     = True
                            print(f"[worker] id={loc_id} OK '{query}' conf={r_conf} gran={r_gran}")
                            break
                        last_err = f"low_conf:{r_conf}:{r_gran}:'{query}'"
                    else:
                        last_err = f"no_result:'{query}'"
                except RuntimeError as exc:
                    if "429" in str(exc):
                        print("[worker] rate limit Geoapify, attente 60s")
                        cur.execute(
                            """
                            UPDATE band_locations
                            SET geocode_status='queued',
                                geocode_next_at=now() + interval '60 seconds',
                                updated_at=now()
                            WHERE id=%s
                            """,
                            (loc_id,),
                        )
                        rate_limit = True
                        time.sleep(60)
                        break
                    last_err = str(exc)[:300]
                except Exception as exc:
                    last_err = str(exc)[:300]

            if rate_limit:
                continue

            if success:
                mark_done(cur, loc_id, ma_id, lat, lon, used_query,
                          provider, confidence, granularity)
            else:
                new_tries = tries + 1
                if new_tries >= MAX_GEO_TRIES:
                    cur.execute(
                        """
                        UPDATE band_locations
                        SET geocode_status='llm_needed',
                            geocode_tries_geo=%s,
                            geocode_error=%s,
                            updated_at=now()
                        WHERE id=%s
                        """,
                        (new_tries, last_err or "no_reliable_match", loc_id),
                    )
                    print(f"[worker] id={loc_id} → llm_needed après {new_tries} essais ({last_err})")
                else:
                    backoff = min(1440, 30 * (2 ** new_tries))
                    cur.execute(
                        """
                        UPDATE band_locations
                        SET geocode_status='queued',
                            geocode_tries_geo=%s,
                            geocode_error=%s,
                            geocode_next_at=now() + (%s || ' minutes')::interval,
                            updated_at=now()
                        WHERE id=%s
                        """,
                        (new_tries, last_err, backoff, loc_id),
                    )
                    print(f"[worker] id={loc_id} retry {new_tries}/{MAX_GEO_TRIES} in {backoff}m: {last_err}")

            processed += 1

    conn.close()
    print(f"[worker] terminé, processed={processed}")


if __name__ == "__main__":
    main()
