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 {
  allianceCode,
  balancedSchoolOrder,
  blockMembership,
  buildBlockSchedule,
  CRUCES_R10,
  playOrder,
  rotationPositions,
  round2,
  scoreAlianza,
  snakeDistribution,
  TOTAL_KITS,
  type Multiplicador,
} from "./stemsr.engine.js";

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

const num = (v: unknown): number =>
  typeof v === "bigint" ? Number(v) : Number(v);
const bool = (v: unknown): boolean => !!num(v);
const idParam = z.coerce.number().int().positive();
const BLOQUES = [1, 2] as const;

// =============================================================================
// COMPETENCIA
// =============================================================================
type CompetenciaRow = {
  id: number;
  nombre: string;
  fase_actual: string;
  plan_generado_en: Date | null;
};

stemsrRouter.get("/competencia", async (_req, res, next) => {
  try {
    const [c] = await prisma.$queryRaw<CompetenciaRow[]>`
      SELECT id, nombre, fase_actual, plan_generado_en
      FROM stemsr_competencia WHERE id = 1
    `;
    const [{ n: participantes }] = await prisma.$queryRaw<{ n: bigint }[]>`
      SELECT COUNT(*) AS n FROM stemsr_participantes
    `;
    res.json({
      competencia: {
        ...c,
        plan_generado: !!c.plan_generado_en,
        participantes: num(participantes),
      },
    });
  } catch (e) { next(e); }
});

// =============================================================================
// GENERAR PLAN (fase de grupos)
// Idempotente: borra y reconstruye todo. Importa los kits STEM_SR ordenados
// por código; completa con placeholders si hay menos de 54.
// =============================================================================
type KitRow = {
  kit_id: number | bigint;
  kit_codigo: string | null;
  nombre_robot: string | null;
  estudiantes: string | null;
  colegio: string | null;
};

type ParticipanteSeed = {
  seed_index: number;
  kit_id: number | null;
  kit_codigo: string | null;
  nombre_robot: string | null;
  estudiantes: string | null;
  colegio: string | null;
  es_placeholder: boolean;
};

