import { Router } from "express";
import { Prisma } from "@prisma/client";
import { z } from "zod";
import { prisma } from "../../prisma.js";
import { requireAuth } from "../../middleware/auth.js";
import { HttpError } from "../../lib/http-error.js";
import { recomputeScore, recomputeAllScores } from "./drones.scoring.js";

export const dronesRouter = Router();
dronesRouter.use(requireAuth);

const num = (v: unknown): number =>
  typeof v === "bigint" ? Number(v) : Number(v);
const emptyToNull = (v: unknown) =>
  typeof v === "string" && v.trim() === "" ? null : v;
const idParam = z.coerce.number().int().positive();

// =============================================================================
// COMPETENCIA (config global)
// =============================================================================
type CompetenciaRow = {
  id: number;
  nombre: string;
  n_intentos_por_ronda: number;
  ronda_actual: number;
  finalizada: number;
};

dronesRouter.get("/competencia", async (_req, res, next) => {
  try {
    const [c] = await prisma.$queryRaw<CompetenciaRow[]>`
      SELECT id, nombre, n_intentos_por_ronda, ronda_actual, finalizada
      FROM drones_competencia WHERE id = 1
    `;
    res.json({ competencia: c });
  } catch (e) { next(e); }
});

dronesRouter.patch("/competencia", async (req, res, next) => {
  try {
    const body = z.object({
      nombre: z.string().trim().min(1).max(150).optional(),
      n_intentos_por_ronda: z.coerce.number().int().min(1).max(20).optional(),
      ronda_actual: z.coerce.number().int().min(1).max(20).optional(),
      finalizada: z.boolean().optional(),
    }).parse(req.body);

    const sets: Prisma.Sql[] = [];
    if (body.nombre !== undefined) sets.push(Prisma.sql`nombre = ${body.nombre}`);
    if (body.n_intentos_por_ronda !== undefined)
      sets.push(Prisma.sql`n_intentos_por_ronda = ${body.n_intentos_por_ronda}`);
    if (body.ronda_actual !== undefined)
      sets.push(Prisma.sql`ronda_actual = ${body.ronda_actual}`);
    if (body.finalizada !== undefined)
      sets.push(Prisma.sql`finalizada = ${body.finalizada}`);
    if (sets.length === 0) throw new HttpError(400, "Nada para actualizar");

    await prisma.$executeRaw`
      UPDATE drones_competencia SET ${Prisma.join(sets, ", ")} WHERE id = 1
    `;
    const [c] = await prisma.$queryRaw<CompetenciaRow[]>`
      SELECT * FROM drones_competencia WHERE id = 1
    `;
    res.json({ competencia: c });
  } catch (e) { next(e); }
});

// Reinicia la competencia: borra todos los recorridos, cortes y clasificados.
// Mantiene equipos, grupos, asignaciones y catálogos.
// Requiere body { confirm: "REINICIAR" } como guardarraíl.
dronesRouter.post("/competencia/reiniciar", async (req, res, next) => {
  try {
    const body = z.object({ confirm: z.string() }).parse(req.body);
    if (body.confirm !== "REINICIAR") {
      throw new HttpError(400, "Confirmación inválida. Escribí 'REINICIAR'.");
    }

    const counts = await prisma.$transaction(async (tx) => {
      const [recCount] = await tx.$queryRaw<{ n: number | bigint }[]>`
        SELECT COUNT(*) AS n FROM drones_recorridos
      `;
      const [corteCount] = await tx.$queryRaw<{ n: number | bigint }[]>`
        SELECT COUNT(*) AS n FROM drones_cortes
      `;
      // ON DELETE CASCADE en drones_penalizaciones, _bonificaciones, _violaciones
      // se encarga de los detalles cuando borramos recorridos. Igual con clasificados.
      await tx.$executeRaw`DELETE FROM drones_corte_clasificados`;
      await tx.$executeRaw`DELETE FROM drones_cortes`;
      await tx.$executeRaw`DELETE FROM drones_recorridos`;
      await tx.$executeRaw`
        UPDATE drones_competencia SET ronda_actual = 1, finalizada = FALSE WHERE id = 1
      `;
      return {
        recorridos_borrados: Number(recCount?.n ?? 0),
        cortes_borrados: Number(corteCount?.n ?? 0),
      };
    });

    res.json({ ok: true, ...counts });
  } catch (e) { next(e); }
});

// =============================================================================
// EQUIPOS
// =============================================================================
const equipoSchema = z.object({
  nombre: z.string().trim().min(1).max(150),
  institucion: z.preprocess(emptyToNull, z.string().trim().max(200).nullable()).optional(),
  operador: z.string().trim().min(1).max(150),
  miembros: z.preprocess(emptyToNull, z.string().trim().nullable()).optional(),
  calcomania: z.preprocess(emptyToNull, z.string().trim().max(50).nullable()).optional(),
  activo: z.boolean().optional(),
});

type EquipoRow = {
  id: number;
  nombre: string;
  institucion: string | null;
  operador: string;
  miembros: string | null;
  calcomania: string | null;
  activo: number;
  creado_en: Date;
  grupo_id: number | null;
  grupo_nombre: string | null;
};

dronesRouter.get("/equipos", async (req, res, next) => {
  try {
    const q = z.object({
      search: z.string().trim().max(120).optional(),
      soloActivos: z.preprocess((v) => v === "true" || v === "1", z.boolean()).optional(),
      sinGrupo: z.preprocess((v) => v === "true" || v === "1", z.boolean()).optional(),
    }).parse(req.query);

    const conds: Prisma.Sql[] = [Prisma.sql`1=1`];
    if (q.soloActivos) conds.push(Prisma.sql`e.activo = TRUE`);
    if (q.sinGrupo) conds.push(Prisma.sql`ge.grupo_id IS NULL`);
    if (q.search) {
      const term = `%${q.search}%`;
      conds.push(Prisma.sql`(e.nombre LIKE ${term} OR e.operador LIKE ${term} OR e.institucion LIKE ${term})`);
    }
    const where = Prisma.join(conds, " AND ");

    const rows = await prisma.$queryRaw<EquipoRow[]>`
      SELECT e.id, e.nombre, e.institucion, e.operador, e.miembros, e.calcomania,
             e.activo, e.creado_en, ge.grupo_id, g.nombre AS grupo_nombre
      FROM drones_equipos e
      LEFT JOIN drones_grupo_equipo ge ON ge.equipo_id = e.id
      LEFT JOIN drones_grupos g ON g.id = ge.grupo_id
      WHERE ${where}
      ORDER BY e.nombre
    `;
    res.json({ data: rows });
  } catch (e) { next(e); }
});

