import Fastify from "fastify";
import pg from "pg";
import rateLimit from "@fastify/rate-limit";
import helmet from "@fastify/helmet";
import cookie from "@fastify/cookie";
import { timingSafeEqual } from "crypto";
import {
  ADMIN_COOKIE_NAME,
  seedAdminUser,
  signAdminSession,
  isLockedOut,
  recordLoginAttempt,
  verifyPassword,
  markLoginSuccess,
  requireAdminSession,
} from "./adminAuth.js";
import adminApiRoutes from "./adminRoutes.js";

const { Pool } = pg;

const fastify = Fastify({
  logger: true,
  bodyLimit: 10485760,  // 10 MB max pour éviter DoS mémoire
  trustProxy: true  // derrière Traefik (+ nginx pour /admin/*) : lit X-Forwarded-For
});

// Headers de sécurité
await fastify.register(helmet, {
  contentSecurityPolicy: false,
  crossOriginEmbedderPolicy: false
});

await fastify.register(cookie);

// CORS middleware
fastify.addHook('onRequest', async (request, reply) => {
  const origin = request.headers.origin;
  const allowedOrigins = process.env.CORS_ORIGINS
    ? process.env.CORS_ORIGINS.split(',').map(s => s.trim())
    : ['https://metalfrom.eu', 'https://www.metalfrom.eu'];
  
  if (allowedOrigins.includes(origin)) {
    reply.header('Access-Control-Allow-Origin', origin);
  }
  
  reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
  reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  reply.header('Access-Control-Allow-Credentials', 'true');
  
  if (request.method === 'OPTIONS') {
    reply.status(204).send();
  }
});

// Rate limiting global
await fastify.register(rateLimit, {
  max: 1000,
  timeWindow: '1 minute',
  cache: 10000
});

// Gestion d'erreurs globale
fastify.setErrorHandler((error, request, reply) => {
  fastify.log.error(error);
  reply.status(500).send({ 
    ok: false, 
    error: 'Une erreur est survenue' 
  });
});

const PORT = Number(process.env.PORT || 3000);
const DATABASE_URL = process.env.DATABASE_URL;
const BM_IMPORT_TOKEN = (process.env.BM_IMPORT_TOKEN || "").trim();

let pool = null;
if (DATABASE_URL) {
  pool = new Pool({
    connectionString: DATABASE_URL,
    statement_timeout: 60000
  });
  seedAdminUser(pool).catch((err) => fastify.log.error({ err }, "[admin] seed failed"));
}

function requirePool() {
  if (!pool) throw new Error("DATABASE_URL not set");
  return pool;
}

function authBearer(req) {
  const h = req.headers.authorization || "";
  const m = h.match(/^Bearer\s+(.+)$/i);
  return m ? m[1] : null;
}

function requireAdmin(req, reply) {
  const provided = authBearer(req);
  
  if (!BM_IMPORT_TOKEN || !provided || provided.length !== BM_IMPORT_TOKEN.length) {
    reply.code(401).send({ ok: false, error: "unauthorized" });
    return false;
  }
  
  const valid = timingSafeEqual(
    Buffer.from(provided),
    Buffer.from(BM_IMPORT_TOKEN)
  );
  
  if (!valid) {
    reply.code(401).send({ ok: false, error: "unauthorized" });
    return false;
  }
  
  return true;
}

function sanitizeSearchString(str, fieldName = 'champ') {
  const trimmed = String(str).trim();
  
  if (trimmed.length < 2 || trimmed.length > 100) {
    throw new Error(`${fieldName} doit faire entre 2 et 100 caractères`);
  }
  
  if (/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/.test(trimmed)) {
    throw new Error(`${fieldName} contient des caractères invalides`);
  }
  
  if (/%%%|_{10,}|%{10,}/.test(trimmed)) {
    throw new Error(`${fieldName} contient des patterns invalides`);
  }
  
  return trimmed;
}

fastify.get("/", async () => {
  return { ok: true, service: "BM API" };
});

fastify.get("/api/health", async () => ({ ok: true }));

fastify.get("/api/db", async (req, reply) => {
  try {
    const p = requirePool();
    const r = await p.query("SELECT now() as now, current_database() as db");
    return { ok: true, ...r.rows[0] };
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send({ ok: false, error: 'Erreur de connexion à la base de données' });
  }
});