stemsrRouter.post("/generar", async (_req, res, next) => {
  try {
    const reales = await prisma.$queryRaw<KitRow[]>`
      SELECT
        k.id AS kit_id,
        k.codigo AS kit_codigo,
        k.nombre_robot,
        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
      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 = 'STEM_SR'
      GROUP BY k.id, k.codigo, k.nombre_robot
      ORDER BY k.codigo
    `;

    // 54 slots (kits reales + placeholders), todavía sin seed_index.
    type Slot = Omit<ParticipanteSeed, "seed_index">;
    const slots: Slot[] = [];
    for (let i = 0; i < TOTAL_KITS; i++) {
      const r = reales[i];
      if (r) {
        slots.push({
          kit_id: num(r.kit_id),
          kit_codigo: r.kit_codigo,
          nombre_robot: r.nombre_robot,
          estudiantes: r.estudiantes,
          colegio: r.colegio,
          es_placeholder: false,
        });
      } else {
        slots.push({
          kit_id: null,
          kit_codigo: null,
          nombre_robot: `Kit ${String(i + 1).padStart(2, "0")}`,
          estudiantes: null,
          colegio: null,
          es_placeholder: true,
        });
      }
    }
    // Reparte por colegio para que el bloque 1 quede diverso (no clusters del
    // mismo colegio). El seed_index resultante alimenta el rebaraje del bloque 2.
    const ordenados = balancedSchoolOrder(slots, (s) => s.colegio);
    const participantes: ParticipanteSeed[] = ordenados.map((s, i) => ({
      seed_index: i,
      ...s,
    }));
    const placeholders = participantes.filter((p) => p.es_placeholder).length;
    const schedule = buildBlockSchedule();

    await prisma.$transaction(
      async (tx) => {
        await tx.$executeRaw`DELETE FROM stemsr_penalizaciones`;
        await tx.$executeRaw`DELETE FROM stemsr_partida_alianza`;
        await tx.$executeRaw`DELETE FROM stemsr_partidas`;
        await tx.$executeRaw`DELETE FROM stemsr_rondas`;
        await tx.$executeRaw`DELETE FROM stemsr_miembros`;
        await tx.$executeRaw`DELETE FROM stemsr_alianzas`;
        await tx.$executeRaw`DELETE FROM stemsr_participantes`;

        // Participantes
        await tx.$executeRaw(Prisma.sql`
          INSERT INTO stemsr_participantes
            (seed_index, kit_id, kit_codigo, nombre_robot, estudiantes, colegio, es_placeholder)
          VALUES ${Prisma.join(
            participantes.map(
              (p) => Prisma.sql`(${p.seed_index}, ${p.kit_id}, ${p.kit_codigo}, ${p.nombre_robot}, ${p.estudiantes}, ${p.colegio}, ${p.es_placeholder})`
            )
          )}
        `);
        const partRows = await tx.$queryRaw<{ id: number; seed_index: number }[]>`
          SELECT id, seed_index FROM stemsr_participantes
        `;
        const partBySeed = new Map(
          partRows.map((r) => [num(r.seed_index), num(r.id)])
        );

        // Alianzas (2 bloques x 9)
        const alianzaTuples = [];
        for (const b of BLOQUES) {
          for (let i = 0; i < 9; i++) {
            alianzaTuples.push(Prisma.sql`('grupos', ${b}, ${allianceCode(i)})`);
          }
        }
        await tx.$executeRaw(Prisma.sql`
          INSERT INTO stemsr_alianzas (fase, bloque, codigo)
          VALUES ${Prisma.join(alianzaTuples)}
        `);
        const aSel = await tx.$queryRaw<{ id: number; bloque: number; codigo: string }[]>`
          SELECT id, bloque, codigo FROM stemsr_alianzas WHERE fase = 'grupos'
        `;
        const alianzaId = new Map(
          aSel.map((r) => [`${num(r.bloque)}:${r.codigo}`, num(r.id)])
        );

        // Miembros (108)
        const miembroTuples = [];
        for (const b of BLOQUES) {
          for (const m of blockMembership(b)) {
            miembroTuples.push(
              Prisma.sql`(${alianzaId.get(`${b}:${m.codigo}`)}, ${m.posicion}, ${partBySeed.get(m.seedIndex)})`
            );
          }
        }
        await tx.$executeRaw(Prisma.sql`
          INSERT INTO stemsr_miembros (alianza_id, posicion, participante_id)
          VALUES ${Prisma.join(miembroTuples)}
        `);

        // Rondas (18)
        const rondaTuples = [];
        for (const b of BLOQUES) {
          for (const r of schedule) {
            rondaTuples.push(
              Prisma.sql`('grupos', ${b}, ${r.indice}, 'grupos', ${alianzaId.get(`${b}:${r.a}`)}, ${alianzaId.get(`${b}:${r.b}`)})`
            );
          }
        }
        await tx.$executeRaw(Prisma.sql`
          INSERT INTO stemsr_rondas (fase, bloque, indice, etapa, alianza_a_id, alianza_b_id)
          VALUES ${Prisma.join(rondaTuples)}
        `);
        const rSel = await tx.$queryRaw<
          { id: number; bloque: number; indice: number; alianza_a_id: number; alianza_b_id: number }[]
        >`
          SELECT id, bloque, indice, alianza_a_id, alianza_b_id
          FROM stemsr_rondas WHERE fase = 'grupos'
        `;
        const rondaBy = new Map(
          rSel.map((r) => [
            `${num(r.bloque)}:${num(r.indice)}`,
            { id: num(r.id), a: num(r.alianza_a_id), b: num(r.alianza_b_id) },
          ])
        );

        // Partidas (54)
        const partidaTuples = [];
        for (const b of BLOQUES) {
          for (const r of schedule) {
            const ronda = rondaBy.get(`${b}:${r.indice}`)!;
            for (let p = 1; p <= 3; p++) {
              partidaTuples.push(
                Prisma.sql`(${ronda.id}, ${p}, ${playOrder(r.indice, p)})`
              );
            }
          }
        }
        await tx.$executeRaw(Prisma.sql`
          INSERT INTO stemsr_partidas (ronda_id, indice_partida, orden_juego)
          VALUES ${Prisma.join(partidaTuples)}
        `);
        const ptSel = await tx.$queryRaw<
          { id: number; ronda_id: number; indice_partida: number }[]
        >`
          SELECT pt.id, pt.ronda_id, pt.indice_partida
          FROM stemsr_partidas pt
          JOIN stemsr_rondas r ON r.id = pt.ronda_id
          WHERE r.fase = 'grupos'
        `;
        const partidaBy = new Map(
          ptSel.map((r) => [`${num(r.ronda_id)}:${num(r.indice_partida)}`, num(r.id)])
        );

        // partida_alianza (108): lado A / lado B
        const paTuples = [];
        for (const b of BLOQUES) {
          for (const r of schedule) {
            const ronda = rondaBy.get(`${b}:${r.indice}`)!;
            for (let p = 1; p <= 3; p++) {
              const pid = partidaBy.get(`${ronda.id}:${p}`);
              paTuples.push(Prisma.sql`(${pid}, ${ronda.a}, 'A')`);
              paTuples.push(Prisma.sql`(${pid}, ${ronda.b}, 'B')`);
            }
          }
        }
        await tx.$executeRaw(Prisma.sql`
          INSERT INTO stemsr_partida_alianza (partida_id, alianza_id, lado)
          VALUES ${Prisma.join(paTuples)}
        `);

        await tx.$executeRaw`
          UPDATE stemsr_competencia
          SET fase_actual = 'grupos', plan_generado_en = NOW()
          WHERE id = 1
        `;
      },
      { timeout: 30000, maxWait: 10000 }
    );

    res.json({
      ok: true,
      total: TOTAL_KITS,
      reales: TOTAL_KITS - placeholders,
      placeholders,
      rondas: schedule.length * BLOQUES.length,
      partidas: schedule.length * BLOQUES.length * 3,
    });
  } catch (e) { next(e); }
});

// =============================================================================
// ALIANZAS POR BLOQUE
// =============================================================================
type AlianzaMiembroRow = {
  alianza_id: number;
  bloque: number;
  codigo: string;
  posicion: number | null;
  kit_codigo: string | null;
  nombre_robot: string | null;
  estudiantes: string | null;
  colegio: string | null;
  es_placeholder: number | null;
  seed_index: number | null;
};