dronesRouter.post("/equipos", async (req, res, next) => {
  try {
    const body = equipoSchema.parse(req.body);
    const dup = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM drones_equipos WHERE nombre = ${body.nombre} LIMIT 1
    `;
    if (dup.length > 0) throw new HttpError(409, "Ya existe un equipo con ese nombre");

    await prisma.$executeRaw`
      INSERT INTO drones_equipos (nombre, institucion, operador, miembros, calcomania, activo)
      VALUES (${body.nombre}, ${body.institucion ?? null}, ${body.operador},
              ${body.miembros ?? null}, ${body.calcomania ?? null}, ${body.activo ?? true})
    `;
    const [created] = await prisma.$queryRaw<EquipoRow[]>`
      SELECT e.*, NULL AS grupo_id, NULL AS grupo_nombre
      FROM drones_equipos e WHERE id = LAST_INSERT_ID()
    `;
    res.status(201).json({ equipo: created });
  } catch (e) { next(e); }
});

dronesRouter.patch("/equipos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = equipoSchema.partial().parse(req.body);
    const sets: Prisma.Sql[] = [];
    if (body.nombre !== undefined) sets.push(Prisma.sql`nombre = ${body.nombre}`);
    if (body.institucion !== undefined) sets.push(Prisma.sql`institucion = ${body.institucion}`);
    if (body.operador !== undefined) sets.push(Prisma.sql`operador = ${body.operador}`);
    if (body.miembros !== undefined) sets.push(Prisma.sql`miembros = ${body.miembros}`);
    if (body.calcomania !== undefined) sets.push(Prisma.sql`calcomania = ${body.calcomania}`);
    if (body.activo !== undefined) sets.push(Prisma.sql`activo = ${body.activo}`);
    if (sets.length === 0) throw new HttpError(400, "Nada para actualizar");
    await prisma.$executeRaw`
      UPDATE drones_equipos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}
    `;
    const [eq] = await prisma.$queryRaw<EquipoRow[]>`
      SELECT e.*, ge.grupo_id, g.nombre AS grupo_nombre
      FROM drones_equipos e
      LEFT JOIN drones_grupo_equipo ge ON ge.equipo_id = e.id
      LEFT JOIN drones_grupos g ON g.id = ge.grupo_id
      WHERE e.id = ${id}
    `;
    if (!eq) throw new HttpError(404, "Equipo no encontrado");
    res.json({ equipo: eq });
  } catch (e) { next(e); }
});

dronesRouter.delete("/equipos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    await prisma.$executeRaw`UPDATE drones_equipos SET activo = FALSE WHERE id = ${id}`;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// Importar equipos desde crea_inscripciones con categoria DRONES.
// Agrupa por KIT (1 kit = 1 dron = 1 equipo). Idempotente por calcomania.
type InscritoRow = {
  kit_id: number | bigint;
  kit_codigo: string | null;
  nombre_robot: string | null;
  inscripcion_id: number | bigint;
  estudiantes: string;       // comma-separated list of student names
  colegio: string | null;
  modalidad: string;
};

dronesRouter.post("/equipos/importar-inscritos", async (_req, res, next) => {
  try {
    const inscritos = await prisma.$queryRaw<InscritoRow[]>`
      SELECT
        k.id AS kit_id,
        k.codigo AS kit_codigo,
        k.nombre_robot,
        MIN(ci.id) AS inscripcion_id,
        GROUP_CONCAT(
          DISTINCT TRIM(CONCAT_WS(' ', e.nombres, e.apellido_1, e.apellido_2))
          ORDER BY e.id SEPARATOR ', '
        ) AS estudiantes,
        MAX(col.nombre) AS colegio,
        MAX(ci.modalidad) AS modalidad
      FROM kits k
      JOIN crea_inscripciones ci_main ON ci_main.kit_id = k.id
      LEFT JOIN crea_inscripciones ci
        ON ci.kit_id = k.id
        OR (ci_main.pareja_id IS NOT NULL AND ci.pareja_id = ci_main.pareja_id)
      JOIN estudiantes e ON e.id = ci.estudiante_id
      LEFT JOIN colegios col ON col.id = e.colegio_id
      WHERE k.categoria_codigo = 'DRONES'
      GROUP BY k.id, k.codigo, k.nombre_robot
    `;

    let creados = 0;
    let omitidos = 0;

    await prisma.$transaction(async (tx) => {
      for (const r of inscritos) {
        const insId = num(r.inscripcion_id);

        // Idempotencia por calcomania (kit_codigo): 1 kit = 1 equipo
        if (!r.kit_codigo) {
          omitidos++;
          continue;
        }
        const ya = await tx.$queryRaw<{ id: number }[]>`
          SELECT id FROM drones_equipos WHERE calcomania = ${r.kit_codigo} LIMIT 1
        `;
        if (ya.length > 0) {
          omitidos++;
          continue;
        }

        const nombre = r.nombre_robot?.trim()
          ? `${r.nombre_robot.trim()} (${r.kit_codigo ?? "sin código"})`
          : (r.kit_codigo ?? `Inscripción ${insId}`);
        // operador = primer nombre de la lista; miembros = lista completa
        const primero = r.estudiantes.split(",")[0]?.trim() ?? r.estudiantes;

        const nombreFinal = await uniqueNombre(tx, nombre);

        await tx.$executeRaw`
          INSERT INTO drones_equipos
            (nombre, institucion, operador, miembros, calcomania, inscripcion_id, activo)
          VALUES
            (${nombreFinal}, ${r.colegio}, ${primero}, ${r.estudiantes},
             ${r.kit_codigo}, ${insId}, TRUE)
        `;
        creados++;
      }
    });

    res.json({
      total_inscritos: inscritos.length,
      creados,
      omitidos,
    });
  } catch (e) { next(e); }
});

// Equipos elegibles para competir en una ronda:
//   ronda 1 → todos los equipos asignados a algun grupo
//   ronda N>1 → equipos clasificados en el corte cuya ronda_origen = N-1
type EquipoElegibleRow = {
  id: number | bigint;
  nombre: string;
  grupo_id: number | bigint | null;
  grupo_nombre: string | null;
  posicion_corte: number | bigint | null;
  mejor_puntaje: string | null;
};

dronesRouter.get("/equipos/elegibles", async (req, res, next) => {
  try {
    const ronda = z.coerce.number().int().min(1).parse(req.query.ronda);
    const intento = z.coerce.number().int().min(1).optional().parse(req.query.intento);
    const pistaId = z.coerce.number().int().positive().optional().parse(req.query.pistaId);

    const yaCorrieron = (intento && pistaId)
      ? new Set(
          (
            await prisma.$queryRaw<{ equipo_id: number | bigint }[]>`
              SELECT DISTINCT equipo_id FROM drones_recorridos
              WHERE ronda_numero = ${ronda} AND intento = ${intento} AND pista_id = ${pistaId}
            `
          ).map((r) => num(r.equipo_id))
        )
      : new Set<number>();

    if (ronda === 1) {
      const rows = await prisma.$queryRaw<EquipoElegibleRow[]>`
        SELECT e.id, e.nombre,
               ge.grupo_id, g.nombre AS grupo_nombre,
               NULL AS posicion_corte, NULL AS mejor_puntaje
        FROM drones_equipos e
        JOIN drones_grupo_equipo ge ON ge.equipo_id = e.id
        JOIN drones_grupos g ON g.id = ge.grupo_id
        WHERE e.activo = TRUE
        ORDER BY g.orden, g.nombre, e.nombre
      `;
      const data = rows.filter((r) => !yaCorrieron.has(num(r.id)));
      res.json({ ronda, intento: intento ?? null, data });
      return;
    }

    // Buscar el corte que cierra ronda N-1
    const [corte] = await prisma.$queryRaw<{ id: number | bigint }[]>`
      SELECT id FROM drones_cortes
      WHERE ronda_origen = ${ronda - 1}
      ORDER BY numero DESC LIMIT 1
    `;
    if (!corte) {
      throw new HttpError(
        400,
        `No hay corte registrado para cerrar la Ronda ${ronda - 1}. Hacé el corte primero.`
      );
    }
    const rows = await prisma.$queryRaw<EquipoElegibleRow[]>`
      SELECT e.id, e.nombre,
             c.grupo_id, g.nombre AS grupo_nombre,
             c.posicion AS posicion_corte, c.mejor_puntaje
      FROM drones_corte_clasificados c
      JOIN drones_equipos e ON e.id = c.equipo_id
      JOIN drones_grupos g ON g.id = c.grupo_id
      WHERE c.corte_id = ${num(corte.id)}
      ORDER BY g.orden, g.nombre, c.posicion
    `;
    const data = rows.filter((r) => !yaCorrieron.has(num(r.id)));
    res.json({
      ronda,
      intento: intento ?? null,
      corte_origen_id: num(corte.id),
      data,
    });
  } catch (e) { next(e); }
});

async function uniqueNombre(tx: Prisma.TransactionClient, base: string): Promise<string> {
  let candidato = base;
  let i = 2;
  // Reintentar hasta encontrar uno libre
  while (true) {
    const dup = await tx.$queryRaw<{ id: number }[]>`
      SELECT id FROM drones_equipos WHERE nombre = ${candidato} LIMIT 1
    `;
    if (dup.length === 0) return candidato;
    candidato = `${base} #${i}`;
    i++;
    if (i > 50) return `${base} #${Date.now()}`;
  }
}