fastify.get("/api/stats", async (req, reply) => {
  try {
    const p = requirePool();
    const q = `
      SELECT 
        count(*)::int as total,
        count(*) FILTER (WHERE geom IS NOT NULL)::int as geocoded,
        count(*) FILTER (WHERE geom IS NULL)::int as no_location,
        count(*) FILTER (WHERE enriched = true)::int as enriched
      FROM bands;
    `;
    const r = await p.query(q);
    return { ok: true, ...r.rows[0] };
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send({ ok: false, error: 'Erreur lors de la récupération des stats' });
  }
});

fastify.get("/api/countries", async (req, reply) => {
  try {
    const p = requirePool();
    const q = `
      SELECT 
        country,
        count(*)::int as total,
        count(*) FILTER (WHERE geom IS NOT NULL)::int as geocoded,
        count(*) FILTER (WHERE enriched = true)::int as enriched
      FROM bands
      GROUP BY country
      ORDER BY total DESC;
    `;
    const r = await p.query(q);
    return { ok: true, items: r.rows };
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send({ ok: false, error: 'Erreur lors de la récupération des pays' });
  }
});

fastify.get("/api/statuses", async (req, reply) => {
  try {
    const p = requirePool();
    const q = `
      SELECT 
        COALESCE(NULLIF(trim(status), ''), 'Unknown') as status,
        count(*)::int as total,
        count(*) FILTER (WHERE geom IS NOT NULL)::int as geocoded
      FROM bands
      GROUP BY 1
      ORDER BY total DESC;
    `;
    const r = await p.query(q);
    return { ok: true, items: r.rows };
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send({ ok: false, error: 'Erreur lors de la récupération des statuts' });
  }
});

