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 "./todoterreno.scoring.js";

export const todoterrenoRouter = Router();
todoterrenoRouter.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;
};

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

todoterrenoRouter.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 todoterreno_competencia SET ${Prisma.join(sets, ", ")} WHERE id = 1
    `;
    const [c] = await prisma.$queryRaw<CompetenciaRow[]>`
      SELECT * FROM todoterreno_competencia WHERE id = 1
    `;
    res.json({ competencia: c });
  } 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;
};

todoterrenoRouter.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(),
    }).parse(req.query);

    const conds: Prisma.Sql[] = [Prisma.sql`1=1`];
    if (q.soloActivos) conds.push(Prisma.sql`e.activo = TRUE`);
    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
      FROM todoterreno_equipos e
      WHERE ${where}
      ORDER BY e.nombre
    `;
    res.json({ data: rows });
  } catch (e) { next(e); }
});

todoterrenoRouter.post("/equipos", async (req, res, next) => {
  try {
    const body = equipoSchema.parse(req.body);
    const dup = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM todoterreno_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 todoterreno_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.* FROM todoterreno_equipos e WHERE id = LAST_INSERT_ID()
    `;
    res.status(201).json({ equipo: created });
  } catch (e) { next(e); }
});

todoterrenoRouter.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 todoterreno_equipos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}
    `;
    const [eq] = await prisma.$queryRaw<EquipoRow[]>`
      SELECT e.* FROM todoterreno_equipos e WHERE e.id = ${id}
    `;
    if (!eq) throw new HttpError(404, "Equipo no encontrado");
    res.json({ equipo: eq });
  } catch (e) { next(e); }
});

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

// Importar equipos desde crea_inscripciones con categoria TODO_TERRENO.
// Agrupa por KIT (1 kit = 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;
  colegio: string | null;
  modalidad: string;
};

todoterrenoRouter.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 = 'TODO_TERRENO'
      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);

        if (!r.kit_codigo) {
          omitidos++;
          continue;
        }
        const ya = await tx.$queryRaw<{ id: number }[]>`
          SELECT id FROM todoterreno_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}`);
        const primero = r.estudiantes.split(",")[0]?.trim() ?? r.estudiantes;

        const nombreFinal = await uniqueNombre(tx, nombre);

        await tx.$executeRaw`
          INSERT INTO todoterreno_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); }
});

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

// =============================================================================
// 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: "todoterreno_penalizacion_tipos" | "todoterreno_bonificacion_tipos") {
  return {
    list: async (_req: any, res: any, next: any) => {
      try {
        const rows = tabla === "todoterreno_penalizacion_tipos"
          ? await prisma.$queryRaw<CatRow[]>`SELECT * FROM todoterreno_penalizacion_tipos ORDER BY codigo`
          : await prisma.$queryRaw<CatRow[]>`SELECT * FROM todoterreno_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 === "todoterreno_penalizacion_tipos") {
          await prisma.$executeRaw`
            INSERT INTO todoterreno_penalizacion_tipos (codigo, descripcion, puntos, activo)
            VALUES (${body.codigo}, ${body.descripcion}, ${body.puntos}, ${body.activo})
          `;
        } else {
          await prisma.$executeRaw`
            INSERT INTO todoterreno_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 === "todoterreno_penalizacion_tipos") {
          await prisma.$executeRaw`UPDATE todoterreno_penalizacion_tipos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}`;
        } else {
          await prisma.$executeRaw`UPDATE todoterreno_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 === "todoterreno_penalizacion_tipos") {
          await prisma.$executeRaw`DELETE FROM todoterreno_penalizacion_tipos WHERE id = ${id}`;
        } else {
          await prisma.$executeRaw`DELETE FROM todoterreno_bonificacion_tipos WHERE id = ${id}`;
        }
        await recomputeAllScores();
        res.json({ ok: true });
      } catch (e) { next(e); }
    },
  };
}
const pen = buildCatalogoRoutes("todoterreno_penalizacion_tipos");
const bon = buildCatalogoRoutes("todoterreno_bonificacion_tipos");
todoterrenoRouter.get("/catalogos/penalizaciones", pen.list);
todoterrenoRouter.post("/catalogos/penalizaciones", pen.create);
todoterrenoRouter.patch("/catalogos/penalizaciones/:id", pen.update);
todoterrenoRouter.delete("/catalogos/penalizaciones/:id", pen.remove);
todoterrenoRouter.get("/catalogos/bonificaciones", bon.list);
todoterrenoRouter.post("/catalogos/bonificaciones", bon.create);
todoterrenoRouter.patch("/catalogos/bonificaciones/:id", bon.update);
todoterrenoRouter.delete("/catalogos/bonificaciones/:id", bon.remove);