// =============================================================================
// GRUPOS
// =============================================================================
type GrupoRow = {
  id: number;
  nombre: string;
  pista: string | null;
  es_especial: number;
  orden: number;
  n_equipos: number | bigint;
};

dronesRouter.get("/grupos", async (_req, res, next) => {
  try {
    const rows = await prisma.$queryRaw<GrupoRow[]>`
      SELECT g.id, g.nombre, g.pista, g.es_especial, g.orden,
             COUNT(ge.equipo_id) AS n_equipos
      FROM drones_grupos g
      LEFT JOIN drones_grupo_equipo ge ON ge.grupo_id = g.id
      GROUP BY g.id, g.nombre, g.pista, g.es_especial, g.orden
      ORDER BY g.orden, g.nombre
    `;
    res.json({ data: rows.map((r) => ({ ...r, n_equipos: num(r.n_equipos) })) });
  } catch (e) { next(e); }
});

dronesRouter.get("/grupos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const [grupo] = await prisma.$queryRaw<GrupoRow[]>`
      SELECT g.id, g.nombre, g.pista, g.es_especial, g.orden,
             COUNT(ge.equipo_id) AS n_equipos
      FROM drones_grupos g
      LEFT JOIN drones_grupo_equipo ge ON ge.grupo_id = g.id
      WHERE g.id = ${id}
      GROUP BY g.id, g.nombre, g.pista, g.es_especial, g.orden
    `;
    if (!grupo) throw new HttpError(404, "Grupo no encontrado");
    const equipos = await prisma.$queryRaw<EquipoRow[]>`
      SELECT e.id, e.nombre, e.operador, e.institucion, e.calcomania
      FROM drones_grupo_equipo ge
      JOIN drones_equipos e ON e.id = ge.equipo_id
      WHERE ge.grupo_id = ${id}
      ORDER BY e.nombre
    `;
    res.json({ grupo: { ...grupo, n_equipos: num(grupo.n_equipos) }, equipos });
  } catch (e) { next(e); }
});

const grupoSchema = z.object({
  nombre: z.string().trim().min(1).max(100),
  pista: z.preprocess(emptyToNull, z.string().trim().max(100).nullable()).optional(),
  es_especial: z.boolean().optional(),
  orden: z.coerce.number().int().min(0).optional(),
});

dronesRouter.post("/grupos", async (req, res, next) => {
  try {
    const body = grupoSchema.parse(req.body);
    const dup = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM drones_grupos WHERE nombre = ${body.nombre} LIMIT 1
    `;
    if (dup.length > 0) throw new HttpError(409, "Ya existe un grupo con ese nombre");
    await prisma.$executeRaw`
      INSERT INTO drones_grupos (nombre, pista, es_especial, orden)
      VALUES (${body.nombre}, ${body.pista ?? null},
              ${body.es_especial ?? false}, ${body.orden ?? 0})
    `;
    const [g] = await prisma.$queryRaw<GrupoRow[]>`
      SELECT * FROM drones_grupos WHERE id = LAST_INSERT_ID()
    `;
    res.status(201).json({ grupo: g });
  } catch (e) { next(e); }
});

dronesRouter.patch("/grupos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = grupoSchema.partial().parse(req.body);
    const sets: Prisma.Sql[] = [];
    if (body.nombre !== undefined) sets.push(Prisma.sql`nombre = ${body.nombre}`);
    if (body.pista !== undefined) sets.push(Prisma.sql`pista = ${body.pista}`);
    if (body.es_especial !== undefined) sets.push(Prisma.sql`es_especial = ${body.es_especial}`);
    if (body.orden !== undefined) sets.push(Prisma.sql`orden = ${body.orden}`);
    if (sets.length === 0) throw new HttpError(400, "Nada para actualizar");
    await prisma.$executeRaw`
      UPDATE drones_grupos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}
    `;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

dronesRouter.delete("/grupos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    await prisma.$executeRaw`DELETE FROM drones_grupos WHERE id = ${id}`;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// Asignar equipos a un grupo (manual)
dronesRouter.post("/grupos/:id/equipos", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = z.object({
      equipo_ids: z.array(z.coerce.number().int().positive()).min(1),
      reemplazar: z.boolean().default(false),
    }).parse(req.body);

    await prisma.$transaction(async (tx) => {
      if (body.reemplazar) {
        await tx.$executeRaw`DELETE FROM drones_grupo_equipo WHERE grupo_id = ${id}`;
      }
      // Remover esos equipos de otros grupos antes de re-asignarlos (1 equipo = 1 grupo)
      for (const eid of body.equipo_ids) {
        await tx.$executeRaw`DELETE FROM drones_grupo_equipo WHERE equipo_id = ${eid}`;
        await tx.$executeRaw`
          INSERT IGNORE INTO drones_grupo_equipo (grupo_id, equipo_id)
          VALUES (${id}, ${eid})
        `;
      }
    });
    res.json({ ok: true, asignados: body.equipo_ids.length });
  } catch (e) { next(e); }
});