stemsrRouter.get("/alianzas", async (_req, res, next) => {
  try {
    const rows = await prisma.$queryRaw<AlianzaMiembroRow[]>`
      SELECT a.id AS alianza_id, a.bloque, a.codigo,
             m.posicion, p.kit_codigo, p.nombre_robot, p.estudiantes,
             p.colegio, p.es_placeholder, p.seed_index
      FROM stemsr_alianzas a
      LEFT JOIN stemsr_miembros m ON m.alianza_id = a.id
      LEFT JOIN stemsr_participantes p ON p.id = m.participante_id
      WHERE a.fase = 'grupos'
      ORDER BY a.bloque, LENGTH(a.codigo), a.codigo, m.posicion
    `;
    const bloques = BLOQUES.map((b) => {
      const alianzasMap = new Map<number, {
        id: number; codigo: string; miembros: unknown[];
      }>();
      for (const r of rows) {
        if (num(r.bloque) !== b) continue;
        if (!alianzasMap.has(num(r.alianza_id))) {
          alianzasMap.set(num(r.alianza_id), {
            id: num(r.alianza_id),
            codigo: r.codigo,
            miembros: [],
          });
        }
        if (r.posicion != null) {
          alianzasMap.get(num(r.alianza_id))!.miembros.push({
            posicion: num(r.posicion),
            kit_codigo: r.kit_codigo,
            nombre_robot: r.nombre_robot,
            estudiantes: r.estudiantes,
            colegio: r.colegio,
            es_placeholder: bool(r.es_placeholder),
            seed_index: r.seed_index == null ? null : num(r.seed_index),
          });
        }
      }
      return { bloque: b, alianzas: [...alianzasMap.values()] };
    });
    res.json({ bloques });
  } catch (e) { next(e); }
});

// =============================================================================
// CALENDARIO (rondas + partidas en orden de juego, por bloque)
// =============================================================================
type CalendarioRow = {
  ronda_id: number;
  bloque: number;
  indice: number;
  etapa: string;
  ganador_alianza_id: number | null;
  alianza_a: string;
  alianza_b: string;
  partida_id: number;
  indice_partida: number;
  orden_juego: number;
  jugada: number;
};

stemsrRouter.get("/calendario", async (_req, res, next) => {
  try {
    const rows = await prisma.$queryRaw<CalendarioRow[]>`
      SELECT r.id AS ronda_id, r.bloque, r.indice, r.etapa, r.ganador_alianza_id,
             aa.codigo AS alianza_a, ab.codigo AS alianza_b,
             pt.id AS partida_id, pt.indice_partida, pt.orden_juego, pt.jugada
      FROM stemsr_rondas r
      JOIN stemsr_alianzas aa ON aa.id = r.alianza_a_id
      JOIN stemsr_alianzas ab ON ab.id = r.alianza_b_id
      JOIN stemsr_partidas pt ON pt.ronda_id = r.id
      WHERE r.fase = 'grupos'
      ORDER BY r.bloque, pt.orden_juego
    `;
    const bloques = BLOQUES.map((b) => ({
      bloque: b,
      partidas: rows
        .filter((r) => num(r.bloque) === b)
        .map((r) => ({
          partida_id: num(r.partida_id),
          ronda_id: num(r.ronda_id),
          ronda_indice: num(r.indice),
          indice_partida: num(r.indice_partida),
          orden_juego: num(r.orden_juego),
          alianza_a: r.alianza_a,
          alianza_b: r.alianza_b,
          jugada: bool(r.jugada),
        })),
    }));
    res.json({ bloques });
  } catch (e) { next(e); }
});

// =============================================================================
// DETALLE DE PARTIDA
// =============================================================================
type PartidaRow = {
  id: number;
  ronda_id: number;
  fase: string;
  bloque: number;
  ronda_indice: number;
  etapa: string;
  indice_partida: number;
  orden_juego: number;
  coopertition: number;
  co2_sin_recoger: string | number;
  jugada: number;
  a_id: number;
  a_codigo: string;
  b_id: number;
  b_codigo: string;
};
type LadoRow = {
  id: number;
  alianza_id: number;
  lado: "A" | "B";
  creditos_carbono: string | number;
  multiplicador: Multiplicador;
  co2_ecosistema: string | number;
  puntaje: string | number;
  puntaje_manual: string | number | null;
};
type MiembroRow = {
  alianza_id: number;
  posicion: number;
  kit_codigo: string | null;
  nombre_robot: string | null;
  estudiantes: string | null;
  es_placeholder: number;
};
type PenalRow = {
  partida_alianza_id: number;
  tipo_id: number;
  codigo: string;
  descripcion: string;
  puntos: number;
  orden: number;
  cantidad: number | null;
};