fastify.get("/api/clusters", async (req, reply) => {
  try {
    const p = requirePool();
    const {
      bbox,
      zoom,
      countries,
      status,
      genre,
      year_min,
      year_max,
    } = req.query || {};

    if (!bbox || !zoom) {
      return reply.code(400).send({ ok: false, error: "bbox and zoom are required" });
    }

    const bboxParts = String(bbox).split(",").map(Number);
    if (bboxParts.length !== 4) {
      return reply.code(400).send({ ok: false, error: "bbox must have 4 values" });
    }
    
    const [minLon, minLat, maxLon, maxLat] = bboxParts;
    const zoomLevel = Number(zoom);

    if ([minLon, minLat, maxLon, maxLat, zoomLevel].some(v => !Number.isFinite(v))) {
      return reply.code(400).send({ ok: false, error: "Invalid bbox or zoom values" });
    }

    if (minLat < -90 || maxLat > 90 || minLon < -180 || maxLon > 180) {
      return reply.code(400).send({ ok: false, error: "Coordinates out of range" });
    }
    
    if (minLon >= maxLon || minLat >= maxLat) {
      return reply.code(400).send({ ok: false, error: "Invalid bbox bounds" });
    }
    
    if (zoomLevel < 0 || zoomLevel > 22) {
      return reply.code(400).send({ ok: false, error: "Zoom level must be between 0 and 22" });
    }

    const cellSize = Math.max(0.01, 180 / Math.pow(2, zoomLevel));
    
    // Source: band_locations (un point par localisation géocodée) JOIN bands.
    const where = [
      "bl.geom IS NOT NULL",
      "bl.geocode_status IN ('done','country_only')",
    ];
    const vals = [];
    let i = 1;

    where.push(`bl.geom && ST_MakeEnvelope($${i}, $${i+1}, $${i+2}, $${i+3}, 4326)`);
    vals.push(minLon, minLat, maxLon, maxLat);
    i += 4;

    if (countries) {
      const cs = String(countries).split(",").map(s => s.trim().toUpperCase()).filter(Boolean);
      if (cs.length > 100) {
        return reply.code(400).send({ ok: false, error: "Max 100 pays simultanés" });
      }
      if (cs.length) {
        where.push(`b.country = ANY($${i}::text[])`);
        vals.push(cs);
        i++;
      }
    }

    if (status) {
      const st = String(status).split(",").map(s => s.trim()).filter(Boolean);
      if (st.length > 50) {
        return reply.code(400).send({ ok: false, error: "Max 50 statuts simultanés" });
      }
      if (st.length) {
        where.push(`COALESCE(NULLIF(trim(b.status), ''), 'Unknown') = ANY($${i}::text[])`);
        vals.push(st);
        i++;
      }
    }

    if (genre) {
      try {
        const genreQuery = sanitizeSearchString(genre, 'Genre');
        where.push(`b.genre ILIKE $${i}`);
        vals.push(`%${genreQuery}%`);
        i++;
      } catch (err) {
        return reply.code(400).send({ ok: false, error: err.message });
      }
    }

    if (year_min) {
      const yearMin = Number(year_min);
      if (!Number.isFinite(yearMin) || yearMin < 1800 || yearMin > 2100) {
        return reply.code(400).send({ ok: false, error: 'year_min invalide' });
      }
      where.push(`b.formed_year >= $${i}`);
      vals.push(yearMin);
      i++;
    }

    if (year_max) {
      const yearMax = Number(year_max);
      if (!Number.isFinite(yearMax) || yearMax < 1800 || yearMax > 2100) {
        return reply.code(400).send({ ok: false, error: 'year_max invalide' });
      }
      where.push(`b.formed_year <= $${i}`);
      vals.push(yearMax);
      i++;
    }

    const whereSql = where.join(" AND ");

    if (zoomLevel >= 12) {
      const sql = `
        SELECT
          b.ma_id,
          b.name,
          b.country,
          COALESCE(NULLIF(trim(b.status), ''), 'Unknown') as status,
          b.genre,
          b.location_text,
          b.formed_year,
          bl.lat,
          bl.lon,
          bl.location_raw,
          bl.step_order,
          bl.step_label,
          bl.is_country_only
        FROM band_locations bl
        JOIN bands b ON b.ma_id = bl.ma_id
        WHERE ${whereSql}
        ORDER BY bl.ma_id ASC, bl.step_order ASC
        LIMIT 2000;
      `;
      const r = await p.query(sql, vals);
      return {
        ok: true,
        type: "bands",
        items: r.rows,
        count: r.rows.length
      };
    }

    const sql = `
      WITH grid_cells AS (
        SELECT
          floor(ST_X(bl.geom) / $${i})::int as cell_x,
          floor(ST_Y(bl.geom) / $${i})::int as cell_y,
          b.ma_id,
          b.name,
          b.country,
          COALESCE(NULLIF(trim(b.status), ''), 'Unknown') as status,
          b.genre,
          b.location_text,
          b.formed_year,
          bl.lat,
          bl.lon,
          bl.location_raw,
          bl.step_label
        FROM band_locations bl
        JOIN bands b ON b.ma_id = bl.ma_id
        WHERE ${whereSql}
      ),
      clusters AS (
        SELECT
          cell_x,
          cell_y,
          count(*)::int as band_count,
          avg(lat)::float8 as center_lat,
          avg(lon)::float8 as center_lon,
          array_agg(json_build_object(
            'ma_id', ma_id,
            'name', name,
            'country', country,
            'status', status,
            'genre', genre,
            'location_text', location_text,
            'formed_year', formed_year,
            'lat', lat,
            'lon', lon,
            'location_raw', location_raw,
            'step_label', step_label
          ) ORDER BY name) as bands
        FROM grid_cells
        GROUP BY cell_x, cell_y
      )
      SELECT 
        center_lat as lat,
        center_lon as lon,
        band_count as count,
        CASE 
          WHEN band_count <= 5 THEN bands 
          ELSE bands[1:5] 
        END as sample_bands
      FROM clusters
      ORDER BY band_count DESC
      LIMIT 1000;
    `;
    vals.push(cellSize);
    
    const r = await p.query(sql, vals);
    return {
      ok: true,
      type: "clusters",
      items: r.rows,
      total_clusters: r.rows.length,
      cell_size: cellSize,
      zoom: zoomLevel
    };
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send({ ok: false, error: 'Erreur lors de la récupération des clusters' });
  }
});