dronesRouter.delete("/grupos/:id/equipos/:eid", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const eid = idParam.parse(req.params.eid);
    await prisma.$executeRaw`
      DELETE FROM drones_grupo_equipo WHERE grupo_id = ${id} AND equipo_id = ${eid}
    `;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// Reparto aleatorio: distribuye TODOS los equipos activos sin grupo en N grupos
dronesRouter.post("/grupos/sortear", async (req, res, next) => {
  try {
    const body = z.object({
      n_grupos: z.coerce.number().int().min(1).max(50),
      pistas: z.array(z.string()).optional(),
      reemplazar: z.boolean().default(false),
      especial_4: z.boolean().default(false),
      prefijo: z.string().trim().max(50).default("Grupo"),
    }).parse(req.body);

    const equipos = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM drones_equipos WHERE activo = TRUE
    `;
    if (equipos.length === 0) throw new HttpError(400, "No hay equipos activos para sortear");

    // Shuffle Fisher-Yates
    const shuffled = equipos.map((e) => num(e.id));
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }

    await prisma.$transaction(async (tx) => {
      if (body.reemplazar) {
        await tx.$executeRaw`DELETE FROM drones_grupo_equipo`;
        await tx.$executeRaw`DELETE FROM drones_grupos`;
      }
      // Grupo especial primero (toma los 4 primeros aleatorios)
      let pool = shuffled;
      if (body.especial_4) {
        const cuatro = pool.slice(0, 4);
        pool = pool.slice(4);
        await tx.$executeRaw`
          INSERT INTO drones_grupos (nombre, pista, es_especial, orden)
          VALUES (${"Grupo Especial"}, ${body.pistas?.[0] ?? null}, TRUE, 0)
        `;
        const [{ id: especialId }] = await tx.$queryRaw<{ id: number }[]>`
          SELECT LAST_INSERT_ID() AS id
        `;
        for (const eid of cuatro) {
          await tx.$executeRaw`
            INSERT INTO drones_grupo_equipo (grupo_id, equipo_id)
            VALUES (${num(especialId)}, ${eid})
          `;
        }
      }

      // Reparto round-robin en los N grupos normales
      const grupoIds: number[] = [];
      for (let g = 0; g < body.n_grupos; g++) {
        const pista = body.pistas?.[g + (body.especial_4 ? 1 : 0)] ?? null;
        await tx.$executeRaw`
          INSERT INTO drones_grupos (nombre, pista, es_especial, orden)
          VALUES (${body.prefijo + " " + String.fromCharCode(65 + g)},
                  ${pista}, FALSE, ${g + 1})
        `;
        const [{ id }] = await tx.$queryRaw<{ id: number }[]>`SELECT LAST_INSERT_ID() AS id`;
        grupoIds.push(num(id));
      }
      for (let i = 0; i < pool.length; i++) {
        const gid = grupoIds[i % grupoIds.length];
        await tx.$executeRaw`
          INSERT IGNORE INTO drones_grupo_equipo (grupo_id, equipo_id)
          VALUES (${gid}, ${pool[i]})
        `;
      }
    });
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// =============================================================================
// CATÁLOGOS (penalizaciones / bonificaciones) — CRUD COMPLETO
// =============================================================================
type CatRow = {
  id: number;
  codigo: string;
  descripcion: string;
  puntos: number;
  activo: number;
};
const catCreateSchema = z.object({
  codigo: z.string().trim().min(1).max(50),
  descripcion: z.string().trim().min(1).max(255),
  puntos: z.coerce.number().int(),
  activo: z.boolean().default(true),
});
const catUpdateSchema = z.object({
  codigo: z.string().trim().min(1).max(50).optional(),
  descripcion: z.string().trim().min(1).max(255).optional(),
  puntos: z.coerce.number().int().optional(),
  activo: z.boolean().optional(),
  aplicar_existentes: z.boolean().default(false),
});

function buildCatalogoRoutes(tabla: "drones_penalizacion_tipos" | "drones_bonificacion_tipos") {
  return {
    list: async (_req: any, res: any, next: any) => {
      try {
        const rows = tabla === "drones_penalizacion_tipos"
          ? await prisma.$queryRaw<CatRow[]>`SELECT * FROM drones_penalizacion_tipos ORDER BY codigo`
          : await prisma.$queryRaw<CatRow[]>`SELECT * FROM drones_bonificacion_tipos ORDER BY codigo`;
        res.json({ data: rows });
      } catch (e) { next(e); }
    },
    create: async (req: any, res: any, next: any) => {
      try {
        const body = catCreateSchema.parse(req.body);
        if (tabla === "drones_penalizacion_tipos") {
          await prisma.$executeRaw`
            INSERT INTO drones_penalizacion_tipos (codigo, descripcion, puntos, activo)
            VALUES (${body.codigo}, ${body.descripcion}, ${body.puntos}, ${body.activo})
          `;
        } else {
          await prisma.$executeRaw`
            INSERT INTO drones_bonificacion_tipos (codigo, descripcion, puntos, activo)
            VALUES (${body.codigo}, ${body.descripcion}, ${body.puntos}, ${body.activo})
          `;
        }
        res.status(201).json({ ok: true });
      } catch (e) { next(e); }
    },
    update: async (req: any, res: any, next: any) => {
      try {
        const id = idParam.parse(req.params.id);
        const body = catUpdateSchema.parse(req.body);
        const sets: Prisma.Sql[] = [];
        if (body.codigo !== undefined) sets.push(Prisma.sql`codigo = ${body.codigo}`);
        if (body.descripcion !== undefined) sets.push(Prisma.sql`descripcion = ${body.descripcion}`);
        if (body.puntos !== undefined) sets.push(Prisma.sql`puntos = ${body.puntos}`);
        if (body.activo !== undefined) sets.push(Prisma.sql`activo = ${body.activo}`);
        if (sets.length === 0) throw new HttpError(400, "Nada para actualizar");

        if (tabla === "drones_penalizacion_tipos") {
          await prisma.$executeRaw`UPDATE drones_penalizacion_tipos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}`;
        } else {
          await prisma.$executeRaw`UPDATE drones_bonificacion_tipos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}`;
        }

        let recalculados = 0;
        if (body.aplicar_existentes) {
          recalculados = await recomputeAllScores();
        }
        res.json({ ok: true, aplicar_existentes: body.aplicar_existentes, recalculados });
      } catch (e) { next(e); }
    },
    remove: async (req: any, res: any, next: any) => {
      try {
        const id = idParam.parse(req.params.id);
        if (tabla === "drones_penalizacion_tipos") {
          await prisma.$executeRaw`DELETE FROM drones_penalizacion_tipos WHERE id = ${id}`;
        } else {
          await prisma.$executeRaw`DELETE FROM drones_bonificacion_tipos WHERE id = ${id}`;
        }
        await recomputeAllScores();
        res.json({ ok: true });
      } catch (e) { next(e); }
    },
  };
}
const pen = buildCatalogoRoutes("drones_penalizacion_tipos");
const bon = buildCatalogoRoutes("drones_bonificacion_tipos");

// =============================================================================
// PISTAS
// =============================================================================
dronesRouter.get("/pistas", async (_req, res, next) => {
  try {
    const rows = await prisma.$queryRaw<any[]>`
      SELECT * FROM drones_pistas ORDER BY orden, nombre
    `;
    res.json({ data: rows });
  } catch (e) { next(e); }
});

dronesRouter.post("/pistas", async (req, res, next) => {
  try {
    const body = z.object({
      nombre: z.string().trim().min(1).max(100),
      orden: z.coerce.number().int().default(0),
    }).parse(req.body);
    await prisma.$executeRaw`
      INSERT INTO drones_pistas (nombre, orden) VALUES (${body.nombre}, ${body.orden})
    `;
    const [row] = await prisma.$queryRaw<any[]>`SELECT * FROM drones_pistas WHERE id = LAST_INSERT_ID()`;
    res.status(201).json({ pista: row });
  } catch (e) { next(e); }
});

dronesRouter.patch("/pistas/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = z.object({
      nombre: z.string().trim().min(1).max(100).optional(),
      orden: z.coerce.number().int().optional(),
      activa: z.boolean().optional(),
    }).parse(req.body);
    const sets: Prisma.Sql[] = [];
    if (body.nombre !== undefined) sets.push(Prisma.sql`nombre = ${body.nombre}`);
    if (body.orden !== undefined) sets.push(Prisma.sql`orden = ${body.orden}`);
    if (body.activa !== undefined) sets.push(Prisma.sql`activa = ${body.activa}`);
    if (sets.length > 0) {
      await prisma.$executeRaw`UPDATE drones_pistas SET ${Prisma.join(sets, ", ")} WHERE id = ${id}`;
    }
    res.json({ ok: true });
  } catch (e) { next(e); }
});

dronesRouter.delete("/pistas/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    await prisma.$executeRaw`DELETE FROM drones_pistas WHERE id = ${id}`;
    res.json({ ok: true });
  } catch (e) { next(e); }
});
dronesRouter.get("/catalogos/penalizaciones", pen.list);
dronesRouter.post("/catalogos/penalizaciones", pen.create);
dronesRouter.patch("/catalogos/penalizaciones/:id", pen.update);
dronesRouter.delete("/catalogos/penalizaciones/:id", pen.remove);
dronesRouter.get("/catalogos/bonificaciones", bon.list);
dronesRouter.post("/catalogos/bonificaciones", bon.create);
dronesRouter.patch("/catalogos/bonificaciones/:id", bon.update);
dronesRouter.delete("/catalogos/bonificaciones/:id", bon.remove);

// =============================================================================
// RECORRIDOS
// =============================================================================
const penAplSchema = z.object({
  tipo_id: z.coerce.number().int().positive(),
  cantidad: z.coerce.number().int().positive().default(1),
});
const violacionSchema = z.object({
  codigo: z.string().trim().min(1).max(80),
  descalifica: z.boolean().optional(),
  notas: z.preprocess(emptyToNull, z.string().trim().nullable()).optional(),
});
const recorridoSchema = z.object({
  equipo_id: z.coerce.number().int().positive(),
  grupo_id: z.coerce.number().int().positive(),
  pista_id: z.coerce.number().int().positive(),
  ronda_numero: z.coerce.number().int().min(1).max(20),
  intento: z.coerce.number().int().min(1).max(20),
  tiempo_segundos: z.coerce.number().nonnegative(),
  completado: z.boolean().optional(),
  descalificado: z.boolean().optional(),
  motivo_dq: z.preprocess(emptyToNull, z.string().trim().nullable()).optional(),
  observaciones: z.preprocess(emptyToNull, z.string().trim().nullable()).optional(),
  juez: z.preprocess(emptyToNull, z.string().trim().max(150).nullable()).optional(),
  penalizaciones: z.array(penAplSchema).default([]),
  bonificaciones: z.array(penAplSchema).default([]),
  violaciones: z.array(violacionSchema).default([]),
});

type RecorridoFullRow = {
  id: number;
  equipo_id: number;
  equipo_nombre: string;
  grupo_id: number;
  grupo_nombre: string;
  ronda_numero: number;
  intento: number;
  tiempo_segundos: string;
  completado: number;
  descalificado: number;
  motivo_dq: string | null;
  puntaje_final: string | null;
  ajuste_puntos: string | null;
  ajuste_motivo: string | null;
  observaciones: string | null;
  juez: string | null;
  creado_en: Date;
};

type DesgloseLineaRow = {
  tipo_id: number | bigint;
  codigo: string;
  descripcion: string;
  puntos: number | bigint;
  cantidad: number | bigint;
};

async function fetchRecorrido(id: number): Promise<RecorridoFullRow | null> {
  const [row] = await prisma.$queryRaw<RecorridoFullRow[]>`
    SELECT r.id, r.equipo_id, e.nombre AS equipo_nombre, r.grupo_id, g.nombre AS grupo_nombre,
           r.ronda_numero, r.intento, r.tiempo_segundos, r.completado, r.descalificado,
           r.motivo_dq, r.puntaje_final, r.ajuste_puntos, r.ajuste_motivo,
           r.observaciones, r.juez, r.creado_en
    FROM drones_recorridos r
    JOIN drones_equipos e ON e.id = r.equipo_id
    JOIN drones_grupos g ON g.id = r.grupo_id
    WHERE r.id = ${id}
  `;
  return row ?? null;
}

async function fetchRecorridoConDesglose(id: number) {
  const recorrido = await fetchRecorrido(id);
  if (!recorrido) return null;
  const penalizaciones = await prisma.$queryRaw<DesgloseLineaRow[]>`
    SELECT t.id AS tipo_id, t.codigo, t.descripcion, t.puntos, p.cantidad
    FROM drones_penalizaciones p
    JOIN drones_penalizacion_tipos t ON t.id = p.tipo_id
    WHERE p.recorrido_id = ${id}
  `;
  const bonificaciones = await prisma.$queryRaw<DesgloseLineaRow[]>`
    SELECT t.id AS tipo_id, t.codigo, t.descripcion, t.puntos, b.cantidad
    FROM drones_bonificaciones b
    JOIN drones_bonificacion_tipos t ON t.id = b.tipo_id
    WHERE b.recorrido_id = ${id}
  `;
  return { recorrido, penalizaciones, bonificaciones };
}

dronesRouter.get("/recorridos", async (req, res, next) => {
  try {
    const q = z.object({
      equipoId: z.coerce.number().int().positive().optional(),
      grupoId: z.coerce.number().int().positive().optional(),
      rondaNumero: z.coerce.number().int().positive().optional(),
      soloMejor: z.preprocess((v) => v === "true" || v === "1", z.boolean()).optional(),
    }).parse(req.query);

    const conds: Prisma.Sql[] = [Prisma.sql`1=1`];
    if (q.equipoId) conds.push(Prisma.sql`r.equipo_id = ${q.equipoId}`);
    if (q.grupoId) conds.push(Prisma.sql`r.grupo_id = ${q.grupoId}`);
    if (q.rondaNumero) conds.push(Prisma.sql`r.ronda_numero = ${q.rondaNumero}`);
    const where = Prisma.join(conds, " AND ");

    if (q.soloMejor) {
      const rows = await prisma.$queryRaw<RecorridoFullRow[]>`
        SELECT r.id, r.equipo_id, e.nombre AS equipo_nombre, r.grupo_id, g.nombre AS grupo_nombre,
               r.pista_id, COALESCE(p.nombre, '') AS pista_nombre,
               r.ronda_numero, r.intento, r.tiempo_segundos, r.completado, r.descalificado,
               r.motivo_dq, r.puntaje_final, r.observaciones, r.juez, r.creado_en
        FROM drones_recorridos r
        JOIN drones_equipos e ON e.id = r.equipo_id
        JOIN drones_grupos g ON g.id = r.grupo_id
        LEFT JOIN drones_pistas p ON p.id = r.pista_id
        JOIN (
          SELECT equipo_id, pista_id, MIN(puntaje_final) AS mejor
          FROM drones_recorridos
          WHERE descalificado = FALSE AND puntaje_final IS NOT NULL
          GROUP BY equipo_id, pista_id
        ) best ON best.equipo_id = r.equipo_id AND best.pista_id = r.pista_id AND best.mejor = r.puntaje_final
        WHERE ${where}
        ORDER BY g.orden, r.puntaje_final
      `;
      return res.json({ data: rows });
    }
    const rows = await prisma.$queryRaw<RecorridoFullRow[]>`
      SELECT r.id, r.equipo_id, e.nombre AS equipo_nombre, r.grupo_id, g.nombre AS grupo_nombre,
             r.pista_id, COALESCE(p.nombre, '') AS pista_nombre,
             r.ronda_numero, r.intento, r.tiempo_segundos, r.completado, r.descalificado,
             r.motivo_dq, r.puntaje_final, r.observaciones, r.juez, r.creado_en
      FROM drones_recorridos r
      JOIN drones_equipos e ON e.id = r.equipo_id
      JOIN drones_grupos g ON g.id = r.grupo_id
      LEFT JOIN drones_pistas p ON p.id = r.pista_id
      WHERE ${where}
      ORDER BY g.orden, r.ronda_numero, e.nombre, r.pista_id, r.intento
    `;
    res.json({ data: rows });
  } catch (e) { next(e); }
});

dronesRouter.post("/recorridos", async (req, res, next) => {
  try {
    const body = recorridoSchema.parse(req.body);
    const eq = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM drones_equipos WHERE id = ${body.equipo_id} LIMIT 1
    `;
    if (eq.length === 0) throw new HttpError(404, "Equipo no encontrado");
    const grp = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM drones_grupos WHERE id = ${body.grupo_id} LIMIT 1
    `;
    if (grp.length === 0) throw new HttpError(404, "Grupo no encontrado");

    const dup = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM drones_recorridos
      WHERE equipo_id = ${body.equipo_id}
        AND ronda_numero = ${body.ronda_numero}
        AND pista_id = ${body.pista_id}
        AND intento = ${body.intento}
      LIMIT 1
    `;
    if (dup.length > 0)
      throw new HttpError(409, `Ya existe el intento ${body.intento} en esa pista para la ronda ${body.ronda_numero}`);

    const desc = body.descalificado ?? body.violaciones.some((v) => v.descalifica);

    const recorridoId = await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        INSERT INTO drones_recorridos
          (equipo_id, grupo_id, pista_id, ronda_numero, intento, tiempo_segundos,
           completado, descalificado, motivo_dq, observaciones, juez)
        VALUES
          (${body.equipo_id}, ${body.grupo_id}, ${body.pista_id}, ${body.ronda_numero},
           ${body.intento}, ${body.tiempo_segundos},
           ${body.completado ?? false}, ${desc}, ${body.motivo_dq ?? null},
           ${body.observaciones ?? null}, ${body.juez ?? null})
      `;
      const [{ id }] = await tx.$queryRaw<{ id: number }[]>`SELECT LAST_INSERT_ID() AS id`;
      const newId = num(id);
      for (const p of body.penalizaciones) {
        await tx.$executeRaw`
          INSERT INTO drones_penalizaciones (recorrido_id, tipo_id, cantidad)
          VALUES (${newId}, ${p.tipo_id}, ${p.cantidad})
        `;
      }
      for (const b of body.bonificaciones) {
        await tx.$executeRaw`
          INSERT INTO drones_bonificaciones (recorrido_id, tipo_id, cantidad)
          VALUES (${newId}, ${b.tipo_id}, ${b.cantidad})
        `;
      }
      for (const v of body.violaciones) {
        await tx.$executeRaw`
          INSERT INTO drones_violaciones (recorrido_id, codigo, descalifica, notas)
          VALUES (${newId}, ${v.codigo}, ${v.descalifica ?? false}, ${v.notas ?? null})
        `;
      }
      return newId;
    });
    await recomputeScore(recorridoId);
    const desglose = await fetchRecorridoConDesglose(recorridoId);
    res.status(201).json(desglose ?? { recorrido: null });
  } catch (e) { next(e); }
});

dronesRouter.patch("/recorridos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = recorridoSchema.partial().parse(req.body);
    const sets: Prisma.Sql[] = [];
    if (body.tiempo_segundos !== undefined) sets.push(Prisma.sql`tiempo_segundos = ${body.tiempo_segundos}`);
    if (body.completado !== undefined) sets.push(Prisma.sql`completado = ${body.completado}`);
    if (body.descalificado !== undefined) sets.push(Prisma.sql`descalificado = ${body.descalificado}`);
    if (body.motivo_dq !== undefined) sets.push(Prisma.sql`motivo_dq = ${body.motivo_dq}`);
    if (body.observaciones !== undefined) sets.push(Prisma.sql`observaciones = ${body.observaciones}`);
    if (body.juez !== undefined) sets.push(Prisma.sql`juez = ${body.juez}`);
    if (sets.length > 0) {
      await prisma.$executeRaw`UPDATE drones_recorridos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}`;
    }
    if (body.penalizaciones !== undefined) {
      await prisma.$executeRaw`DELETE FROM drones_penalizaciones WHERE recorrido_id = ${id}`;
      for (const p of body.penalizaciones) {
        await prisma.$executeRaw`
          INSERT INTO drones_penalizaciones (recorrido_id, tipo_id, cantidad)
          VALUES (${id}, ${p.tipo_id}, ${p.cantidad})
        `;
      }
    }
    if (body.bonificaciones !== undefined) {
      await prisma.$executeRaw`DELETE FROM drones_bonificaciones WHERE recorrido_id = ${id}`;
      for (const b of body.bonificaciones) {
        await prisma.$executeRaw`
          INSERT INTO drones_bonificaciones (recorrido_id, tipo_id, cantidad)
          VALUES (${id}, ${b.tipo_id}, ${b.cantidad})
        `;
      }
    }
    if (body.violaciones !== undefined) {
      await prisma.$executeRaw`DELETE FROM drones_violaciones WHERE recorrido_id = ${id}`;
      for (const v of body.violaciones) {
        await prisma.$executeRaw`
          INSERT INTO drones_violaciones (recorrido_id, codigo, descalifica, notas)
          VALUES (${id}, ${v.codigo}, ${v.descalifica ?? false}, ${v.notas ?? null})
        `;
      }
    }
    await recomputeScore(id);
    res.json({ recorrido: await fetchRecorrido(id) });
  } catch (e) { next(e); }
});

dronesRouter.patch("/recorridos/:id/ajuste", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = z
      .object({
        puntos: z.coerce.number(),
        motivo: z
          .preprocess(emptyToNull, z.string().trim().max(255).nullable())
          .optional(),
      })
      .parse(req.body);

    const exists = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM drones_recorridos WHERE id = ${id} LIMIT 1
    `;
    if (exists.length === 0) throw new HttpError(404, "Recorrido no encontrado");

    await prisma.$executeRaw`
      UPDATE drones_recorridos
      SET ajuste_puntos = ${body.puntos}, ajuste_motivo = ${body.motivo ?? null}
      WHERE id = ${id}
    `;
    await recomputeScore(id);
    const desglose = await fetchRecorridoConDesglose(id);
    res.json(desglose ?? { recorrido: null });
  } catch (e) { next(e); }
});