async function partidaDetalle(partidaId: number) {
  const [pt] = await prisma.$queryRaw<PartidaRow[]>`
    SELECT pt.id, pt.ronda_id, r.fase, r.bloque, r.indice AS ronda_indice, r.etapa,
           pt.indice_partida, pt.orden_juego, pt.coopertition, pt.co2_sin_recoger,
           pt.jugada,
           aa.id AS a_id, aa.codigo AS a_codigo,
           ab.id AS b_id, ab.codigo AS b_codigo
    FROM stemsr_partidas pt
    JOIN stemsr_rondas r ON r.id = pt.ronda_id
    JOIN stemsr_alianzas aa ON aa.id = r.alianza_a_id
    JOIN stemsr_alianzas ab ON ab.id = r.alianza_b_id
    WHERE pt.id = ${partidaId}
  `;
  if (!pt) throw new HttpError(404, "Partida no encontrada");

  const lados = await prisma.$queryRaw<LadoRow[]>`
    SELECT id, alianza_id, lado, creditos_carbono, multiplicador, co2_ecosistema,
           puntaje, puntaje_manual
    FROM stemsr_partida_alianza WHERE partida_id = ${partidaId} ORDER BY lado
  `;
  const miembros = await prisma.$queryRaw<MiembroRow[]>`
    SELECT m.alianza_id, m.posicion,
           p.kit_codigo, p.nombre_robot, p.estudiantes, p.es_placeholder
    FROM stemsr_miembros m
    JOIN stemsr_participantes p ON p.id = m.participante_id
    WHERE m.alianza_id IN (${num(pt.a_id)}, ${num(pt.b_id)})
    ORDER BY m.alianza_id, m.posicion
  `;
  const penal = await prisma.$queryRaw<PenalRow[]>`
    SELECT pa.id AS partida_alianza_id, t.id AS tipo_id, t.codigo, t.descripcion,
           t.puntos, t.orden, pen.cantidad
    FROM stemsr_partida_alianza pa
    CROSS JOIN stemsr_penalizacion_tipos t
    LEFT JOIN stemsr_penalizaciones pen
      ON pen.partida_alianza_id = pa.id AND pen.tipo_id = t.id
    WHERE pa.partida_id = ${partidaId} AND t.activo = TRUE
    ORDER BY pa.lado, t.orden
  `;

  // En eliminación las alianzas son de 4 y no hay rotación: juegan los 4.
  const esEliminacion = pt.fase === "eliminacion";
  const jugando = rotationPositions(num(pt.indice_partida));
  const ladosOut = lados.map((l) => ({
    partida_alianza_id: num(l.id),
    lado: l.lado,
    alianza_id: num(l.alianza_id),
    alianza_codigo: l.lado === "A" ? pt.a_codigo : pt.b_codigo,
    creditos_carbono: num(l.creditos_carbono),
    multiplicador: l.multiplicador,
    co2_ecosistema: num(l.co2_ecosistema),
    puntaje: num(l.puntaje),
    puntaje_manual: l.puntaje_manual == null ? null : num(l.puntaje_manual),
    miembros: miembros
      .filter((m) => num(m.alianza_id) === num(l.alianza_id))
      .map((m) => ({
        posicion: num(m.posicion),
        juega: esEliminacion ? true : jugando.includes(num(m.posicion)),
        kit_codigo: m.kit_codigo,
        nombre_robot: m.nombre_robot,
        estudiantes: m.estudiantes,
        es_placeholder: bool(m.es_placeholder),
      })),
    penalizaciones: penal
      .filter((p) => num(p.partida_alianza_id) === num(l.id))
      .map((p) => ({
        tipo_id: num(p.tipo_id),
        codigo: p.codigo,
        descripcion: p.descripcion,
        puntos: num(p.puntos),
        cantidad: p.cantidad == null ? 0 : num(p.cantidad),
      })),
  }));

  return {
    partida: {
      id: num(pt.id),
      ronda_id: num(pt.ronda_id),
      fase: pt.fase,
      bloque: num(pt.bloque),
      ronda_indice: num(pt.ronda_indice),
      etapa: pt.etapa,
      indice_partida: num(pt.indice_partida),
      orden_juego: num(pt.orden_juego),
      coopertition: bool(pt.coopertition),
      co2_sin_recoger: num(pt.co2_sin_recoger),
      jugada: bool(pt.jugada),
      alianza_a: pt.a_codigo,
      alianza_b: pt.b_codigo,
    },
    lados: ladosOut,
  };
}

stemsrRouter.get("/partidas/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    res.json(await partidaDetalle(id));
  } catch (e) { next(e); }
});

// =============================================================================
// GUARDAR RESULTADO DE PARTIDA
// =============================================================================
const ladoSchema = z.object({
  lado: z.enum(["A", "B"]),
  creditos_carbono: z.number().min(0),
  multiplicador: z.enum(["basico", "intermedio", "completo"]),
  co2_ecosistema: z.number().min(0),
  // Si viene un número, ese es el puntaje TOTAL (override) y se ignora la fórmula.
  // null o ausente = usar la fórmula.
  puntaje_manual: z.number().nullable().optional(),
  penalizaciones: z
    .array(z.object({ tipo_id: z.number().int().positive(), cantidad: z.number().int().min(0) }))
    .default([]),
});
const putSchema = z.object({
  coopertition: z.boolean(),
  co2_sin_recoger: z.number().min(0),
  lados: z.array(ladoSchema).length(2),
});