// =============================================================================
// RECORRIDOS (sin grupo_id, sin violaciones)
// =============================================================================
const penAplSchema = z.object({
  tipo_id: z.coerce.number().int().positive(),
  cantidad: z.coerce.number().int().positive().default(1),
});
const recorridoSchema = z.object({
  equipo_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([]),
});

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

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.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 todoterreno_recorridos r
    JOIN todoterreno_equipos e ON e.id = r.equipo_id
    WHERE r.id = ${id}
  `;
  return row ?? null;
}

todoterrenoRouter.get("/recorridos", async (req, res, next) => {
  try {
    const q = z.object({
      equipoId: 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.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.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 todoterreno_recorridos r
        JOIN todoterreno_equipos e ON e.id = r.equipo_id
        JOIN (
          SELECT equipo_id, MIN(puntaje_final) AS mejor
          FROM todoterreno_recorridos
          WHERE descalificado = FALSE AND puntaje_final IS NOT NULL
          GROUP BY equipo_id
        ) best ON best.equipo_id = r.equipo_id AND best.mejor = r.puntaje_final
        WHERE ${where}
        ORDER BY 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.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 todoterreno_recorridos r
      JOIN todoterreno_equipos e ON e.id = r.equipo_id
      WHERE ${where}
      ORDER BY r.ronda_numero, e.nombre, r.intento
    `;
    res.json({ data: rows });
  } catch (e) { next(e); }
});

todoterrenoRouter.get("/recorridos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const recorrido = await fetchRecorrido(id);
    if (!recorrido) throw new HttpError(404, "Recorrido no encontrado");
    const penalizaciones = await prisma.$queryRaw<{ tipo_id: number; cantidad: number }[]>`
      SELECT tipo_id, cantidad FROM todoterreno_penalizaciones WHERE recorrido_id = ${id}
    `;
    const bonificaciones = await prisma.$queryRaw<{ tipo_id: number; cantidad: number }[]>`
      SELECT tipo_id, cantidad FROM todoterreno_bonificaciones WHERE recorrido_id = ${id}
    `;
    res.json({
      recorrido,
      penalizaciones: penalizaciones.map((p) => ({ tipo_id: num(p.tipo_id), cantidad: num(p.cantidad) })),
      bonificaciones: bonificaciones.map((b) => ({ tipo_id: num(b.tipo_id), cantidad: num(b.cantidad) })),
    });
  } catch (e) { next(e); }
});

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

    // No se puede registrar en una ronda inexistente más allá de la siguiente.
    // Rondas válidas: las que ya existen + la siguiente nueva (o la ronda actual).
    const [{ maxr }] = await prisma.$queryRaw<{ maxr: number | bigint | null }[]>`
      SELECT MAX(ronda_numero) AS maxr FROM todoterreno_recorridos
    `;
    const [comp] = await prisma.$queryRaw<{ ronda_actual: number | bigint }[]>`
      SELECT ronda_actual FROM todoterreno_competencia WHERE id = 1
    `;
    const maxExistente = maxr == null ? 0 : num(maxr);
    const limiteRonda = Math.max(maxExistente + 1, num(comp?.ronda_actual ?? 1));
    if (body.ronda_numero > limiteRonda)
      throw new HttpError(
        400,
        `No podés saltar a la ronda ${body.ronda_numero}. La siguiente ronda disponible es la ${limiteRonda}.`
      );

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

    const recorridoId = await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        INSERT INTO todoterreno_recorridos
          (equipo_id, ronda_numero, intento, tiempo_segundos,
           completado, descalificado, motivo_dq, observaciones, juez)
        VALUES
          (${body.equipo_id}, ${body.ronda_numero},
           ${body.intento}, ${body.tiempo_segundos},
           ${body.completado ?? false}, ${body.descalificado ?? false}, ${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 todoterreno_penalizaciones (recorrido_id, tipo_id, cantidad)
          VALUES (${newId}, ${p.tipo_id}, ${p.cantidad})
        `;
      }
      for (const b of body.bonificaciones) {
        await tx.$executeRaw`
          INSERT INTO todoterreno_bonificaciones (recorrido_id, tipo_id, cantidad)
          VALUES (${newId}, ${b.tipo_id}, ${b.cantidad})
        `;
      }
      return newId;
    });
    await recomputeScore(recorridoId);
    res.status(201).json({ recorrido: await fetchRecorrido(recorridoId) });
  } catch (e) { next(e); }
});

todoterrenoRouter.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 todoterreno_recorridos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}`;
    }
    if (body.penalizaciones !== undefined) {
      await prisma.$executeRaw`DELETE FROM todoterreno_penalizaciones WHERE recorrido_id = ${id}`;
      for (const p of body.penalizaciones) {
        await prisma.$executeRaw`
          INSERT INTO todoterreno_penalizaciones (recorrido_id, tipo_id, cantidad)
          VALUES (${id}, ${p.tipo_id}, ${p.cantidad})
        `;
      }
    }
    if (body.bonificaciones !== undefined) {
      await prisma.$executeRaw`DELETE FROM todoterreno_bonificaciones WHERE recorrido_id = ${id}`;
      for (const b of body.bonificaciones) {
        await prisma.$executeRaw`
          INSERT INTO todoterreno_bonificaciones (recorrido_id, tipo_id, cantidad)
          VALUES (${id}, ${b.tipo_id}, ${b.cantidad})
        `;
      }
    }
    await recomputeScore(id);
    res.json({ recorrido: await fetchRecorrido(id) });
  } catch (e) { next(e); }
});

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