dronesRouter.delete("/recorridos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    await prisma.$executeRaw`DELETE FROM drones_recorridos WHERE id = ${id}`;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// =============================================================================
// CORTES
// =============================================================================
type CorteRow = {
  id: number;
  numero: number;
  ronda_origen: number;
  n_pasan_por_grupo: number;
  es_final: number;
  ejecutado_en: Date;
  observaciones: string | null;
  grupo_destino_id: number | null;
};

dronesRouter.get("/cortes", async (_req, res, next) => {
  try {
    const rows = await prisma.$queryRaw<CorteRow[]>`
      SELECT * FROM drones_cortes ORDER BY numero
    `;
    res.json({ data: rows });
  } catch (e) { next(e); }
});

dronesRouter.get("/cortes/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const [corte] = await prisma.$queryRaw<CorteRow[]>`
      SELECT * FROM drones_cortes WHERE id = ${id}
    `;
    if (!corte) throw new HttpError(404, "Corte no encontrado");
    const clas = await prisma.$queryRaw<any[]>`
      SELECT c.posicion, c.mejor_puntaje,
             c.equipo_id, e.nombre AS equipo_nombre, e.institucion,
             c.grupo_id, g.nombre AS grupo_nombre
      FROM drones_corte_clasificados c
      JOIN drones_equipos e ON e.id = c.equipo_id
      JOIN drones_grupos g ON g.id = c.grupo_id
      WHERE c.corte_id = ${id}
      ORDER BY g.orden, c.posicion
    `;
    res.json({ corte, clasificados: clas });
  } catch (e) { next(e); }
});

dronesRouter.post("/cortes", async (req, res, next) => {
  try {
    const body = z.object({
      ronda_origen: z.coerce.number().int().min(1).max(20),
      n_pasan_por_grupo: z.coerce.number().int().min(1).max(50),
      es_final: z.boolean().default(false),
      observaciones: z.preprocess(emptyToNull, z.string().trim().nullable()).optional(),
      grupo_destino_id: z.coerce.number().int().positive().nullable().default(null),
    }).parse(req.body);

    if (body.grupo_destino_id) {
      const [dest] = await prisma.$queryRaw<{ id: number }[]>`
        SELECT id FROM drones_grupos WHERE id = ${body.grupo_destino_id}
      `;
      if (!dest) throw new HttpError(400, "El grupo destino no existe");
    }

    const [{ next: nextNum }] = await prisma.$queryRaw<{ next: number | bigint }[]>`
      SELECT COALESCE(MAX(numero), 0) + 1 AS next FROM drones_cortes
    `;
    const numero = num(nextNum);

    const corteId = await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        INSERT INTO drones_cortes (numero, ronda_origen, n_pasan_por_grupo, es_final, observaciones, grupo_destino_id)
        VALUES (${numero}, ${body.ronda_origen}, ${body.n_pasan_por_grupo},
                ${body.es_final}, ${body.observaciones ?? null}, ${body.grupo_destino_id})
      `;
      const [{ id }] = await tx.$queryRaw<{ id: number }[]>`SELECT LAST_INSERT_ID() AS id`;
      const cid = num(id);

      const grupos = await tx.$queryRaw<{ id: number }[]>`
        SELECT id FROM drones_grupos WHERE es_especial = FALSE
      `;
      const allClasificados: number[] = [];
      const agg = body.es_final ? "AVG" : "SUM";
      for (const g of grupos) {
        // Puntaje agregado = SUM (o AVG si final) de los mejores puntajes por pista
        const top = await tx.$queryRaw<{
          equipo_id: number;
          puntaje_agg: string;
        }[]>`
          SELECT sub.equipo_id, ${Prisma.raw(agg)}(sub.mejor_pista) AS puntaje_agg
          FROM (
            SELECT r.equipo_id, r.pista_id, MIN(r.puntaje_final) AS mejor_pista
            FROM drones_recorridos r
            WHERE r.grupo_id = ${num(g.id)}
              AND r.ronda_numero = ${body.ronda_origen}
              AND r.descalificado = FALSE
              AND r.puntaje_final IS NOT NULL
            GROUP BY r.equipo_id, r.pista_id
          ) sub
          GROUP BY sub.equipo_id
          ORDER BY puntaje_agg ASC
          LIMIT ${body.n_pasan_por_grupo}
        `;
        let pos = 1;
        for (const t of top) {
          await tx.$executeRaw`
            INSERT INTO drones_corte_clasificados
              (corte_id, equipo_id, grupo_id, posicion, mejor_puntaje)
            VALUES (${cid}, ${num(t.equipo_id)}, ${num(g.id)}, ${pos}, ${t.puntaje_agg})
          `;
          allClasificados.push(num(t.equipo_id));
          pos++;
        }
      }

      // Mover clasificados al grupo destino
      if (body.grupo_destino_id && allClasificados.length > 0) {
        for (const eid of allClasificados) {
          await tx.$executeRaw`
            DELETE FROM drones_grupo_equipo WHERE equipo_id = ${eid}
          `;
          await tx.$executeRaw`
            INSERT INTO drones_grupo_equipo (grupo_id, equipo_id)
            VALUES (${body.grupo_destino_id}, ${eid})
          `;
        }
      }

      if (!body.es_final) {
        await tx.$executeRaw`
          UPDATE drones_competencia SET ronda_actual = ${body.ronda_origen + 1} WHERE id = 1
        `;
      } else {
        await tx.$executeRaw`UPDATE drones_competencia SET finalizada = TRUE WHERE id = 1`;
      }
      return cid;
    });

    res.status(201).json({ corte_id: corteId, numero });
  } catch (e) { next(e); }
});