stemsrRouter.put("/partidas/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = putSchema.parse(req.body);

    const lados = await prisma.$queryRaw<{ id: number; lado: string }[]>`
      SELECT id, lado FROM stemsr_partida_alianza WHERE partida_id = ${id}
    `;
    if (lados.length !== 2) throw new HttpError(404, "Partida no encontrada");
    const paId = new Map(lados.map((l) => [l.lado, num(l.id)]));

    const tipos = await prisma.$queryRaw<{ id: number; puntos: number }[]>`
      SELECT id, puntos FROM stemsr_penalizacion_tipos WHERE activo = TRUE
    `;
    const puntosTipo = new Map(tipos.map((t) => [num(t.id), num(t.puntos)]));

    await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`
        UPDATE stemsr_partidas
        SET coopertition = ${body.coopertition},
            co2_sin_recoger = ${body.co2_sin_recoger},
            jugada = TRUE
        WHERE id = ${id}
      `;
      for (const l of body.lados) {
        const partidaAlianzaId = paId.get(l.lado);
        if (!partidaAlianzaId) throw new HttpError(400, `Lado inválido: ${l.lado}`);

        const puntajeFormula = scoreAlianza({
          creditosCarbono: l.creditos_carbono,
          multiplicador: l.multiplicador as Multiplicador,
          coopertition: body.coopertition,
          co2SinRecoger: body.co2_sin_recoger,
          co2Ecosistema: l.co2_ecosistema,
          penalizaciones: l.penalizaciones.map((p) => ({
            puntos: puntosTipo.get(p.tipo_id) ?? 0,
            cantidad: p.cantidad,
          })),
        });
        // El puntaje manual (override) manda sobre la fórmula cuando está presente.
        // El total nunca baja de 0 (aplica también al override manual).
        const manual = l.puntaje_manual == null ? null : round2(l.puntaje_manual);
        const puntaje = Math.max(0, manual ?? puntajeFormula);

        await tx.$executeRaw`
          UPDATE stemsr_partida_alianza
          SET creditos_carbono = ${l.creditos_carbono},
              multiplicador = ${l.multiplicador},
              co2_ecosistema = ${l.co2_ecosistema},
              puntaje_manual = ${manual},
              puntaje = ${puntaje}
          WHERE id = ${partidaAlianzaId}
        `;
        await tx.$executeRaw`
          DELETE FROM stemsr_penalizaciones WHERE partida_alianza_id = ${partidaAlianzaId}
        `;
        const conCantidad = l.penalizaciones.filter((p) => p.cantidad > 0);
        if (conCantidad.length > 0) {
          await tx.$executeRaw(Prisma.sql`
            INSERT INTO stemsr_penalizaciones (partida_alianza_id, tipo_id, cantidad)
            VALUES ${Prisma.join(
              conCantidad.map((p) => Prisma.sql`(${partidaAlianzaId}, ${p.tipo_id}, ${p.cantidad})`)
            )}
          `);
        }
      }
    });

    res.json(await partidaDetalle(id));
  } catch (e) { next(e); }
});

// =============================================================================
// RANKING POR KIT
// Cada kit suma los puntos de su alianza en bloque 1 + bloque 2.
// =============================================================================
type AllianceTotalRow = { alianza_id: number; bloque: number; total: string | number };
type MembershipRow = {
  participante_id: number;
  bloque: number;
  codigo: string;
  alianza_id: number;
  kit_codigo: string | null;
  nombre_robot: string | null;
  estudiantes: string | null;
  es_placeholder: number;
  seed_index: number;
};

async function computeRanking() {
    const totales = await prisma.$queryRaw<AllianceTotalRow[]>`
      SELECT a.id AS alianza_id, a.bloque, COALESCE(SUM(pa.puntaje), 0) AS total
      FROM stemsr_alianzas a
      LEFT JOIN stemsr_partida_alianza pa ON pa.alianza_id = a.id
      WHERE a.fase = 'grupos'
      GROUP BY a.id, a.bloque
    `;
    const totalPorAlianza = new Map(totales.map((t) => [num(t.alianza_id), num(t.total)]));

    const membership = await prisma.$queryRaw<MembershipRow[]>`
      SELECT m.participante_id, a.bloque, a.codigo, a.id AS alianza_id,
             p.kit_codigo, p.nombre_robot, p.estudiantes, p.es_placeholder, p.seed_index
      FROM stemsr_miembros m
      JOIN stemsr_alianzas a ON a.id = m.alianza_id
      JOIN stemsr_participantes p ON p.id = m.participante_id
      WHERE a.fase = 'grupos'
    `;

    const porKit = new Map<number, {
      participante_id: number;
      kit_codigo: string | null;
      nombre_robot: string | null;
      estudiantes: string | null;
      es_placeholder: boolean;
      seed_index: number;
      b1: { codigo: string; puntos: number } | null;
      b2: { codigo: string; puntos: number } | null;
    }>();
    for (const r of membership) {
      const pid = num(r.participante_id);
      if (!porKit.has(pid)) {
        porKit.set(pid, {
          participante_id: pid,
          kit_codigo: r.kit_codigo,
          nombre_robot: r.nombre_robot,
          estudiantes: r.estudiantes,
          es_placeholder: bool(r.es_placeholder),
          seed_index: num(r.seed_index),
          b1: null,
          b2: null,
        });
      }
      const entry = porKit.get(pid)!;
      const puntos = totalPorAlianza.get(num(r.alianza_id)) ?? 0;
      if (num(r.bloque) === 1) entry.b1 = { codigo: r.codigo, puntos };
      else entry.b2 = { codigo: r.codigo, puntos };
    }

    const ranking = [...porKit.values()]
      .map((k) => ({
        ...k,
        total: Math.round(((k.b1?.puntos ?? 0) + (k.b2?.puntos ?? 0)) * 100) / 100,
      }))
      .sort((a, b) => b.total - a.total || a.seed_index - b.seed_index)
      .map((k, i) => ({ posicion: i + 1, ...k }));

  return ranking;
}

stemsrRouter.get("/ranking", async (_req, res, next) => {
  try {
    res.json({ ranking: await computeRanking() });
  } catch (e) { next(e); }
});