fastify.get("/api/bands", async (req, reply) => {
  try {
    const p = requirePool();
    const {
      countries,
      geocoded,
      q,
      status,
      only_black,
      limit,
      offset,
    } = req.query || {};

    const lim = Math.min(Number(limit || 1000), 150000);
    const off = Math.max(Number(offset || 0), 0);
    
    if (off > 1000000) {
      return reply.code(400).send({ 
        ok: false, 
        error: "Offset trop grand (max 1 million)" 
      });
    }

    if (lim > 10000) {
      fastify.log.warn(`Large query: ${lim} rows requested from ${req.ip}`);
    }

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

    if (countries) {
      const cs = String(countries).split(",").map(s => s.trim().toUpperCase()).filter(Boolean);
      if (cs.length > 100) {
        return reply.code(400).send({ ok: false, error: "Max 100 pays simultanés" });
      }
      if (cs.length) {
        where.push(`country = ANY($${i}::text[])`);
        vals.push(cs);
        i++;
      }
    }

    if (geocoded === "1") where.push(`geom IS NOT NULL`);
    if (geocoded === "0") where.push(`geom IS NULL`);

    if (status) {
      const st = String(status).split(",").map(s => s.trim()).filter(Boolean);
      if (st.length > 50) {
        return reply.code(400).send({ ok: false, error: "Max 50 statuts simultanés" });
      }
      if (st.length) {
        where.push(`COALESCE(NULLIF(trim(status), ''), 'Unknown') = ANY($${i}::text[])`);
        vals.push(st);
        i++;
      }
    }

    if (only_black === "1") {
      where.push(`genre ILIKE '%black%'`);
    }

    if (q) {
      try {
        const query = sanitizeSearchString(q, 'Recherche');
        const qq = `%${query}%`;
        where.push(`(name ILIKE $${i} OR genre ILIKE $${i} OR location_text ILIKE $${i} OR COALESCE(status,'') ILIKE $${i})`);
        vals.push(qq);
        i++;
      } catch (err) {
        return reply.code(400).send({ ok: false, error: err.message });
      }
    }

    const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
    
    const sql = `
      SELECT 
        ma_id,
        name,
        country,
        COALESCE(NULLIF(trim(status), ''), 'Unknown') as status,
        genre,
        location_text,
        enriched,
        formed_year,
        themes,
        lat,
        lon
      FROM bands
      ${whereSql}
      ORDER BY ma_id ASC
      LIMIT $${i} OFFSET $${i+1};
    `;
    vals.push(lim, off);

    const r = await p.query(sql, vals);
    return {
      ok: true,
      items: r.rows,
      limit: lim,
      offset: off
    };
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send({ ok: false, error: 'Erreur lors de la recherche' });
  }
});

fastify.get("/api/facets", async (req, reply) => {
  try {
    const p = requirePool();
    
    const [statusRes, countryRes, genreRes, yearRes] = await Promise.all([
      p.query(`
        SELECT 
          COALESCE(NULLIF(trim(status), ''), 'Unknown') as value,
          count(*)::int as count
        FROM bands
        WHERE geom IS NOT NULL
        GROUP BY 1
        ORDER BY count DESC
      `),
      p.query(`
        SELECT 
          COALESCE(country, '??') as value,
          count(*)::int as count
        FROM bands
        WHERE geom IS NOT NULL
        GROUP BY 1
        ORDER BY count DESC
      `),
      p.query(`
        SELECT 
          genre as value,
          count(*)::int as count
        FROM bands
        WHERE geom IS NOT NULL 
          AND genre IS NOT NULL 
          AND genre != ''
        GROUP BY 1
        ORDER BY count DESC
        LIMIT 500
      `),
      p.query(`
        SELECT 
          MIN(formed_year)::int as min_year,
          MAX(formed_year)::int as max_year
        FROM bands
        WHERE geom IS NOT NULL 
          AND formed_year >= 1900 
          AND formed_year <= extract(year from now())
      `)
    ]);

    return {
      ok: true,
      statuses: statusRes.rows,
      countries: countryRes.rows,
      genres: genreRes.rows,
      year_range: yearRes.rows[0] || { min_year: null, max_year: null }
    };
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send({ ok: false, error: 'Erreur lors de la récupération des facettes' });
  }
});

fastify.get("/api/band/:ma_id", async (req, reply) => {
  try {
    const p = requirePool();
    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 = await p.query(
      `SELECT 
        ma_id,
        name,
        country,
        status,
        genre,
        location_text,
        enriched,
        data,
        formed_year,
        themes,
        lat,
        lon,
        geocoded_at
      FROM bands
      WHERE ma_id = $1
      LIMIT 1`,
      [id]
    );

    if (!r.rows.length) {
      return reply.code(404).send({ ok: false, error: "not found" });
    }

    return { ok: true, item: r.rows[0] };
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send({ ok: false, error: 'Erreur lors de la récupération du groupe' });
  }
});

