import { requireAdminSession, writeAuditLog } from "./adminAuth.js";

const BAND_SORT_COLUMNS = new Set([
  "ma_id", "name", "country", "status", "genre",
  "formed_year", "enriched", "crawled_at", "updated_at", "first_seen_at",
]);

const BAND_EDITABLE_FIELDS = [
  "name", "country", "status", "genre", "formed_year",
  "themes", "location_text", "lat", "lon",
];

function pagination(query) {
  const page = Math.max(1, Number(query.page) || 1);
  const pageSize = Math.min(200, Math.max(1, Number(query.pageSize) || 50));
  return { page, pageSize, offset: (page - 1) * pageSize };
}

export default async function adminRoutes(fastify, opts) {
  const { pool } = opts;

  fastify.addHook("preHandler", async (req, reply) => {
    const username = requireAdminSession(req, reply);
    if (!username) return reply; // already sent 401
    req.adminUsername = username;
  });

  // ------------------------------------------------------------------
  // Stats globales
  // ------------------------------------------------------------------
  fastify.get("/admin/api/stats", async (req, reply) => {
    try {
      const [totals, byStatus, byCountry, byGenre] = await Promise.all([
        pool.query(`
          SELECT
            count(*)::int AS total,
            count(*) FILTER (WHERE enriched = true)::int AS enriched,
            count(*) FILTER (WHERE enriched = false)::int AS not_enriched,
            count(*) FILTER (WHERE geom IS NOT NULL)::int AS geocoded,
            count(*) FILTER (WHERE geom IS NULL)::int AS not_geocoded,
            count(*) FILTER (WHERE crawled_at IS NOT NULL)::int AS crawled_by_current_system,
            count(*) FILTER (WHERE data->'band_page' IS NULL)::int AS never_enriched,
            count(*) FILTER (WHERE crawled_at < now() - interval '30 days')::int AS stale
          FROM bands
        `),
        pool.query(`
          SELECT COALESCE(NULLIF(trim(status), ''), 'Unknown') AS status, count(*)::int AS total
          FROM bands GROUP BY 1 ORDER BY total DESC
        `),
        pool.query(`
          SELECT COALESCE(country, '??') AS country, count(*)::int AS total
          FROM bands GROUP BY 1 ORDER BY total DESC
        `),
        pool.query(`
          SELECT genre, count(*)::int AS total
          FROM bands WHERE genre IS NOT NULL AND genre != ''
          GROUP BY 1 ORDER BY total DESC
        `),
      ]);
      return {
        ok: true,
        totals: totals.rows[0],
        by_status: byStatus.rows,
        by_country: byCountry.rows,
        by_genre: byGenre.rows,
      };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur stats" });
    }
  });

  // ------------------------------------------------------------------
  // Queue d'enrichissement (même logique que le crawler Python)
  // ------------------------------------------------------------------
  fastify.get("/admin/api/queue", async (req, reply) => {
    try {
      const r = await pool.query(`
        SELECT
          count(*) FILTER (WHERE data->'band_page' IS NULL)::int AS new_bands,
          count(*) FILTER (
            WHERE crawled_at IS NOT NULL AND updated_at > crawled_at + interval '1 minute'
          )::int AS modified_since_enrich,
          count(*) FILTER (
            WHERE crawled_at IS NULL AND data->'band_page' IS NOT NULL
          )::int AS legacy_pending,
          count(*) FILTER (
            WHERE crawled_at IS NOT NULL AND crawled_at < now() - interval '30 days'
          )::int AS stale
        FROM bands
        WHERE data->>'url' IS NOT NULL
      `);
      const recentRuns = await pool.query(`
        SELECT id, run_type, status, started_at, finished_at,
               bands_seen, bands_new, bands_updated, bands_enriched, error
        FROM crawl_run
        WHERE run_type = 'enrich'
        ORDER BY started_at DESC
        LIMIT 5
      `);
      return { ok: true, breakdown: r.rows[0], recent_enrich_runs: recentRuns.rows };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur queue" });
    }
  });

  // ------------------------------------------------------------------
  // Bands - liste paginée / recherche / filtres
  // ------------------------------------------------------------------
  fastify.get("/admin/api/bands", async (req, reply) => {
    try {
      const { q, location_q, country, status, genre, themes_q, enriched, has_lat, has_location, sort, dir } = req.query || {};
      const { page, pageSize, offset } = pagination(req.query || {});

      const where = [];
      const vals = [];
      let i = 1;

      if (q) {
        const query = String(q).trim().slice(0, 100);
        if (query.length >= 2) {
          where.push(`(name ILIKE $${i} OR genre ILIKE $${i})`);
          vals.push(`%${query}%`);
          i++;
        }
      }
      if (location_q) {
        const lq = String(location_q).trim().slice(0, 100);
        if (lq.length >= 1) {
          where.push(`location_text ILIKE $${i}`);
          vals.push(`%${lq}%`);
          i++;
        }
      }
      if (country) {
        where.push(`country = $${i}`);
        vals.push(String(country).toUpperCase());
        i++;
      }
      if (status) {
        where.push(`COALESCE(NULLIF(trim(status), ''), 'Unknown') = $${i}`);
        vals.push(String(status));
        i++;
      }
      if (genre) {
        where.push(`genre ILIKE $${i}`);
        vals.push(`%${String(genre)}%`);
        i++;
      }
      if (enriched === "true" || enriched === "1") where.push(`enriched = true`);
      if (enriched === "false" || enriched === "0") where.push(`enriched = false`);
      if (has_lat === "true" || has_lat === "1") where.push(`lat IS NOT NULL`);
      if (has_lat === "false" || has_lat === "0") where.push(`lat IS NULL`);
      if (has_location === "true" || has_location === "1") where.push(`location_text IS NOT NULL AND location_text != ''`);
      if (has_location === "false" || has_location === "0") where.push(`(location_text IS NULL OR location_text = '')`);
      if (themes_q) {
        const tq = String(themes_q).trim().slice(0, 100);
        if (tq.length >= 1) { where.push(`themes ILIKE $${i}`); vals.push(`%${tq}%`); i++; }
      }

      const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
      const sortCol = BAND_SORT_COLUMNS.has(sort) ? sort : "ma_id";
      const sortDir = dir === "desc" ? "DESC" : "ASC";

      const countSql = `SELECT count(*)::int AS total FROM bands ${whereSql}`;
      const dataSql = `
        SELECT ma_id, name, country, status, genre, location_text, themes, formed_year,
               enriched, lat, lon, crawled_at, updated_at, first_seen_at, locked_fields
        FROM bands
        ${whereSql}
        ORDER BY ${sortCol} ${sortDir} NULLS LAST
        LIMIT $${i} OFFSET $${i + 1}
      `;
      vals.push(pageSize, offset);

      const [countRes, dataRes] = await Promise.all([
        pool.query(countSql, vals.slice(0, i - 1)),
        pool.query(dataSql, vals),
      ]);

      return {
        ok: true,
        items: dataRes.rows,
        total: countRes.rows[0].total,
        page,
        pageSize,
      };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur recherche bands" });
    }
  });

  fastify.get("/admin/api/bands/:ma_id", async (req, reply) => {
    try {
      const id = Number(req.params.ma_id);
      if (!Number.isFinite(id) || id < 0) {
        return reply.code(400).send({ ok: false, error: "bad ma_id" });
      }
      const [r, locs, llm] = await Promise.all([
        pool.query(`SELECT * FROM bands WHERE ma_id = $1 LIMIT 1`, [id]),
        pool.query(`
          SELECT id, step_order, step_label, location_raw, is_country_only,
                 lat, lon, geocode_status, geocode_query, geocode_provider,
                 geocode_confidence, geocode_error, geocode_tries_geo,
                 geocode_tries_llm, geocode_next_at, updated_at
          FROM band_locations
          WHERE ma_id = $1
          ORDER BY step_order ASC, id ASC
        `, [id]).catch(() => ({ rows: [] })),
        pool.query(`
          SELECT id, model, location_raw, country, parsed_city, parsed_country,
                 is_null, tokens_in, tokens_out, cost_usd, prompt, response, created_at
          FROM llm_cache
          WHERE ma_id = $1
             OR lower(location_raw) IN (
                  SELECT lower(location_raw) FROM band_locations WHERE ma_id = $1
                )
          ORDER BY created_at DESC
          LIMIT 50
        `, [id]).catch(() => ({ rows: [] })),
      ]);
      if (!r.rows.length) return reply.code(404).send({ ok: false, error: "not found" });
      return { ok: true, item: r.rows[0], locations: locs.rows, llm: llm.rows };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur récupération band" });
    }
  });

  fastify.patch("/admin/api/bands/:ma_id", async (req, reply) => {
    try {
      const id = Number(req.params.ma_id);
      if (!Number.isFinite(id) || id < 0) {
        return reply.code(400).send({ ok: false, error: "bad ma_id" });
      }
      const body = req.body || {};
      const updates = {};
      for (const field of BAND_EDITABLE_FIELDS) {
        if (Object.prototype.hasOwnProperty.call(body, field)) {
          updates[field] = body[field];
        }
      }
      if (Object.keys(updates).length === 0) {
        return reply.code(400).send({ ok: false, error: "Aucun champ à modifier" });
      }
      if ("formed_year" in updates) {
        const y = updates.formed_year === null ? null : Number(updates.formed_year);
        if (y !== null && (!Number.isFinite(y) || y < 1800 || y > 2100)) {
          return reply.code(400).send({ ok: false, error: "formed_year invalide" });
        }
        updates.formed_year = y;
      }
      if ("lat" in updates) {
        const v = updates.lat === null ? null : Number(updates.lat);
        if (v !== null && (!Number.isFinite(v) || v < -90 || v > 90)) {
          return reply.code(400).send({ ok: false, error: "lat invalide" });
        }
        updates.lat = v;
      }
      if ("lon" in updates) {
        const v = updates.lon === null ? null : Number(updates.lon);
        if (v !== null && (!Number.isFinite(v) || v < -180 || v > 180)) {
          return reply.code(400).send({ ok: false, error: "lon invalide" });
        }
        updates.lon = v;
      }

      const before = await pool.query(`SELECT * FROM bands WHERE ma_id = $1 LIMIT 1`, [id]);
      if (!before.rows.length) return reply.code(404).send({ ok: false, error: "not found" });

      const setCols = Object.keys(updates);
      // Marque les champs édités comme verrouillés (ne seront plus écrasés par le crawler)
      const newLocked = Object.fromEntries(setCols.map((c) => [c, true]));
      const setSql = setCols.map((c, idx) => `${c} = $${idx + 2}`).join(", ");
      const vals = [id, ...setCols.map((c) => updates[c])];

      const after = await pool.query(
        `UPDATE bands SET ${setSql},
           locked_fields = locked_fields || $${vals.length + 1}::jsonb,
           crawler_pending = (
             SELECT jsonb_strip_nulls(jsonb_build_object(${
               setCols.map((c) => `'${c}', crawler_pending->'${c}'`).join(", ")
             }))
             FROM bands WHERE ma_id = $1
           )
         WHERE ma_id = $1 RETURNING *`,
        [...vals, JSON.stringify(newLocked)]
      );

      await writeAuditLog(
        pool, req.adminUsername, "update", "bands", id,
        before.rows[0], after.rows[0]
      );

      return { ok: true, item: after.rows[0] };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur mise à jour band" });
    }
  });

  // ------------------------------------------------------------------
  // Crawl runs / checkpoints
  // ------------------------------------------------------------------
  fastify.get("/admin/api/crawl-runs", async (req, reply) => {
    try {
      const { run_type, status } = req.query || {};
      const { page, pageSize, offset } = pagination(req.query || {});
      const where = [];
      const vals = [];
      let i = 1;
      if (run_type) { where.push(`run_type = $${i}`); vals.push(run_type); i++; }
      if (status) { where.push(`status = $${i}`); vals.push(status); i++; }
      const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";

      const countRes = await pool.query(`SELECT count(*)::int AS total FROM crawl_run ${whereSql}`, vals);
      vals.push(pageSize, offset);
      const dataRes = await pool.query(`
        SELECT id, run_type, countries, status, started_at, finished_at,
               bands_seen, bands_new, bands_updated, bands_enriched, error
        FROM crawl_run ${whereSql}
        ORDER BY started_at DESC
        LIMIT $${i} OFFSET $${i + 1}
      `, vals);

      return { ok: true, items: dataRes.rows, total: countRes.rows[0].total, page, pageSize };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur crawl runs" });
    }
  });

  fastify.get("/admin/api/crawl-checkpoints", async (req, reply) => {
    try {
      const r = await pool.query(`SELECT key, value, updated_at FROM crawl_checkpoint ORDER BY key`);
      return { ok: true, items: r.rows };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur checkpoints" });
    }
  });

  // ------------------------------------------------------------------
  // Logs crawler
  // ------------------------------------------------------------------
  fastify.get("/admin/api/logs", async (req, reply) => {
    try {
      const { level, run_id, since, min_id } = req.query || {};
      const { page, pageSize, offset } = pagination(req.query || {});
      const where = [];
      const vals = [];
      let i = 1;
      if (level) { where.push(`level = $${i}`); vals.push(level); i++; }
      if (run_id) { where.push(`run_id = $${i}`); vals.push(Number(run_id)); i++; }
      if (since) { where.push(`created_at > $${i}`); vals.push(since); i++; }
      if (min_id) { where.push(`id > $${i}`); vals.push(Number(min_id)); i++; }
      const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";

      vals.push(pageSize, offset);
      const r = await pool.query(`
        SELECT id, run_id, level, message, ma_id, created_at
        FROM crawl_log ${whereSql}
        ORDER BY created_at DESC
        LIMIT $${i} OFFSET $${i + 1}
      `, vals);

      return { ok: true, items: r.rows };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur logs" });
    }
  });

  // ------------------------------------------------------------------
  // Cleanup des runs bloqués (status=running depuis trop longtemps)
  // ------------------------------------------------------------------
  fastify.post("/admin/api/crawl-runs/cleanup", async (req, reply) => {
    try {
      const olderThanMinutes = Math.max(1, Number(req.body?.older_than_minutes) || 30);
      const r = await pool.query(`
        UPDATE crawl_run
        SET status = 'error',
            finished_at = now(),
            error = 'annulé manuellement (run bloqué)'
        WHERE status = 'running'
          AND started_at < now() - make_interval(mins => $1)
        RETURNING id, run_type, started_at
      `, [olderThanMinutes]);
      await writeAuditLog(pool, req.adminUsername, "cleanup_stuck_runs", "crawl_run", null, null, { cleaned: r.rows });
      return { ok: true, cleaned: r.rows.length, runs: r.rows };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur cleanup" });
    }
  });

  // ------------------------------------------------------------------
  // Conflits : champs verrouillés avec valeur différente côté crawler
  // ------------------------------------------------------------------
  fastify.get("/admin/api/conflicts", async (req, reply) => {
    try {
      const { page, pageSize, offset } = pagination(req.query || {});
      const r = await pool.query(`
        SELECT ma_id, name, country, status, genre, themes, formed_year,
               locked_fields, crawler_pending
        FROM bands
        WHERE crawler_pending IS NOT NULL AND crawler_pending != '{}'::jsonb
        ORDER BY updated_at DESC
        LIMIT $1 OFFSET $2
      `, [pageSize, offset]);
      const count = await pool.query(`SELECT count(*)::int AS n FROM bands WHERE crawler_pending IS NOT NULL AND crawler_pending != '{}'::jsonb`);
      return { ok: true, items: r.rows, total: count.rows[0].n, page, pageSize };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur conflicts" });
    }
  });

  fastify.post("/admin/api/bands/:ma_id/resolve-conflict", async (req, reply) => {
    try {
      const id = Number(req.params.ma_id);
      if (!Number.isFinite(id)) return reply.code(400).send({ ok: false, error: "bad ma_id" });
      const { field, action } = req.body || {};  // action: 'keep_mine' | 'accept_crawler'
      if (!field || !["keep_mine", "accept_crawler"].includes(action)) {
        return reply.code(400).send({ ok: false, error: "field et action requis" });
      }
      const before = await pool.query(`SELECT * FROM bands WHERE ma_id = $1`, [id]);
      if (!before.rows.length) return reply.code(404).send({ ok: false, error: "not found" });

      let sql, vals;
      if (action === "keep_mine") {
        // Garde la valeur actuelle, efface juste le pending pour ce champ
        sql = `UPDATE bands SET crawler_pending = crawler_pending - $2 WHERE ma_id = $1`;
        vals = [id, field];
      } else {
        // Applique la valeur du crawler, déverrouille le champ
        const crawlerVal = before.rows[0].crawler_pending?.[field];
        sql = `UPDATE bands SET ${field} = $2::text, locked_fields = locked_fields - $3, crawler_pending = crawler_pending - $3 WHERE ma_id = $1`;
        vals = [id, crawlerVal, field];
      }
      const after = await pool.query(sql + " RETURNING *", vals);
      await writeAuditLog(pool, req.adminUsername, `resolve_conflict:${action}`, "bands", id, before.rows[0], after.rows[0]);
      return { ok: true, item: after.rows[0] };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur résolution conflit" });
    }
  });

  // ------------------------------------------------------------------
  // Géocodage : stats + déclenchement
  // ------------------------------------------------------------------
  fastify.get("/admin/api/geocoding", async (req, reply) => {
    try {
      const [queue, cache, recent, locations, llm, providers, llmModels] = await Promise.all([
        pool.query(`
          SELECT status, count(*)::int AS n FROM geocode_queue GROUP BY status ORDER BY n DESC
        `),
        pool.query(`SELECT count(*)::int AS n FROM geocode_cache`),
        pool.query(`
          SELECT b.ma_id, b.name, b.country, b.geocode_provider, b.geocoded_at,
                 b.geocode_query, b.geocode_error
          FROM bands b
          WHERE b.geocoded_at IS NOT NULL
          ORDER BY b.geocoded_at DESC LIMIT 20
        `),
        pool.query(`
          SELECT geocode_status AS status, count(*)::int AS n
          FROM band_locations
          GROUP BY geocode_status ORDER BY n DESC
        `).catch(() => ({ rows: [] })),
        pool.query(`
          SELECT count(*)::int AS n,
                 sum(cost_usd)::numeric(10,6) AS total_cost_usd,
                 count(*) FILTER (WHERE is_null)::int AS n_null
          FROM llm_cache
        `).catch(() => ({ rows: [{ n: 0, total_cost_usd: 0, n_null: 0 }] })),
        pool.query(`
          SELECT COALESCE(geocode_provider, '(en attente)') AS provider,
                 count(*)::int AS n,
                 round(avg(geocode_confidence)::numeric, 2) AS avg_conf
          FROM band_locations
          GROUP BY geocode_provider ORDER BY n DESC
        `).catch(() => ({ rows: [] })),
        pool.query(`
          SELECT model,
                 count(*)::int AS n,
                 count(*) FILTER (WHERE is_null)::int AS n_null,
                 sum(cost_usd)::numeric(10,6) AS cost
          FROM llm_cache GROUP BY model ORDER BY n DESC
        `).catch(() => ({ rows: [] })),
      ]);
      return {
        ok: true,
        queue: queue.rows,
        cache_size: cache.rows[0].n,
        recent: recent.rows,
        locations: locations.rows,
        llm_cache: llm.rows[0],
        providers: providers.rows,
        llm_models: llmModels.rows,
      };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur geocoding stats" });
    }
  });

  // Appels LLM récents (debug). Filtres: model, only_null=1, q (location).
  fastify.get("/admin/api/llm", async (req, reply) => {
    try {
      const { model, only_null, q } = req.query || {};
      const { pageSize, offset } = pagination(req.query || {});
      const where = [];
      const vals = [];
      let i = 1;
      if (model) { where.push(`model = $${i}`); vals.push(String(model)); i++; }
      if (only_null === "1" || only_null === "true") where.push(`is_null = true`);
      if (q) {
        const qq = String(q).trim().slice(0, 100);
        if (qq.length >= 1) {
          where.push(`(location_raw ILIKE $${i} OR parsed_city ILIKE $${i})`);
          vals.push(`%${qq}%`); i++;
        }
      }
      const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
      const [rows, cnt] = await Promise.all([
        pool.query(`
          SELECT lc.id, lc.ma_id, b.name AS band_name, lc.model, lc.location_raw,
                 lc.country, lc.parsed_city, lc.parsed_country, lc.is_null,
                 lc.tokens_in, lc.tokens_out, lc.cost_usd, lc.prompt, lc.response,
                 lc.created_at
          FROM llm_cache lc
          LEFT JOIN bands b ON b.ma_id = lc.ma_id
          ${whereSql}
          ORDER BY lc.created_at DESC
          LIMIT $${i} OFFSET $${i + 1}
        `, [...vals, pageSize, offset]),
        pool.query(`SELECT count(*)::int AS total FROM llm_cache lc ${whereSql}`, vals),
      ]);
      return { ok: true, items: rows.rows, total: cnt.rows[0].total };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur LLM debug" });
    }
  });

  // ------------------------------------------------------------------
  // Job triggers (déclenche un job dans le crawler sans redéployer)
  // ------------------------------------------------------------------
  fastify.get("/admin/api/job-triggers", async (req, reply) => {
    try {
      const r = await pool.query(`
        SELECT id, job_type, status, requested_by, created_at, started_at, finished_at, error
        FROM job_triggers ORDER BY created_at DESC LIMIT 30
      `);
      return { ok: true, items: r.rows };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur job triggers" });
    }
  });

  fastify.post("/admin/api/job-triggers", async (req, reply) => {
    try {
      const { job_type } = req.body || {};
      const allowed = ["enrich", "incremental", "full_crawl", "geocoder_enqueue"];
      if (!allowed.includes(job_type)) {
        return reply.code(400).send({ ok: false, error: `job_type doit être: ${allowed.join(", ")}` });
      }
      const r = await pool.query(
        `INSERT INTO job_triggers (job_type, requested_by) VALUES ($1, $2) RETURNING id`,
        [job_type, req.adminUsername]
      );
      const triggerId = r.rows[0].id;
      await pool.query(
        `INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
        ["info", `[admin:${req.adminUsername}] job trigger créé: ${job_type} (#${triggerId})`]
      ).catch(() => {});
      return { ok: true, id: triggerId };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur création job" });
    }
  });

  // ------------------------------------------------------------------
  // Géocodage — actions directes sur la queue
  // ------------------------------------------------------------------
  fastify.post("/admin/api/geocoding/reset-errors", async (req, reply) => {
    try {
      const r = await pool.query(`
        UPDATE geocode_queue
        SET status='queued', next_run_at=now(), updated_at=now()
        WHERE status='error'
      `);
      const count = r.rowCount;
      await pool.query(
        `INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
        ["info", `[admin:${req.adminUsername}] geocoding reset-errors: ${count} entrée(s) remise(s) en queue`]
      ).catch(() => {});
      return { ok: true, count };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur reset-errors" });
    }
  });

  fastify.post("/admin/api/geocoding/requeue-all", async (req, reply) => {
    try {
      const { include_done = false } = req.body || {};
      const statuses = include_done ? ["error", "done"] : ["error"];
      const r = await pool.query(`
        UPDATE geocode_queue
        SET status='queued', next_run_at=now(), tries=0, last_error=NULL, updated_at=now()
        WHERE status = ANY($1::text[])
      `, [statuses]);
      const count = r.rowCount;
      await pool.query(
        `INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
        ["info", `[admin:${req.adminUsername}] geocoding requeue-all (include_done=${include_done}): ${count} re-queueée(s)`]
      ).catch(() => {});
      return { ok: true, count };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur requeue-all" });
    }
  });

  // ------------------------------------------------------------------
  // Géocodage — actions sur band_locations (nouveau pipeline)
  // ------------------------------------------------------------------
  fastify.post("/admin/api/locations/reset-errors", async (req, reply) => {
    try {
      const r = await pool.query(`
        UPDATE band_locations
        SET geocode_status='queued', geocode_tries_geo=0,
            geocode_error=NULL, geocode_next_at=now(), updated_at=now()
        WHERE geocode_status = 'error'
      `);
      const count = r.rowCount;
      await pool.query(
        `INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
        ["info", `[admin:${req.adminUsername}] locations reset-errors: ${count} remise(s) en queue`]
      ).catch(() => {});
      return { ok: true, count };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur locations reset-errors" });
    }
  });

  fastify.post("/admin/api/locations/reset-llm", async (req, reply) => {
    try {
      const r = await pool.query(`
        UPDATE band_locations
        SET geocode_status='queued', geocode_tries_geo=0, geocode_tries_llm=0,
            geocode_query=NULL, geocode_error=NULL, geocode_next_at=now(), updated_at=now()
        WHERE geocode_status IN ('llm_needed', 'manual')
      `);
      const count = r.rowCount;
      await pool.query(
        `INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
        ["info", `[admin:${req.adminUsername}] locations reset-llm: ${count} remise(s) en queue`]
      ).catch(() => {});
      return { ok: true, count };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur locations reset-llm" });
    }
  });

  fastify.post("/admin/api/locations/requeue-all", async (req, reply) => {
    try {
      const { include_done = false } = req.body || {};
      const statuses = include_done
        ? ["error", "llm_needed", "manual", "done"]
        : ["error", "llm_needed"];
      const r = await pool.query(`
        UPDATE band_locations
        SET geocode_status='queued', geocode_tries_geo=0, geocode_tries_llm=0,
            geocode_query=NULL, geocode_error=NULL, geocode_next_at=now(), updated_at=now()
        WHERE geocode_status = ANY($1::text[])
          AND is_country_only = FALSE
      `, [statuses]);
      const count = r.rowCount;
      await pool.query(
        `INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
        ["info", `[admin:${req.adminUsername}] locations requeue-all (include_done=${include_done}): ${count}`]
      ).catch(() => {});
      return { ok: true, count };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur locations requeue-all" });
    }
  });

  // ------------------------------------------------------------------
  // Live status (agrégé pour le Monitor)
  // ------------------------------------------------------------------
  fastify.get("/admin/api/live", async (req, reply) => {
    try {
      const [active_runs, pending_jobs, geo_queue, geo_processing, checkpoints] = await Promise.all([
        pool.query(`
          SELECT id, run_type, countries, status, started_at,
                 bands_seen, bands_new, bands_updated, bands_enriched, error
          FROM crawl_run WHERE status = 'running' ORDER BY started_at DESC
        `),
        pool.query(`
          SELECT id, job_type, status, requested_by, created_at
          FROM job_triggers WHERE status = 'pending' ORDER BY created_at ASC
        `),
        pool.query(`SELECT status, count(*)::int AS n FROM geocode_queue GROUP BY status ORDER BY n DESC`),
        pool.query(`
          SELECT gq.ma_id, b.name, b.country, gq.tries, gq.last_error, gq.updated_at
          FROM geocode_queue gq JOIN bands b ON b.ma_id = gq.ma_id
          WHERE gq.status = 'processing' LIMIT 3
        `),
        pool.query(`SELECT key, value, updated_at FROM crawl_checkpoint ORDER BY key`),
      ]);
      return {
        ok: true,
        active_runs: active_runs.rows,
        pending_jobs: pending_jobs.rows,
        geo_queue: geo_queue.rows,
        geo_processing: geo_processing.rows,
        checkpoints: checkpoints.rows,
      };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur live status" });
    }
  });

  // ------------------------------------------------------------------
  // Annuler un crawl run spécifique
  // ------------------------------------------------------------------
  fastify.post("/admin/api/crawl-runs/:id/cancel", async (req, reply) => {
    try {
      const id = Number(req.params.id);
      if (!Number.isFinite(id)) return reply.code(400).send({ ok: false, error: "bad id" });
      const r = await pool.query(`
        UPDATE crawl_run SET status='error', finished_at=now(), error='annulé manuellement par admin'
        WHERE id=$1 AND status='running' RETURNING id, run_type
      `, [id]);
      if (!r.rows.length) return reply.code(404).send({ ok: false, error: "Run non trouvé ou déjà terminé" });
      await pool.query(
        `INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
        ["warning", `[admin:${req.adminUsername}] run #${id} (${r.rows[0].run_type}) annulé manuellement`]
      ).catch(() => {});
      return { ok: true, id };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur annulation run" });
    }
  });

  // ------------------------------------------------------------------
  // Annuler un job trigger en attente
  // ------------------------------------------------------------------
  fastify.post("/admin/api/job-triggers/:id/cancel", async (req, reply) => {
    try {
      const id = Number(req.params.id);
      if (!Number.isFinite(id)) return reply.code(400).send({ ok: false, error: "bad id" });
      const r = await pool.query(`
        UPDATE job_triggers SET status='cancelled', finished_at=now()
        WHERE id=$1 AND status='pending' RETURNING id, job_type
      `, [id]);
      if (!r.rows.length) return reply.code(404).send({ ok: false, error: "Job non trouvé ou déjà traité" });
      await pool.query(
        `INSERT INTO crawl_log (level, message) VALUES ($1, $2)`,
        ["warning", `[admin:${req.adminUsername}] job trigger #${id} (${r.rows[0].job_type}) annulé`]
      ).catch(() => {});
      return { ok: true, id };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur annulation job" });
    }
  });

  // ------------------------------------------------------------------
  // Journal d'audit (actions admin)
  // ------------------------------------------------------------------
  fastify.get("/admin/api/audit-log", async (req, reply) => {
    try {
      const { page, pageSize, offset } = pagination(req.query || {});
      const r = await pool.query(`
        SELECT id, admin_username, action, target_table, target_id, before_data, after_data, created_at
        FROM admin_audit_log
        ORDER BY created_at DESC
        LIMIT $1 OFFSET $2
      `, [pageSize, offset]);
      return { ok: true, items: r.rows, page, pageSize };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur audit log" });
    }
  });
}