// =============================================================================
// CORTES (top-N global, sin grupos)
// =============================================================================
type CorteRow = {
  id: number;
  numero: number;
  ronda_origen: number;
  n_pasan_global: number;
  es_final: number;
  ejecutado_en: Date;
  observaciones: string | null;
};

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

todoterrenoRouter.get("/cortes/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const [corte] = await prisma.$queryRaw<CorteRow[]>`
      SELECT * FROM todoterreno_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
      FROM todoterreno_corte_clasificados c
      JOIN todoterreno_equipos e ON e.id = c.equipo_id
      WHERE c.corte_id = ${id}
      ORDER BY c.posicion
    `;
    res.json({ corte, clasificados: clas });
  } catch (e) { next(e); }
});

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

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

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

      // Top N GLOBAL por mejor puntaje en la ronda_origen
      const top = await tx.$queryRaw<{
        equipo_id: number;
        mejor: string;
      }[]>`
        SELECT r.equipo_id, MIN(r.puntaje_final) AS mejor
        FROM todoterreno_recorridos r
        WHERE r.ronda_numero = ${body.ronda_origen}
          AND r.descalificado = FALSE
          AND r.puntaje_final IS NOT NULL
        GROUP BY r.equipo_id
        ORDER BY mejor ASC
        LIMIT ${body.n_pasan_global}
      `;
      let pos = 1;
      for (const t of top) {
        await tx.$executeRaw`
          INSERT INTO todoterreno_corte_clasificados
            (corte_id, equipo_id, posicion, mejor_puntaje)
          VALUES (${cid}, ${num(t.equipo_id)}, ${pos}, ${t.mejor})
        `;
        pos++;
      }

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

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

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

// =============================================================================
// RONDAS (rondas reales con recorridos registrados)
// =============================================================================
todoterrenoRouter.get("/rondas", async (_req, res, next) => {
  try {
    const rows = await prisma.$queryRaw<{ ronda_numero: number | bigint }[]>`
      SELECT DISTINCT ronda_numero
      FROM todoterreno_recorridos
      ORDER BY ronda_numero
    `;
    res.json({ data: rows.map((r) => num(r.ronda_numero)) });
  } catch (e) { next(e); }
});

// =============================================================================
// RANKING (global, sin agrupar por grupo)
// =============================================================================
todoterrenoRouter.get("/ranking", async (req, res, next) => {
  try {
    const q = z.object({
      rondas: z.string().trim().optional(),
      soloMejor: z.preprocess((v) => v === "true" || v === "1", z.boolean()).optional(),
    }).parse(req.query);

    const rondas = q.rondas
      ? q.rondas
          .split(",")
          .map((s) => Number(s.trim()))
          .filter((n) => Number.isInteger(n) && n > 0)
      : [];

    const conds: Prisma.Sql[] = [Prisma.sql`r.descalificado = FALSE AND r.puntaje_final IS NOT NULL`];
    if (rondas.length > 0) conds.push(Prisma.sql`r.ronda_numero IN (${Prisma.join(rondas)})`);
    const where = Prisma.join(conds, " AND ");

    // Una fila por recorrido válido (todos los intentos), ordenados por tiempo final.
    let rows = await prisma.$queryRaw<any[]>`
      SELECT r.id AS recorrido_id, e.id AS equipo_id, e.nombre AS equipo_nombre, e.institucion,
             r.ronda_numero, r.intento, r.tiempo_segundos, r.puntaje_final
      FROM todoterreno_recorridos r
      JOIN todoterreno_equipos e ON e.id = r.equipo_id
      WHERE ${where}
      ORDER BY r.puntaje_final ASC, e.nombre, r.ronda_numero, r.intento
    `;

    // "Filtrar por el mejor": deja solo el mejor intento por equipo.
    // Como ya viene ordenado por puntaje ascendente, el primero de cada equipo es su mejor.
    if (q.soloMejor) {
      const vistos = new Set<number>();
      rows = rows.filter((r) => {
        const eid = num(r.equipo_id);
        if (vistos.has(eid)) return false;
        vistos.add(eid);
        return true;
      });
    }

    res.json({
      data: rows.map((r, i) => ({
        posicion: i + 1,
        ...r,
        ronda_numero: num(r.ronda_numero),
        intento: num(r.intento),
      })),
    });
  } catch (e) { next(e); }
});

// =============================================================================
// RESET — borra todos los datos operativos. Mantiene catálogos y nombre/intentos.
// =============================================================================
// Requiere body { confirmar: "BORRAR_TODO" } como salvaguarda extra.
todoterrenoRouter.post("/reset", async (req, res, next) => {
  try {
    const body = z.object({
      confirmar: z.literal("BORRAR_TODO"),
    }).parse(req.body);
    void body;

    const result = await prisma.$transaction(async (tx) => {
      // Conteos pre-borrado para reportar al usuario
      const [{ equipos }] = await tx.$queryRaw<{ equipos: bigint }[]>`
        SELECT COUNT(*) AS equipos FROM todoterreno_equipos
      `;
      const [{ recorridos }] = await tx.$queryRaw<{ recorridos: bigint }[]>`
        SELECT COUNT(*) AS recorridos FROM todoterreno_recorridos
      `;
      const [{ cortes }] = await tx.$queryRaw<{ cortes: bigint }[]>`
        SELECT COUNT(*) AS cortes FROM todoterreno_cortes
      `;

      // Las FK con ON DELETE CASCADE limpian las tablas hijas automáticamente,
      // pero borramos explícitamente para evitar sorpresas si cambia el schema.
      await tx.$executeRaw`DELETE FROM todoterreno_corte_clasificados`;
      await tx.$executeRaw`DELETE FROM todoterreno_cortes`;
      await tx.$executeRaw`DELETE FROM todoterreno_bonificaciones`;
      await tx.$executeRaw`DELETE FROM todoterreno_penalizaciones`;
      await tx.$executeRaw`DELETE FROM todoterreno_recorridos`;
      await tx.$executeRaw`DELETE FROM todoterreno_equipos`;

      // Reinicia el estado de la competencia, conserva nombre e intentos por ronda.
      await tx.$executeRaw`
        UPDATE todoterreno_competencia
        SET ronda_actual = 1, finalizada = FALSE
        WHERE id = 1
      `;

      return {
        equipos: num(equipos),
        recorridos: num(recorridos),
        cortes: num(cortes),
      };
    });

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