fastify.register(async function(adminRoutes) {
  await adminRoutes.register(rateLimit, {
    max: 10,
    timeWindow: '1 minute',
    keyGenerator: (req) => {
      return authBearer(req) || req.ip;
    }
  });

  adminRoutes.post("/admin/import", async (req, reply) => {
    if (!requireAdmin(req, reply)) return;

    try {
      const body = req.body;
      
      if (!body || typeof body !== "object") {
        return reply.code(400).send({ ok: false, error: "invalid body/json" });
      }

      const bands = Array.isArray(body.bands) ? body.bands : null;
      
      if (!bands) {
        return reply.code(400).send({ ok: false, error: "missing bands[]" });
      }
      
      if (bands.length > 1000) {
        return reply.code(400).send({ 
          ok: false, 
          error: "Max 1000 bands par import" 
        });
      }

      const p = requirePool();
      let upserted = 0;

      for (const b of bands) {
        if (!b || typeof b !== "object") continue;

        const ma_id = Number(b.ma_id);
        if (!Number.isFinite(ma_id)) continue;

        const name = b.name ?? null;
        const country = b.country ?? null;
        const status = b.status ?? null;
        const genre = b.genre ?? null;
        const location_text = b.location_text ?? b.location ?? null;
        const hasData = b.data && typeof b.data === "object";
        const data = hasData ? b.data : null;
        const enriched = hasData ? true : (b.enriched ?? null);

        await p.query(
          `
          INSERT INTO bands (ma_id, name, country, status, genre, location_text, data, enriched)
          VALUES ($1,$2,$3,$4,$5,$6,$7, COALESCE($8,false))
          ON CONFLICT (ma_id) DO UPDATE SET
            name = COALESCE(EXCLUDED.name, bands.name),
            country = COALESCE(EXCLUDED.country, bands.country),
            status = COALESCE(EXCLUDED.status, bands.status),
            genre = COALESCE(EXCLUDED.genre, bands.genre),
            location_text = COALESCE(EXCLUDED.location_text, bands.location_text),
            data = COALESCE(EXCLUDED.data, bands.data),
            enriched = COALESCE(EXCLUDED.enriched, bands.enriched)
          `,
          [ma_id, name, country, status, genre, location_text, data, enriched]
        );
        upserted++;
      }

      return reply.send({ ok: true, upserted });
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: 'Erreur lors de l\'import' });
    }
  });

  adminRoutes.get("/admin/enrich/next", async (req, reply) => {
    if (!requireAdmin(req, reply)) return;

    try {
      const p = requirePool();
      const limitRaw = Number(req.query?.limit ?? 50);
      const limit = Math.max(1, Math.min(Number.isFinite(limitRaw) ? limitRaw : 50, 500));
      const country = (req.query?.country ?? "").toString().trim().toUpperCase();

      const params = [];
      let where = "WHERE (data->'band_page') IS NULL";

      if (country) {
        params.push(country);
        where += ` AND country = $${params.length}`;
      }

      params.push(limit);

      const sql = `
        SELECT 
          ma_id,
          (data->>'url') AS url
        FROM bands
        ${where}
        ORDER BY ma_id ASC
        LIMIT $${params.length}
      `;

      const r = await p.query(sql, params);
      const items = (r.rows || []).filter((x) => x.url);

      return { ok: true, count: items.length, items };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: 'Erreur lors de la récupération des groupes à enrichir' });
    }
  });
});

// ------------------------------------------------------------------
// Auth dashboard admin (session cookie, distincte du token BM_IMPORT_TOKEN)
// ------------------------------------------------------------------
fastify.register(async function (authRoutes) {
  await authRoutes.register(rateLimit, {
    max: 10,
    timeWindow: "1 minute",
    keyGenerator: (req) => req.ip,
  });

  authRoutes.post("/admin/auth/login", async (req, reply) => {
    try {
      const p = requirePool();
      const { username, password } = req.body || {};
      if (typeof username !== "string" || typeof password !== "string" || !username || !password) {
        return reply.code(400).send({ ok: false, error: "username et password requis" });
      }
      const uname = username.trim().slice(0, 100);
      const ip = req.ip;

      if (await isLockedOut(p, uname, ip)) {
        return reply.code(429).send({ ok: false, error: "Trop de tentatives, réessayez dans quelques minutes" });
      }

      const valid = await verifyPassword(p, uname, password);
      await recordLoginAttempt(p, uname, ip, valid);

      if (!valid) {
        return reply.code(401).send({ ok: false, error: "Identifiants invalides" });
      }

      await markLoginSuccess(p, uname);
      const token = signAdminSession(uname);
      reply.setCookie(ADMIN_COOKIE_NAME, token, {
        httpOnly: true,
        secure: true,
        sameSite: "strict",
        path: "/",
        maxAge: 12 * 3600,
      });
      return { ok: true, username: uname };
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send({ ok: false, error: "Erreur de connexion" });
    }
  });

  authRoutes.post("/admin/auth/logout", async (req, reply) => {
    reply.clearCookie(ADMIN_COOKIE_NAME, { path: "/" });
    return { ok: true };
  });

  authRoutes.get("/admin/auth/me", async (req, reply) => {
    const username = requireAdminSession(req, reply);
    if (!username) return;
    return { ok: true, username };
  });
});

await fastify.register(adminApiRoutes, { pool });

fastify.listen({ port: PORT, host: "0.0.0.0" });