-- 014_run_cancellation_and_geo_sync.sql
--
-- Deux corrections d'invariants qui rendaient des fonctionnalités cosmétiques.
--
-- A) Annulation réelle des crawl_run
--    Avant : « Annuler » écrivait status='error' en base. Le crawler Python ne
--    lisait jamais cette colonne et continuait sa boucle jusqu'au bout. Pire,
--    son UPDATE final `WHERE id=%s AND status='running'` ne matchait plus, donc
--    un run mené à terme restait affiché en erreur. Le bouton ne faisait rien
--    d'autre que mentir.
--    Après : l'admin pose un DRAPEAU (cancel_requested) sans toucher au statut.
--    Le crawler le lit à chaque itération et s'arrête proprement, en écrivant
--    lui-même status='cancelled'. Le statut reflète donc toujours la réalité du
--    process.
--
-- B) Désynchronisation du point principal
--    bands.lat/lon/geom est une dénormalisation du PREMIER lieu géocodé de
--    band_locations (voir _sync_band_point dans geocoder/worker.py). Le trigger
--    de la migration 013 supprime les band_locations quand location_text change,
--    mais laissait l'ancien point sur bands : la carte (qui lit band_locations)
--    n'affichait plus rien tandis que la liste et la heatmap (qui lisent bands)
--    continuaient de montrer l'ancienne ville, parfois pendant des jours.

-- ------------------------------------------------------------------
-- A) Annulation coopérative
-- ------------------------------------------------------------------

ALTER TABLE crawl_run ADD COLUMN IF NOT EXISTS cancel_requested    BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE crawl_run ADD COLUMN IF NOT EXISTS cancel_requested_at TIMESTAMPTZ;
ALTER TABLE crawl_run ADD COLUMN IF NOT EXISTS cancel_requested_by TEXT;

-- Le crawler interroge ce drapeau en boucle : index partiel pour que le
-- lookup reste un index-only scan même avec un historique de runs volumineux.
CREATE INDEX IF NOT EXISTS idx_crawl_run_cancel
  ON crawl_run (id)
  WHERE status = 'running' AND cancel_requested = TRUE;

-- Note : on n'ajoute délibérément PAS de cancel_requested sur job_triggers.
-- Un job 'pending' se retire de la file (c'est ce que fait la route existante) ;
-- une fois réclamé par le crawler il n'existe plus qu'à travers le crawl_run
-- qu'il a créé, et c'est CE run qu'on annule. Deux drapeaux pour un seul état
-- réel, c'est exactement le genre de colonne qui finit morte.

-- ------------------------------------------------------------------
-- B) Le point principal suit la suppression des localisations
-- ------------------------------------------------------------------

CREATE OR REPLACE FUNCTION bands_geocode_dirty() RETURNS trigger AS $$
BEGIN
  IF TG_OP = 'UPDATE' AND OLD.location_text IS DISTINCT FROM NEW.location_text THEN
    DELETE FROM band_locations WHERE ma_id = NEW.ma_id;

    -- Purge aussi le point dénormalisé : il décrivait l'ancienne localisation.
    -- Cet UPDATE ne touche pas location_text, donc il ne redéclenche pas ce
    -- trigger (défini AFTER UPDATE OF location_text) — pas de récursion.
    UPDATE bands
    SET lat = NULL,
        lon = NULL,
        geocoded_at = NULL,
        geocode_provider = NULL,
        geocode_query = NULL
    WHERE ma_id = NEW.ma_id
      AND (lat IS NOT NULL OR lon IS NOT NULL);
    -- geom est recalculé (à NULL) par le trigger BEFORE bands_set_geom.
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- ------------------------------------------------------------------
-- C) Localisation posée manuellement par un admin
-- ------------------------------------------------------------------
-- Éditer lat/lon depuis le dashboard n'écrivait que sur bands : la carte, qui
-- clusterise depuis band_locations, ignorait totalement la correction. On
-- réserve step_order = -1 à l'override manuel, qui passe donc avant tous les
-- steps issus du parsing (tri step_order ASC) et devient le point principal.
COMMENT ON COLUMN band_locations.step_order IS
  'Ordre de l''étape dans location_text (0 = première). -1 = override manuel admin, prioritaire.';