dronesRouter.delete("/cortes/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    await prisma.$executeRaw`DELETE FROM drones_cortes WHERE id = ${id}`;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// =============================================================================
// RANKING (acumulado por grupo, mejor intento por equipo)
// =============================================================================
dronesRouter.get("/ranking", async (req, res, next) => {
  try {
    const q = z.object({
      grupoId: z.coerce.number().int().positive().optional(),
      rondaNumero: z.coerce.number().int().positive().optional(),
    }).parse(req.query);

    const conds: Prisma.Sql[] = [Prisma.sql`r.descalificado = FALSE AND r.puntaje_final IS NOT NULL`];
    if (q.grupoId) conds.push(Prisma.sql`r.grupo_id = ${q.grupoId}`);
    if (q.rondaNumero) conds.push(Prisma.sql`r.ronda_numero = ${q.rondaNumero}`);
    const where = Prisma.join(conds, " AND ");

    // Ranking = SUM of best puntaje per pista per equipo
    const rows = await prisma.$queryRaw<any[]>`
      SELECT e.id AS equipo_id, e.nombre AS equipo_nombre, e.institucion,
             g.id AS grupo_id, g.nombre AS grupo_nombre,
             COUNT(DISTINCT r.id) AS intentos,
             (
               SELECT SUM(sub.mejor_pista)
               FROM (
                 SELECT r2.pista_id, MIN(r2.puntaje_final) AS mejor_pista
                 FROM drones_recorridos r2
                 WHERE r2.equipo_id = e.id
                   AND r2.descalificado = FALSE
                   AND r2.puntaje_final IS NOT NULL
                   ${q.rondaNumero ? Prisma.sql`AND r2.ronda_numero = ${q.rondaNumero}` : Prisma.empty}
                 GROUP BY r2.pista_id
               ) sub
             ) AS mejor_puntaje
      FROM drones_equipos e
      LEFT JOIN drones_grupo_equipo ge ON ge.equipo_id = e.id
      LEFT JOIN drones_grupos g ON g.id = ge.grupo_id
      LEFT JOIN drones_recorridos r ON r.equipo_id = e.id AND ${where}
      WHERE e.activo = TRUE
      GROUP BY e.id, e.nombre, e.institucion, g.id, g.nombre
      ORDER BY g.orden, mejor_puntaje IS NULL, mejor_puntaje ASC, e.nombre
    `;
    res.json({
      data: rows.map((r, i) => ({
        posicion: i + 1,
        ...r,
        intentos: num(r.intentos),
      })),
    });
  } catch (e) { next(e); }
});

// =============================================================================
// METRICAS — agregaciones para dashboard
// =============================================================================
dronesRouter.get("/metricas", async (_req, res, next) => {
  try {
    // Resumen global
    const [resumen] = await prisma.$queryRaw<
      {
        total_equipos: number | bigint;
        total_recorridos: number | bigint;
        descalificados: number | bigint;
        tiempo_promedio: string | null;
        puntaje_promedio: string | null;
      }[]
    >`
      SELECT
        (SELECT COUNT(*) FROM drones_equipos WHERE activo = TRUE) AS total_equipos,
        (SELECT COUNT(*) FROM drones_recorridos) AS total_recorridos,
        (SELECT COUNT(*) FROM drones_recorridos WHERE descalificado = TRUE) AS descalificados,
        (SELECT AVG(tiempo_segundos) FROM drones_recorridos WHERE descalificado = FALSE) AS tiempo_promedio,
        (SELECT AVG(puntaje_final) FROM drones_recorridos WHERE puntaje_final IS NOT NULL) AS puntaje_promedio
    `;

    // Tiempo mínimo absoluto (mejor récord)
    const [tMin] = await prisma.$queryRaw<
      { tiempo_segundos: string | null; equipo_nombre: string | null; ronda_numero: number | bigint | null }[]
    >`
      SELECT r.tiempo_segundos, e.nombre AS equipo_nombre, r.ronda_numero
      FROM drones_recorridos r
      JOIN drones_equipos e ON e.id = r.equipo_id
      WHERE r.descalificado = FALSE AND r.tiempo_segundos > 0
      ORDER BY r.tiempo_segundos ASC LIMIT 1
    `;

    // Mejor puntaje absoluto
    const [pMin] = await prisma.$queryRaw<
      { puntaje_final: string | null; equipo_nombre: string | null; ronda_numero: number | bigint | null }[]
    >`
      SELECT r.puntaje_final, e.nombre AS equipo_nombre, r.ronda_numero
      FROM drones_recorridos r
      JOIN drones_equipos e ON e.id = r.equipo_id
      WHERE r.puntaje_final IS NOT NULL
      ORDER BY r.puntaje_final ASC LIMIT 1
    `;

    // Promedio del mejor intento por grupo
    const porGrupo = await prisma.$queryRaw<
      {
        grupo_id: number | bigint;
        grupo_nombre: string;
        equipos: number | bigint;
        tiempo_min: string | null;
        puntaje_promedio_mejor: string | null;
      }[]
    >`
      SELECT
        g.id AS grupo_id,
        g.nombre AS grupo_nombre,
        COUNT(DISTINCT r.equipo_id) AS equipos,
        MIN(r.tiempo_segundos) AS tiempo_min,
        AVG(mejor.mejor_puntaje) AS puntaje_promedio_mejor
      FROM drones_grupos g
      LEFT JOIN drones_recorridos r
        ON r.grupo_id = g.id AND r.descalificado = FALSE
      LEFT JOIN (
        SELECT grupo_id, equipo_id, MIN(puntaje_final) AS mejor_puntaje
        FROM drones_recorridos
        WHERE descalificado = FALSE AND puntaje_final IS NOT NULL
        GROUP BY grupo_id, equipo_id
      ) AS mejor ON mejor.grupo_id = g.id
      GROUP BY g.id, g.nombre
      ORDER BY g.orden, g.nombre
    `;

    // Por ronda
    const porRonda = await prisma.$queryRaw<
      {
        ronda: number | bigint;
        recorridos: number | bigint;
        tiempo_min: string | null;
        puntaje_promedio: string | null;
      }[]
    >`
      SELECT
        ronda_numero AS ronda,
        COUNT(*) AS recorridos,
        MIN(tiempo_segundos) AS tiempo_min,
        AVG(puntaje_final) AS puntaje_promedio
      FROM drones_recorridos
      WHERE descalificado = FALSE
      GROUP BY ronda_numero
      ORDER BY ronda_numero
    `;

    // Top equipos por bonificaciones aplicadas
    const masBonificaciones = await prisma.$queryRaw<
      { equipo_nombre: string; veces: number | bigint }[]
    >`
      SELECT e.nombre AS equipo_nombre, SUM(b.cantidad) AS veces
      FROM drones_bonificaciones b
      JOIN drones_recorridos r ON r.id = b.recorrido_id
      JOIN drones_equipos e ON e.id = r.equipo_id
      GROUP BY e.id, e.nombre
      ORDER BY veces DESC LIMIT 5
    `;

    // Top equipos por penalizaciones aplicadas
    const masPenalizaciones = await prisma.$queryRaw<
      { equipo_nombre: string; veces: number | bigint }[]
    >`
      SELECT e.nombre AS equipo_nombre, SUM(p.cantidad) AS veces
      FROM drones_penalizaciones p
      JOIN drones_recorridos r ON r.id = p.recorrido_id
      JOIN drones_equipos e ON e.id = r.equipo_id
      GROUP BY e.id, e.nombre
      ORDER BY veces DESC LIMIT 5
    `;

    // Apariciones en top de cortes (clasificó)
    const masClasifico = await prisma.$queryRaw<
      { equipo_nombre: string; veces: number | bigint }[]
    >`
      SELECT e.nombre AS equipo_nombre, COUNT(*) AS veces
      FROM drones_corte_clasificados c
      JOIN drones_equipos e ON e.id = c.equipo_id
      GROUP BY e.id, e.nombre
      ORDER BY veces DESC LIMIT 5
    `;

    res.json({
      global: {
        total_equipos: Number(resumen?.total_equipos ?? 0),
        total_recorridos: Number(resumen?.total_recorridos ?? 0),
        descalificados: Number(resumen?.descalificados ?? 0),
        tiempo_promedio: resumen?.tiempo_promedio ? Number(resumen.tiempo_promedio) : null,
        puntaje_promedio: resumen?.puntaje_promedio ? Number(resumen.puntaje_promedio) : null,
        tiempo_minimo: tMin
          ? {
              valor: tMin.tiempo_segundos ? Number(tMin.tiempo_segundos) : null,
              equipo: tMin.equipo_nombre,
              ronda: tMin.ronda_numero ? Number(tMin.ronda_numero) : null,
            }
          : null,
        puntaje_minimo: pMin
          ? {
              valor: pMin.puntaje_final ? Number(pMin.puntaje_final) : null,
              equipo: pMin.equipo_nombre,
              ronda: pMin.ronda_numero ? Number(pMin.ronda_numero) : null,
            }
          : null,
      },
      por_grupo: porGrupo.map((g) => ({
        grupo_id: Number(g.grupo_id),
        grupo_nombre: g.grupo_nombre,
        equipos: Number(g.equipos),
        tiempo_min: g.tiempo_min ? Number(g.tiempo_min) : null,
        puntaje_promedio_mejor: g.puntaje_promedio_mejor ? Number(g.puntaje_promedio_mejor) : null,
      })),
      por_ronda: porRonda.map((r) => ({
        ronda: Number(r.ronda),
        recorridos: Number(r.recorridos),
        tiempo_min: r.tiempo_min ? Number(r.tiempo_min) : null,
        puntaje_promedio: r.puntaje_promedio ? Number(r.puntaje_promedio) : null,
      })),
      rankings: {
        mas_bonificaciones: masBonificaciones.map((r) => ({
          equipo: r.equipo_nombre,
          veces: Number(r.veces),
        })),
        mas_penalizaciones: masPenalizaciones.map((r) => ({
          equipo: r.equipo_nombre,
          veces: Number(r.veces),
        })),
        mas_clasifico: masClasifico.map((r) => ({
          equipo: r.equipo_nombre,
          veces: Number(r.veces),
        })),
      },
    });
  } catch (e) { next(e); }
});