// =============================================================================
// SIMULACIÓN DE GRUPOS (para pruebas de eliminación)
// Llena todas las partidas de grupos con créditos aleatorios y las marca como
// jugadas, así el ranking queda poblado sin cargar a mano.
// =============================================================================
stemsrRouter.post("/simular-grupos", async (_req, res, next) => {
  try {
    const rows = await prisma.$queryRaw<{ id: number }[]>`
      SELECT pa.id FROM stemsr_partida_alianza pa
      JOIN stemsr_partidas pt ON pt.id = pa.partida_id
      JOIN stemsr_rondas r ON r.id = pt.ronda_id
      WHERE r.fase = 'grupos'
    `;
    if (rows.length === 0) throw new HttpError(400, "Generá el plan de grupos primero.");

    await prisma.$transaction(async (tx) => {
      for (const row of rows) {
        const creditos = Math.floor(Math.random() * 251) + 50; // 50..300
        const puntaje = scoreAlianza({
          creditosCarbono: creditos,
          multiplicador: "completo",
          coopertition: false,
          co2SinRecoger: 0,
          co2Ecosistema: 0,
          penalizaciones: [],
        });
        await tx.$executeRaw`
          UPDATE stemsr_partida_alianza
          SET creditos_carbono = ${creditos}, multiplicador = 'completo',
              co2_ecosistema = 0, puntaje_manual = NULL, puntaje = ${puntaje}
          WHERE id = ${num(row.id)}
        `;
      }
      await tx.$executeRaw`
        UPDATE stemsr_partidas pt
        JOIN stemsr_rondas r ON r.id = pt.ronda_id
        SET pt.coopertition = FALSE, pt.co2_sin_recoger = 0, pt.jugada = TRUE
        WHERE r.fase = 'grupos'
      `;
    }, { timeout: 30000, maxWait: 10000 });

    res.json({ ok: true, partidas_alianza: rows.length });
  } catch (e) { next(e); }
});

// =============================================================================
// FASE 3 — ELIMINACIÓN
// =============================================================================

// Inserta una ronda de eliminación con sus 2 partidas (sin rotación) y los
// lados A/B vacíos. Devuelve el id de la ronda.
async function insertRondaElim(
  tx: Prisma.TransactionClient,
  etapa: string,
  indice: number,
  aId: number,
  bId: number,
  orden: number
): Promise<number> {
  await tx.$executeRaw`
    INSERT INTO stemsr_rondas (fase, bloque, indice, etapa, alianza_a_id, alianza_b_id)
    VALUES ('eliminacion', NULL, ${indice}, ${etapa}, ${aId}, ${bId})
  `;
  const [{ id: rid }] = await tx.$queryRaw<{ id: number }[]>`SELECT LAST_INSERT_ID() AS id`;
  const rondaId = num(rid);
  for (let p = 1; p <= 2; p++) {
    await tx.$executeRaw`
      INSERT INTO stemsr_partidas (ronda_id, indice_partida, orden_juego)
      VALUES (${rondaId}, ${p}, ${orden + p - 1})
    `;
    const [{ id: pid }] = await tx.$queryRaw<{ id: number }[]>`SELECT LAST_INSERT_ID() AS id`;
    const partidaId = num(pid);
    await tx.$executeRaw`
      INSERT INTO stemsr_partida_alianza (partida_id, alianza_id, lado)
      VALUES (${partidaId}, ${aId}, 'A'), (${partidaId}, ${bId}, 'B')
    `;
  }
  return rondaId;
}

type LadoElim = { alianza_id: number; codigo: string; seed: number; puntos: number; creditos: number };
// Ganador de un cruce: mayor suma de puntos; empate → más créditos de carbono
// (pelotas negras); empate → mejor sembrado (seed menor).
function decidirGanador(a: LadoElim, b: LadoElim): LadoElim {
  if (a.puntos !== b.puntos) return a.puntos > b.puntos ? a : b;
  if (a.creditos !== b.creditos) return a.creditos > b.creditos ? a : b;
  return a.seed <= b.seed ? a : b;
}

async function estadoEliminacion() {
  const alianzas = await prisma.$queryRaw<{ id: number; codigo: string; seed: number }[]>`
    SELECT id, codigo, seed FROM stemsr_alianzas WHERE fase = 'eliminacion' ORDER BY seed
  `;
  if (alianzas.length === 0) {
    return {
      generado: false,
      alianzas: [] as unknown[],
      rondas: { r10: [], semi: [], final: [], tercer_lugar: [] } as Record<string, any[]>,
      corte: null as any,
      campeon: null as string | null,
      tercero: null as string | null,
      puede_avanzar: null as string | null,
    };
  }

  const miembros = await prisma.$queryRaw<{
    alianza_id: number; posicion: number; kit_codigo: string | null;
    nombre_robot: string | null; estudiantes: string | null;
  }[]>`
    SELECT m.alianza_id, m.posicion, p.kit_codigo, p.nombre_robot, p.estudiantes
    FROM stemsr_miembros m
    JOIN stemsr_alianzas a ON a.id = m.alianza_id
    JOIN stemsr_participantes p ON p.id = m.participante_id
    WHERE a.fase = 'eliminacion'
    ORDER BY m.alianza_id, m.posicion
  `;
  const alianzasOut = alianzas.map((a) => ({
    id: num(a.id),
    codigo: a.codigo,
    seed: num(a.seed),
    miembros: miembros
      .filter((m) => num(m.alianza_id) === num(a.id))
      .map((m) => ({
        posicion: num(m.posicion),
        kit_codigo: m.kit_codigo,
        nombre_robot: m.nombre_robot,
        estudiantes: m.estudiantes,
      })),
  }));

  const rondas = await prisma.$queryRaw<{
    ronda_id: number; etapa: string; indice: number;
    a_id: number; a_codigo: string; a_seed: number;
    b_id: number; b_codigo: string; b_seed: number;
  }[]>`
    SELECT r.id AS ronda_id, r.etapa, r.indice,
           aa.id AS a_id, aa.codigo AS a_codigo, aa.seed AS a_seed,
           ab.id AS b_id, ab.codigo AS b_codigo, ab.seed AS b_seed
    FROM stemsr_rondas r
    JOIN stemsr_alianzas aa ON aa.id = r.alianza_a_id
    JOIN stemsr_alianzas ab ON ab.id = r.alianza_b_id
    WHERE r.fase = 'eliminacion'
    ORDER BY FIELD(r.etapa, 'r10','semi','final','tercer_lugar'), r.indice
  `;
  const agg = await prisma.$queryRaw<{
    ronda_id: number; partida_id: number; indice_partida: number; jugada: number;
    alianza_id: number; puntaje: string | number; creditos_carbono: string | number;
  }[]>`
    SELECT pt.ronda_id, pt.id AS partida_id, pt.indice_partida, pt.jugada,
           pa.alianza_id, pa.puntaje, pa.creditos_carbono
    FROM stemsr_partidas pt
    JOIN stemsr_rondas r ON r.id = pt.ronda_id
    JOIN stemsr_partida_alianza pa ON pa.partida_id = pt.id
    WHERE r.fase = 'eliminacion'
    ORDER BY pt.ronda_id, pt.indice_partida
  `;

  const sumByRonda = new Map<number, Map<number, { puntos: number; creditos: number }>>();
  const partidasByRonda = new Map<number, Map<number, { partida_id: number; indice_partida: number; jugada: boolean }>>();
  for (const r of agg) {
    const rid = num(r.ronda_id);
    if (!sumByRonda.has(rid)) sumByRonda.set(rid, new Map());
    const m = sumByRonda.get(rid)!;
    const aid = num(r.alianza_id);
    if (!m.has(aid)) m.set(aid, { puntos: 0, creditos: 0 });
    const e = m.get(aid)!;
    e.puntos += num(r.puntaje);
    e.creditos += num(r.creditos_carbono);
    if (!partidasByRonda.has(rid)) partidasByRonda.set(rid, new Map());
    partidasByRonda.get(rid)!.set(num(r.partida_id), {
      partida_id: num(r.partida_id),
      indice_partida: num(r.indice_partida),
      jugada: bool(r.jugada),
    });
  }

  const grouped: Record<string, any[]> = { r10: [], semi: [], final: [], tercer_lugar: [] };
  for (const r of rondas) {
    const rid = num(r.ronda_id);
    const sums = sumByRonda.get(rid) ?? new Map();
    const aSum = sums.get(num(r.a_id)) ?? { puntos: 0, creditos: 0 };
    const bSum = sums.get(num(r.b_id)) ?? { puntos: 0, creditos: 0 };
    const A: LadoElim = { alianza_id: num(r.a_id), codigo: r.a_codigo, seed: num(r.a_seed), puntos: round2(aSum.puntos), creditos: round2(aSum.creditos) };
    const B: LadoElim = { alianza_id: num(r.b_id), codigo: r.b_codigo, seed: num(r.b_seed), puntos: round2(bSum.puntos), creditos: round2(bSum.creditos) };
    const parts = [...(partidasByRonda.get(rid)?.values() ?? [])].sort((x, y) => x.indice_partida - y.indice_partida);
    const completa = parts.length === 2 && parts.every((p) => p.jugada);
    const g = completa ? decidirGanador(A, B) : null;
    grouped[r.etapa]?.push({
      ronda_id: rid,
      etapa: r.etapa,
      indice: num(r.indice),
      a: { alianza_id: A.alianza_id, codigo: A.codigo, seed: A.seed, puntos: A.puntos },
      b: { alianza_id: B.alianza_id, codigo: B.codigo, seed: B.seed, puntos: B.puntos },
      partidas: parts,
      completa,
      ganador: g ? { alianza_id: g.alianza_id, codigo: g.codigo, seed: g.seed, puntos: g.puntos } : null,
    });
  }

  // Corte 5 → 4 (cuando los 5 cruces de la r10 están completos)
  let corte: any = null;
  const r10Completa = grouped.r10.length === 5 && grouped.r10.every((x) => x.completa);
  if (r10Completa) {
    const winners = grouped.r10.map((x) => x.ganador);
    const sorted = [...winners].sort((p, q) => q.puntos - p.puntos || p.seed - q.seed);
    corte = { listo: true, avanzan: sorted.slice(0, 4), eliminado: sorted[4] };
  }

  let puede_avanzar: string | null = null;
  if (grouped.semi.length === 0 && r10Completa) puede_avanzar = "r10";
  else if (grouped.final.length === 0 && grouped.semi.length === 2 && grouped.semi.every((x) => x.completa))
    puede_avanzar = "semis";

  const finalRonda = grouped.final[0];
  const terceroRonda = grouped.tercer_lugar[0];

  return {
    generado: true,
    alianzas: alianzasOut,
    rondas: grouped,
    corte,
    campeon: finalRonda?.completa ? finalRonda.ganador.codigo : null,
    tercero: terceroRonda?.completa ? terceroRonda.ganador.codigo : null,
    puede_avanzar,
  };
}

// Genera la fase de eliminación: top 40 → 10 alianzas (serpentina) → cruces r10.
stemsrRouter.post("/eliminacion/generar", async (req, res, next) => {
  try {
    const force = z.object({ force: z.boolean().optional() }).parse(req.body ?? {}).force === true;

    const [{ pend }] = await prisma.$queryRaw<{ pend: bigint }[]>`
      SELECT COUNT(*) AS pend FROM stemsr_partidas pt
      JOIN stemsr_rondas r ON r.id = pt.ronda_id
      WHERE r.fase = 'grupos' AND pt.jugada = FALSE
    `;
    if (num(pend) > 0 && !force) {
      throw new HttpError(400, `Faltan ${num(pend)} partidas de grupos por jugar. Termina los grupos primero.`);
    }

    const ranking = await computeRanking();
    if (ranking.length < 40) throw new HttpError(400, "Se necesitan al menos 40 kits en el ranking.");
    const top40 = ranking.slice(0, 40);
    const grupos10 = snakeDistribution(top40, 10); // 10 x 4 participantes

    await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`DELETE FROM stemsr_penalizaciones WHERE partida_alianza_id IN (
        SELECT pa.id FROM stemsr_partida_alianza pa
        JOIN stemsr_partidas pt ON pt.id = pa.partida_id
        JOIN stemsr_rondas r ON r.id = pt.ronda_id WHERE r.fase = 'eliminacion')`;
      await tx.$executeRaw`DELETE FROM stemsr_partida_alianza WHERE partida_id IN (
        SELECT pt.id FROM stemsr_partidas pt
        JOIN stemsr_rondas r ON r.id = pt.ronda_id WHERE r.fase = 'eliminacion')`;
      await tx.$executeRaw`DELETE FROM stemsr_partidas WHERE ronda_id IN (
        SELECT id FROM stemsr_rondas WHERE fase = 'eliminacion')`;
      await tx.$executeRaw`DELETE FROM stemsr_miembros WHERE alianza_id IN (
        SELECT id FROM stemsr_alianzas WHERE fase = 'eliminacion')`;
      await tx.$executeRaw`DELETE FROM stemsr_rondas WHERE fase = 'eliminacion'`;
      await tx.$executeRaw`DELETE FROM stemsr_alianzas WHERE fase = 'eliminacion'`;

      const alianzaTuples: Prisma.Sql[] = [];
      for (let i = 0; i < 10; i++) alianzaTuples.push(Prisma.sql`('eliminacion', ${allianceCode(i)}, ${i + 1})`);
      await tx.$executeRaw(Prisma.sql`
        INSERT INTO stemsr_alianzas (fase, codigo, seed) VALUES ${Prisma.join(alianzaTuples)}
      `);
      const aSel = await tx.$queryRaw<{ id: number; codigo: string }[]>`
        SELECT id, codigo FROM stemsr_alianzas WHERE fase = 'eliminacion'
      `;
      const alianzaId = new Map(aSel.map((r) => [r.codigo, num(r.id)]));

      const miembroTuples: Prisma.Sql[] = [];
      for (let i = 0; i < 10; i++) {
        const aId = alianzaId.get(allianceCode(i));
        grupos10[i].forEach((p: any, idx: number) => {
          miembroTuples.push(Prisma.sql`(${aId}, ${idx + 1}, ${p.participante_id})`);
        });
      }
      await tx.$executeRaw(Prisma.sql`
        INSERT INTO stemsr_miembros (alianza_id, posicion, participante_id) VALUES ${Prisma.join(miembroTuples)}
      `);

      let orden = 1;
      for (let i = 0; i < CRUCES_R10.length; i++) {
        const [sa, sb] = CRUCES_R10[i];
        await insertRondaElim(tx, "r10", i + 1, alianzaId.get(allianceCode(sa - 1))!, alianzaId.get(allianceCode(sb - 1))!, orden);
        orden += 2;
      }

      await tx.$executeRaw`UPDATE stemsr_competencia SET fase_actual = 'eliminacion' WHERE id = 1`;
    }, { timeout: 30000, maxWait: 10000 });

    res.json(await estadoEliminacion());
  } catch (e) { next(e); }
});

stemsrRouter.get("/eliminacion", async (_req, res, next) => {
  try {
    res.json(await estadoEliminacion());
  } catch (e) { next(e); }
});

// Avanza de etapa cuando la actual está completa: r10 → semis ; semis → final + 3er.
stemsrRouter.post("/eliminacion/avanzar", async (_req, res, next) => {
  try {
    const est = await estadoEliminacion();
    if (!est.generado) throw new HttpError(400, "La eliminación no está generada.");

    if (est.puede_avanzar === "r10") {
      const [w1, w2, w3, w4] = (est.corte as any).avanzan;
      await prisma.$transaction(async (tx) => {
        for (const r of est.rondas.r10 as any[]) {
          await tx.$executeRaw`UPDATE stemsr_rondas SET ganador_alianza_id = ${r.ganador.alianza_id} WHERE id = ${r.ronda_id}`;
        }
        await insertRondaElim(tx, "semi", 1, w1.alianza_id, w4.alianza_id, 11);
        await insertRondaElim(tx, "semi", 2, w2.alianza_id, w3.alianza_id, 13);
      });
    } else if (est.puede_avanzar === "semis") {
      const semis = est.rondas.semi as any[];
      const s1 = semis.find((x) => x.indice === 1);
      const s2 = semis.find((x) => x.indice === 2);
      const loser = (s: any) => (s.a.alianza_id === s.ganador.alianza_id ? s.b : s.a);
      await prisma.$transaction(async (tx) => {
        for (const s of semis) {
          await tx.$executeRaw`UPDATE stemsr_rondas SET ganador_alianza_id = ${s.ganador.alianza_id} WHERE id = ${s.ronda_id}`;
        }
        await insertRondaElim(tx, "final", 1, s1.ganador.alianza_id, s2.ganador.alianza_id, 15);
        await insertRondaElim(tx, "tercer_lugar", 1, loser(s1).alianza_id, loser(s2).alianza_id, 17);
      });
    } else {
      throw new HttpError(400, "No hay nada que avanzar (faltan partidas por jugar en la etapa actual).");
    }

    res.json(await estadoEliminacion());
  } catch (e) { next(e); }
});
