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 { calcularFormatos } from "./robofut.formats.js";

export const robofutRouter = Router();
robofutRouter.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
// =============================================================================
type CompetenciaRow = {
  id: number;
  nombre: string;
  fase_actual: string;
  formato_elegido: string | null;
  voluntario_kit_id: number | null;
  ida_vuelta_grupos: number;
  ida_vuelta_eliminatoria: number;
};

robofutRouter.get("/competencia", async (_req, res, next) => {
  try {
    const [c] = await prisma.$queryRaw<CompetenciaRow[]>`
      SELECT id, nombre, fase_actual, formato_elegido, voluntario_kit_id,
             ida_vuelta_grupos, ida_vuelta_eliminatoria
      FROM robofut_competencia WHERE id = 1
    `;
    res.json({
      competencia: {
        ...c,
        ida_vuelta_grupos: !!c.ida_vuelta_grupos,
        ida_vuelta_eliminatoria: !!c.ida_vuelta_eliminatoria,
        formato_elegido: c.formato_elegido
          ? typeof c.formato_elegido === "string"
            ? JSON.parse(c.formato_elegido)
            : c.formato_elegido
          : null,
      },
    });
  } catch (e) { next(e); }
});

robofutRouter.patch("/competencia/config", async (req, res, next) => {
  try {
    const body = z.object({
      ida_vuelta_grupos: z.boolean().optional(),
      ida_vuelta_eliminatoria: z.boolean().optional(),
    }).parse(req.body);
    if (body.ida_vuelta_grupos !== undefined) {
      await prisma.$executeRaw`
        UPDATE robofut_competencia SET ida_vuelta_grupos = ${body.ida_vuelta_grupos} WHERE id = 1
      `;
    }
    if (body.ida_vuelta_eliminatoria !== undefined) {
      await prisma.$executeRaw`
        UPDATE robofut_competencia SET ida_vuelta_eliminatoria = ${body.ida_vuelta_eliminatoria} WHERE id = 1
      `;
    }
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// =============================================================================
// KITS
// =============================================================================
type KitRow = {
  id: number;
  inscripcion_id: number | null;
  kit_codigo: string | null;
  nombre_robot: string | null;
  estudiantes: string | null;
  colegio: string | null;
  activo: number;
  creado_en: Date;
  equipos_count: number | bigint;
};

robofutRouter.get("/kits", async (_req, res, next) => {
  try {
    const rows = await prisma.$queryRaw<KitRow[]>`
      SELECT k.id, k.inscripcion_id, k.kit_codigo, k.nombre_robot,
             k.estudiantes, k.colegio, k.activo, k.creado_en,
             (SELECT COUNT(*) FROM robofut_equipo_kit ek WHERE ek.kit_id = k.id) AS equipos_count
      FROM robofut_kits k
      ORDER BY k.kit_codigo
    `;
    res.json({ data: rows });
  } catch (e) { next(e); }
});

// Importar kits desde crea_inscripciones con categoria ROBOFUT.
// Agrupa por kit_codigo (1 kit = 1 fila). Idempotente.
type InscritoRow = {
  kit_id: number | bigint;
  kit_codigo: string | null;
  nombre_robot: string | null;
  inscripcion_id: number | bigint;
  estudiantes: string;
  colegio: string | null;
};

robofutRouter.post("/kits/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
      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 = 'ROBOFUT'
      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) {
        if (!r.kit_codigo) {
          omitidos++;
          continue;
        }
        const ya = await tx.$queryRaw<{ id: number }[]>`
          SELECT id FROM robofut_kits WHERE kit_codigo = ${r.kit_codigo} LIMIT 1
        `;
        if (ya.length > 0) {
          omitidos++;
          continue;
        }
        await tx.$executeRaw`
          INSERT INTO robofut_kits
            (inscripcion_id, kit_codigo, nombre_robot, estudiantes, colegio, activo)
          VALUES
            (${num(r.inscripcion_id)}, ${r.kit_codigo}, ${r.nombre_robot},
             ${r.estudiantes}, ${r.colegio}, TRUE)
        `;
        creados++;
      }
    });

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

// =============================================================================
// EQUIPOS
// =============================================================================
type EquipoRow = {
  id: number;
  nombre: string;
  activo: number;
  creado_en: Date;
};
type EquipoKitRow = {
  equipo_id: number;
  kit_id: number;
  es_voluntario: number;
  kit_codigo: string | null;
  nombre_robot: string | null;
  estudiantes: string | null;
  colegio: string | null;
};

async function fetchEquipos() {
  const equipos = await prisma.$queryRaw<EquipoRow[]>`
    SELECT id, nombre, activo, creado_en FROM robofut_equipos
    WHERE activo = TRUE
    ORDER BY id
  `;
  if (equipos.length === 0) return [];
  const kits = await prisma.$queryRaw<EquipoKitRow[]>`
    SELECT ek.equipo_id, ek.kit_id, ek.es_voluntario,
           k.kit_codigo, k.nombre_robot, k.estudiantes, k.colegio
    FROM robofut_equipo_kit ek
    JOIN robofut_kits k ON k.id = ek.kit_id
    ORDER BY ek.equipo_id, ek.kit_id
  `;
  return equipos.map((e) => ({
    ...e,
    kits: kits.filter((k) => k.equipo_id === e.id),
  }));
}

robofutRouter.get("/equipos", async (_req, res, next) => {
  try {
    res.json({ data: await fetchEquipos() });
  } catch (e) { next(e); }
});

// Sortear equipos de 2 kits. Si N es impar, requiere voluntario_kit_id.
const sortearSchema = z.object({
  voluntario_kit_id: z.coerce.number().int().positive().optional(),
});

function shuffle<T>(arr: T[]): T[] {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

robofutRouter.post("/equipos/sortear", async (req, res, next) => {
  try {
    const body = sortearSchema.parse(req.body);
    const kits = await prisma.$queryRaw<{ id: number }[]>`
      SELECT id FROM robofut_kits WHERE activo = TRUE
    `;
    const ids = kits.map((k) => num(k.id));
    if (ids.length < 2) throw new HttpError(400, "Necesitás al menos 2 kits");

    const esImpar = ids.length % 2 === 1;
    if (esImpar && !body.voluntario_kit_id) {
      throw new HttpError(
        400,
        "El número de kits es impar. Pedí un kit voluntario antes de sortear."
      );
    }
    if (body.voluntario_kit_id && !ids.includes(body.voluntario_kit_id)) {
      throw new HttpError(404, "El kit voluntario no existe");
    }

    // Construir teams
    const voluntarioId = esImpar ? body.voluntario_kit_id! : null;
    const otros = ids.filter((id) => id !== voluntarioId);
    const shuffled = shuffle(otros);

    // Pares secuenciales
    const teams: { kits: number[]; voluntario: boolean }[] = [];
    for (let i = 0; i < shuffled.length; i += 2) {
      teams.push({ kits: [shuffled[i], shuffled[i + 1]], voluntario: false });
    }

    if (voluntarioId !== null) {
      // Tomamos el último par (k1, k2) y lo dividimos en (k1, vol) + (k2, vol)
      const last = teams.pop()!;
      teams.push({ kits: [last.kits[0], voluntarioId], voluntario: true });
      teams.push({ kits: [last.kits[1], voluntarioId], voluntario: true });
    }

    // Persistir: limpiar equipos previos primero
    await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`DELETE FROM robofut_equipo_kit`;
      await tx.$executeRaw`DELETE FROM robofut_equipos`;
      await tx.$executeRaw`
        UPDATE robofut_competencia
        SET voluntario_kit_id = ${voluntarioId}, fase_actual = 'equipos'
        WHERE id = 1
      `;
      for (const t of teams) {
        // Build name from robot names
        const kitNames: string[] = [];
        for (const k of t.kits) {
          if (voluntarioId !== null && k === voluntarioId && kitNames.some(n => n !== '')) continue;
          const [kit] = await tx.$queryRaw<{ nombre_robot: string | null }[]>`
            SELECT nombre_robot FROM robofut_kits WHERE id = ${k}
          `;
          kitNames.push(kit?.nombre_robot || `Kit-${k}`);
        }
        // For voluntario teams, include the voluntario robot name
        if (voluntarioId !== null && t.kits.includes(voluntarioId)) {
          const names: string[] = [];
          for (const k of t.kits) {
            const [kit] = await tx.$queryRaw<{ nombre_robot: string | null }[]>`
              SELECT nombre_robot FROM robofut_kits WHERE id = ${k}
            `;
            names.push(kit?.nombre_robot || `Kit-${k}`);
          }
          kitNames.length = 0;
          kitNames.push(...names);
        }
        const nombre = kitNames.join(' / ');
        await tx.$executeRaw`
          INSERT INTO robofut_equipos (nombre) VALUES (${nombre})
        `;
        const [{ id: equipoId }] = await tx.$queryRaw<{ id: number }[]>`
          SELECT LAST_INSERT_ID() AS id
        `;
        for (const k of t.kits) {
          const esVol = voluntarioId !== null && k === voluntarioId;
          await tx.$executeRaw`
            INSERT INTO robofut_equipo_kit (equipo_id, kit_id, es_voluntario)
            VALUES (${num(equipoId)}, ${k}, ${esVol})
          `;
        }
      }
    });

    res.json({
      ok: true,
      total_kits: ids.length,
      voluntario_kit_id: voluntarioId,
      equipos: await fetchEquipos(),
    });
  } catch (e) { next(e); }
});

// Editar equipo: cambiar nombre y/o reemplazar kits
const editEquipoSchema = z.object({
  nombre: z.string().trim().min(1).max(150).optional(),
  kits: z.array(z.coerce.number().int().positive()).length(2).optional(),
});

robofutRouter.patch("/equipos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = editEquipoSchema.parse(req.body);

    const [eq] = await prisma.$queryRaw<EquipoRow[]>`
      SELECT id FROM robofut_equipos WHERE id = ${id}
    `;
    if (!eq) throw new HttpError(404, "Equipo no encontrado");

    if (body.nombre !== undefined) {
      const dup = await prisma.$queryRaw<{ id: number }[]>`
        SELECT id FROM robofut_equipos WHERE nombre = ${body.nombre} AND id <> ${id} LIMIT 1
      `;
      if (dup.length > 0) throw new HttpError(409, "Ya existe un equipo con ese nombre");
    }

    if (body.kits !== undefined) {
      if (body.kits[0] === body.kits[1]) {
        throw new HttpError(400, "Los dos kits deben ser distintos");
      }
    }

    await prisma.$transaction(async (tx) => {
      if (body.nombre !== undefined) {
        await tx.$executeRaw`
          UPDATE robofut_equipos SET nombre = ${body.nombre} WHERE id = ${id}
        `;
      }
      if (body.kits !== undefined) {
        await tx.$executeRaw`DELETE FROM robofut_equipo_kit WHERE equipo_id = ${id}`;
        for (const k of body.kits) {
          await tx.$executeRaw`
            INSERT INTO robofut_equipo_kit (equipo_id, kit_id, es_voluntario)
            VALUES (${id}, ${k}, FALSE)
          `;
        }
        // Recalculate es_voluntario globally: a kit in 2+ teams is voluntario
        await tx.$executeRaw`
          UPDATE robofut_equipo_kit ek
          SET es_voluntario = (
            SELECT COUNT(*) > 1
            FROM (SELECT equipo_id FROM robofut_equipo_kit WHERE kit_id = ek.kit_id) sub
          )
        `;
        // Auto-rename this team
        const kitRows = await tx.$queryRaw<{ nombre_robot: string | null; kit_codigo: string }[]>`
          SELECT k.nombre_robot, k.kit_codigo
          FROM robofut_equipo_kit ek
          JOIN robofut_kits k ON k.id = ek.kit_id
          WHERE ek.equipo_id = ${id}
          ORDER BY ek.kit_id
        `;
        const newName = kitRows.map((r) => r.nombre_robot || r.kit_codigo).join(" / ");
        await tx.$executeRaw`UPDATE robofut_equipos SET nombre = ${newName} WHERE id = ${id}`;
      }
    });

    res.json({ data: await fetchEquipos() });
  } catch (e) { next(e); }
});

// Renombrar todos los equipos a "Robot1 / Robot2"
robofutRouter.post("/equipos/renombrar", async (_req, res, next) => {
  try {
    const equipos = await fetchEquipos();
    let renombrados = 0;
    await prisma.$transaction(async (tx) => {
      for (const eq of equipos) {
        const names = eq.kits.map((k: EquipoKitRow) => k.nombre_robot || k.kit_codigo || `Kit-${k.kit_id}`);
        const nombre = names.join(" / ");
        if (nombre !== eq.nombre) {
          await tx.$executeRaw`UPDATE robofut_equipos SET nombre = ${nombre} WHERE id = ${eq.id}`;
          renombrados++;
        }
      }
    });
    res.json({ ok: true, renombrados, equipos: await fetchEquipos() });
  } catch (e) { next(e); }
});

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

// =============================================================================
// FORMATOS
// =============================================================================
robofutRouter.get("/formatos", async (_req, res, next) => {
  try {
    const [{ n }] = await prisma.$queryRaw<{ n: number | bigint }[]>`
      SELECT COUNT(*) AS n FROM robofut_equipos WHERE activo = TRUE
    `;
    res.json(calcularFormatos(num(n)));
  } catch (e) { next(e); }
});

// =============================================================================
// GRUPOS
// =============================================================================
type GrupoRow = { id: number; nombre: string; pista: string | null; orden: number };
type GrupoEquipoRow = { grupo_id: number; equipo_id: number; equipo_nombre: string };
type GrupoKitRow = {
  equipo_id: number;
  kit_id: number;
  es_voluntario: number;
  kit_codigo: string | null;
  nombre_robot: string | null;
};

async function fetchGrupos() {
  const grupos = await prisma.$queryRaw<GrupoRow[]>`
    SELECT id, nombre, pista, orden FROM robofut_grupos ORDER BY orden, nombre
  `;
  if (grupos.length === 0) return [];
  const ges = await prisma.$queryRaw<GrupoEquipoRow[]>`
    SELECT ge.grupo_id, ge.equipo_id, e.nombre AS equipo_nombre
    FROM robofut_grupo_equipo ge
    JOIN robofut_equipos e ON e.id = ge.equipo_id
    ORDER BY ge.grupo_id, e.nombre
  `;
  const kits = await prisma.$queryRaw<GrupoKitRow[]>`
    SELECT ek.equipo_id, ek.kit_id, ek.es_voluntario, k.kit_codigo, k.nombre_robot
    FROM robofut_equipo_kit ek
    JOIN robofut_kits k ON k.id = ek.kit_id
    WHERE ek.equipo_id IN (SELECT equipo_id FROM robofut_grupo_equipo)
    ORDER BY ek.equipo_id, ek.kit_id
  `;
  return grupos.map((g) => ({
    ...g,
    id: num(g.id),
    equipos: ges
      .filter((ge) => num(ge.grupo_id) === num(g.id))
      .map((ge) => ({
        id: num(ge.equipo_id),
        nombre: ge.equipo_nombre,
        kits: kits
          .filter((k) => num(k.equipo_id) === num(ge.equipo_id))
          .map((k) => ({ ...k, equipo_id: num(k.equipo_id), kit_id: num(k.kit_id) })),
      })),
  }));
}

robofutRouter.get("/grupos", async (_req, res, next) => {
  try {
    res.json({ data: await fetchGrupos() });
  } catch (e) { next(e); }
});

const sortearGruposSchema = z.object({
  formato: z.union([
    z.object({
      cantidadGrupos: z.number().int().positive(),
      equiposPorGrupo: z.number().int().positive(),
      clasificadosFijosPorGrupo: z.number().int().min(0),
      totalClasificadosFijos: z.number().int().min(0),
      cuposMejoresPosicionados: z.number().int().min(0),
      descripcion: z.string().optional(),
    }),
    z.object({
      gruposGrandes: z.object({
        cantidad: z.number().int().min(0),
        tamaño: z.number().int().min(0),
        pasesDirectos: z.number().int().min(0),
      }),
      gruposPequeños: z.object({
        cantidad: z.number().int().min(0),
        tamaño: z.number().int().min(0),
        pasesDirectos: z.number().int().min(0),
      }),
      totalPasesDirectos: z.number().int().min(0),
      comodines: z.number().int().min(0),
    }),
  ]),
  prefijo: z.string().trim().min(1).max(50).default("Grupo"),
  pistas: z.array(z.string().trim()).optional(),
});

function shuffleArr<T>(arr: T[]): T[] {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function buildGrupoConfigs(formato: any, prefijo: string, pistas?: string[]) {
  const configs: { nombre: string; pista: string | null; capacidad: number }[] = [];
  const letras = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  if ("cantidadGrupos" in formato) {
    for (let i = 0; i < formato.cantidadGrupos; i++) {
      configs.push({
        nombre: `${prefijo} ${letras[i] ?? i + 1}`,
        pista: pistas?.[i] ?? null,
        capacidad: formato.equiposPorGrupo,
      });
    }
  } else {
    let i = 0;
    for (let n = 0; n < formato.gruposGrandes.cantidad; n++, i++) {
      configs.push({
        nombre: `${prefijo} ${letras[i] ?? i + 1}`,
        pista: pistas?.[i] ?? null,
        capacidad: formato.gruposGrandes.tamaño,
      });
    }
    for (let n = 0; n < formato.gruposPequeños.cantidad; n++, i++) {
      configs.push({
        nombre: `${prefijo} ${letras[i] ?? i + 1}`,
        pista: pistas?.[i] ?? null,
        capacidad: formato.gruposPequeños.tamaño,
      });
    }
  }
  return configs;
}

robofutRouter.post("/grupos/sortear", async (req, res, next) => {
  try {
    const body = sortearGruposSchema.parse(req.body);

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

    const configs = buildGrupoConfigs(body.formato, body.prefijo, body.pistas);
    const capacidadTotal = configs.reduce((s, c) => s + c.capacidad, 0);
    if (capacidadTotal !== equipoIds.length) {
      throw new HttpError(
        400,
        `El formato espera ${capacidadTotal} equipos pero hay ${equipoIds.length}.`
      );
    }

    const [{ voluntario_kit_id }] = await prisma.$queryRaw<{ voluntario_kit_id: number | null }[]>`
      SELECT voluntario_kit_id FROM robofut_competencia WHERE id = 1
    `;
    let voluntarioEquiposIds: number[] = [];
    if (voluntario_kit_id) {
      const rows = await prisma.$queryRaw<{ equipo_id: number | bigint }[]>`
        SELECT equipo_id FROM robofut_equipo_kit WHERE kit_id = ${voluntario_kit_id}
      `;
      voluntarioEquiposIds = rows.map((r) => num(r.equipo_id));
    }

    const slots = configs.map((c) => ({ ...c, equipos: [] as number[] }));

    // Colocar los 2 equipos del voluntario en grupos distintos
    if (voluntarioEquiposIds.length === 2 && slots.length >= 2) {
      const idxs = shuffleArr(Array.from({ length: slots.length }, (_, i) => i)).slice(0, 2);
      slots[idxs[0]].equipos.push(voluntarioEquiposIds[0]);
      slots[idxs[1]].equipos.push(voluntarioEquiposIds[1]);
    }

    // Distribuir el resto: round-robin entre grupos con espacio, shuffled
    const restantes = shuffleArr(
      equipoIds.filter((id) => !voluntarioEquiposIds.includes(id))
    );
    for (const eqId of restantes) {
      const candidatos = slots
        .map((s, i) => ({ idx: i, libre: s.capacidad - s.equipos.length }))
        .filter((c) => c.libre > 0);
      if (candidatos.length === 0) break;
      const maxLibre = Math.max(...candidatos.map((c) => c.libre));
      const topCandidatos = candidatos.filter((c) => c.libre === maxLibre);
      const pick = topCandidatos[Math.floor(Math.random() * topCandidatos.length)];
      slots[pick.idx].equipos.push(eqId);
    }

    await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`DELETE FROM robofut_grupo_equipo`;
      await tx.$executeRaw`DELETE FROM robofut_grupos`;
      let orden = 1;
      for (const s of slots) {
        await tx.$executeRaw`
          INSERT INTO robofut_grupos (nombre, pista, orden)
          VALUES (${s.nombre}, ${s.pista}, ${orden})
        `;
        const [{ id }] = await tx.$queryRaw<{ id: number }[]>`SELECT LAST_INSERT_ID() AS id`;
        for (const eqId of s.equipos) {
          await tx.$executeRaw`
            INSERT INTO robofut_grupo_equipo (grupo_id, equipo_id)
            VALUES (${num(id)}, ${eqId})
          `;
        }
        orden++;
      }
      await tx.$executeRaw`
        UPDATE robofut_competencia
        SET formato_elegido = ${JSON.stringify(body.formato)}, fase_actual = 'grupos'
        WHERE id = 1
      `;
    });

    res.json({ ok: true, grupos: await fetchGrupos() });
  } catch (e) { next(e); }
});

robofutRouter.patch("/grupos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = z
      .object({
        nombre: z.string().trim().min(1).max(150).optional(),
        pista: z.preprocess(emptyToNull, z.string().trim().max(150).nullable()).optional(),
      })
      .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 (sets.length === 0) throw new HttpError(400, "Nada para actualizar");
    await prisma.$executeRaw`UPDATE robofut_grupos SET ${Prisma.join(sets, ", ")} WHERE id = ${id}`;
    res.json({ data: await fetchGrupos() });
  } catch (e) { next(e); }
});

// Mover un equipo a este grupo (valida restricción del voluntario)
robofutRouter.post("/grupos/:id/equipos", async (req, res, next) => {
  try {
    const grupoId = idParam.parse(req.params.id);
    const body = z.object({ equipo_id: z.coerce.number().int().positive() }).parse(req.body);

    await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`DELETE FROM robofut_grupo_equipo WHERE equipo_id = ${body.equipo_id}`;
      await tx.$executeRaw`
        INSERT INTO robofut_grupo_equipo (grupo_id, equipo_id)
        VALUES (${grupoId}, ${body.equipo_id})
      `;
    });
    res.json({ data: await fetchGrupos() });
  } catch (e) { next(e); }
});

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

// Borrar todos los grupos y volver a la fase de equipos
robofutRouter.delete("/grupos", async (_req, res, next) => {
  try {
    await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`DELETE FROM robofut_partidos`;
      await tx.$executeRaw`DELETE FROM robofut_grupo_equipo`;
      await tx.$executeRaw`DELETE FROM robofut_grupos`;
      await tx.$executeRaw`
        UPDATE robofut_competencia
        SET formato_elegido = NULL, fase_actual = 'equipos'
        WHERE id = 1
      `;
    });
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// =============================================================================
// PARTIDOS (fase de grupos — round-robin)
// =============================================================================
type PartidoRow = {
  id: number | bigint;
  grupo_id: number | bigint | null;
  equipo_local: number | bigint;
  equipo_visita: number | bigint;
  jornada: number;
  goles_local: number | null;
  goles_visita: number | null;
  faltas_local: number;
  faltas_visita: number;
  jugado: number;
  en_juego: number;
  ronda_bracket: string | null;
  bracket_pos: number | null;
  gol_oro: number;
  penales_local: number | null;
  penales_visita: number | null;
  ganador_id: number | bigint | null;
  notas: string | null;
  jugado_en: Date | null;
  local_nombre: string;
  visita_nombre: string;
  grupo_nombre: string | null;
  local_seed: string | null;
  visita_seed: string | null;
};

async function fetchPartidos(grupoId?: number, bracketOnly = false) {
  const where = bracketOnly
    ? Prisma.sql`AND p.ronda_bracket IS NOT NULL`
    : grupoId
      ? Prisma.sql`AND p.grupo_id = ${grupoId} AND p.ronda_bracket IS NULL`
      : Prisma.sql`AND p.ronda_bracket IS NULL`;
  return (
    await prisma.$queryRaw<PartidoRow[]>`
      SELECT p.id, p.grupo_id, p.equipo_local, p.equipo_visita,
             p.jornada, p.goles_local, p.goles_visita,
             p.faltas_local, p.faltas_visita, p.jugado, p.en_juego,
             p.ronda_bracket, p.bracket_pos, p.gol_oro,
             p.penales_local, p.penales_visita, p.ganador_id,
             p.notas, p.jugado_en,
             el.nombre AS local_nombre, ev.nombre AS visita_nombre,
             COALESCE(g.nombre, '') AS grupo_nombre,
             p.local_seed, p.visita_seed
      FROM robofut_partidos p
      JOIN robofut_equipos el ON el.id = p.equipo_local
      JOIN robofut_equipos ev ON ev.id = p.equipo_visita
      LEFT JOIN robofut_grupos g ON g.id = p.grupo_id
      WHERE 1=1 ${where}
      ORDER BY p.grupo_id, p.jornada, p.id
    `
  ).map((r) => ({
    ...r,
    id: num(r.id),
    grupo_id: r.grupo_id == null ? null : num(r.grupo_id),
    equipo_local: num(r.equipo_local),
    equipo_visita: num(r.equipo_visita),
    jornada: num(r.jornada),
    goles_local: r.goles_local == null ? null : num(r.goles_local),
    goles_visita: r.goles_visita == null ? null : num(r.goles_visita),
    faltas_local: num(r.faltas_local),
    faltas_visita: num(r.faltas_visita),
    jugado: num(r.jugado),
    en_juego: num(r.en_juego),
    bracket_pos: r.bracket_pos == null ? null : num(r.bracket_pos),
    gol_oro: num(r.gol_oro),
    penales_local: r.penales_local == null ? null : num(r.penales_local),
    penales_visita: r.penales_visita == null ? null : num(r.penales_visita),
    ganador_id: r.ganador_id == null ? null : num(r.ganador_id),
  }));
}

// Generar partidos round-robin para todos los grupos
robofutRouter.post("/partidos/generar", async (_req, res, next) => {
  try {
    const existing = await prisma.$queryRaw<{ n: number | bigint }[]>`
      SELECT COUNT(*) AS n FROM robofut_partidos
    `;
    if (num(existing[0].n) > 0) {
      throw new HttpError(409, "Ya hay partidos generados. Borrá los existentes primero.");
    }

    const grupos = await fetchGrupos();
    if (grupos.length === 0) throw new HttpError(400, "No hay grupos. Sorteá primero.");

    await prisma.$transaction(async (tx) => {
      for (const g of grupos) {
        const eqs = g.equipos.map((e: { id: number }) => e.id);
        const matches: { local: number; visita: number; jornada: number }[] = [];

        // Round-robin scheduling (circle method)
        const n = eqs.length;
        const teams = [...eqs];
        if (n % 2 === 1) teams.push(-1); // bye
        const rounds = teams.length - 1;
        const half = teams.length / 2;

        for (let r = 0; r < rounds; r++) {
          for (let i = 0; i < half; i++) {
            const home = teams[i];
            const away = teams[teams.length - 1 - i];
            if (home === -1 || away === -1) continue;
            matches.push({ local: home, visita: away, jornada: r + 1 });
          }
          // Rotate: fix first, rotate rest
          const last = teams.pop()!;
          teams.splice(1, 0, last);
        }

        for (const m of matches) {
          await tx.$executeRaw`
            INSERT INTO robofut_partidos (grupo_id, equipo_local, equipo_visita, jornada)
            VALUES (${g.id}, ${m.local}, ${m.visita}, ${m.jornada})
          `;
        }
      }

      await tx.$executeRaw`
        UPDATE robofut_competencia SET fase_actual = 'grupos' WHERE id = 1
      `;
    });

    res.json({ ok: true, partidos: await fetchPartidos() });
  } catch (e) { next(e); }
});

// Listar partidos (filtro opcional por grupo)
robofutRouter.get("/partidos", async (req, res, next) => {
  try {
    const grupoId = req.query.grupo_id ? Number(req.query.grupo_id) : undefined;
    res.json({ data: await fetchPartidos(grupoId) });
  } catch (e) { next(e); }
});

robofutRouter.patch("/partidos/:id/en-juego", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const { en_juego } = z.object({ en_juego: z.boolean() }).parse(req.body);
    await prisma.$executeRaw`
      UPDATE robofut_partidos SET en_juego = ${en_juego} WHERE id = ${id}
    `;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// Registrar resultado de un partido
const resultadoSchema = z.object({
  goles_local: z.coerce.number().int().min(0),
  goles_visita: z.coerce.number().int().min(0),
  faltas_local: z.coerce.number().int().min(0).default(0),
  faltas_visita: z.coerce.number().int().min(0).default(0),
  gol_oro: z.boolean().default(false),
  penales_local: z.coerce.number().int().min(0).nullable().optional(),
  penales_visita: z.coerce.number().int().min(0).nullable().optional(),
  ganador_id: z.coerce.number().int().positive().nullable().optional(),
  notas: z.string().max(500).nullable().optional(),
});

robofutRouter.patch("/partidos/:id", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    const body = resultadoSchema.parse(req.body);

    // Determine winner for bracket matches
    let ganadorId = body.ganador_id ?? null;
    if (!ganadorId) {
      if (body.goles_local > body.goles_visita) {
        const [row] = await prisma.$queryRaw<{ equipo_local: number | bigint }[]>`
          SELECT equipo_local FROM robofut_partidos WHERE id = ${id}
        `;
        ganadorId = num(row.equipo_local);
      } else if (body.goles_visita > body.goles_local) {
        const [row] = await prisma.$queryRaw<{ equipo_visita: number | bigint }[]>`
          SELECT equipo_visita FROM robofut_partidos WHERE id = ${id}
        `;
        ganadorId = num(row.equipo_visita);
      } else if (body.penales_local != null && body.penales_visita != null) {
        const [row] = await prisma.$queryRaw<{ equipo_local: number | bigint; equipo_visita: number | bigint }[]>`
          SELECT equipo_local, equipo_visita FROM robofut_partidos WHERE id = ${id}
        `;
        ganadorId = body.penales_local > body.penales_visita
          ? num(row.equipo_local) : num(row.equipo_visita);
      }
    }

    await prisma.$executeRaw`
      UPDATE robofut_partidos
      SET goles_local = ${body.goles_local},
          goles_visita = ${body.goles_visita},
          faltas_local = ${body.faltas_local},
          faltas_visita = ${body.faltas_visita},
          gol_oro = ${body.gol_oro},
          penales_local = ${body.penales_local ?? null},
          penales_visita = ${body.penales_visita ?? null},
          ganador_id = ${ganadorId},
          notas = ${body.notas ?? null},
          jugado = TRUE,
          jugado_en = NOW()
      WHERE id = ${id}
    `;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// Borrar resultado (marcar como no jugado)
robofutRouter.patch("/partidos/:id/reset", async (req, res, next) => {
  try {
    const id = idParam.parse(req.params.id);
    await prisma.$executeRaw`
      UPDATE robofut_partidos
      SET goles_local = NULL, goles_visita = NULL,
          faltas_local = 0, faltas_visita = 0,
          jugado = FALSE, jugado_en = NULL, notas = NULL
      WHERE id = ${id}
    `;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// Borrar todos los partidos
robofutRouter.delete("/partidos", async (_req, res, next) => {
  try {
    await prisma.$executeRaw`DELETE FROM robofut_partidos`;
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// Tabla de posiciones por grupo (FIFA scoring)
robofutRouter.get("/posiciones", async (_req, res, next) => {
  try {
    const partidos = await fetchPartidos();
    const grupos = await fetchGrupos();

    const posiciones: Record<number, {
      grupo_id: number;
      grupo_nombre: string;
      tabla: {
        equipo_id: number;
        equipo_nombre: string;
        pj: number; pg: number; pe: number; pp: number;
        gf: number; gc: number; dif: number; pts: number;
        faltas: number;
      }[];
    }> = {};

    for (const g of grupos) {
      const tabla = g.equipos.map((e: { id: number; nombre: string }) => ({
        equipo_id: e.id,
        equipo_nombre: e.nombre,
        pj: 0, pg: 0, pe: 0, pp: 0,
        gf: 0, gc: 0, dif: 0, pts: 0,
        faltas: 0,
      }));
      posiciones[g.id] = { grupo_id: g.id, grupo_nombre: g.nombre, tabla };
    }

    // Head-to-head map: key = "eqA-eqB" (sorted), value = { gdA, gdB }
    const h2h: Record<string, { a: number; b: number; gdA: number; gdB: number }> = {};

    for (const p of partidos) {
      if (!p.jugado || p.goles_local == null || p.goles_visita == null) continue;
      if (p.grupo_id == null) continue;
      const pos = posiciones[p.grupo_id];
      if (!pos) continue;

      const local = pos.tabla.find((t) => t.equipo_id === p.equipo_local);
      const visita = pos.tabla.find((t) => t.equipo_id === p.equipo_visita);
      if (!local || !visita) continue;

      local.pj++; visita.pj++;
      local.gf += p.goles_local; local.gc += p.goles_visita;
      visita.gf += p.goles_visita; visita.gc += p.goles_local;
      local.faltas += p.faltas_local; visita.faltas += p.faltas_visita;

      if (p.goles_local > p.goles_visita) {
        local.pg++; local.pts += 3; visita.pp++;
      } else if (p.goles_local < p.goles_visita) {
        visita.pg++; visita.pts += 3; local.pp++;
      } else {
        local.pe++; visita.pe++; local.pts += 1; visita.pts += 1;
      }

      // Head-to-head
      const ids = [p.equipo_local, p.equipo_visita].sort((a, b) => a - b);
      const key = `${ids[0]}-${ids[1]}`;
      if (!h2h[key]) h2h[key] = { a: ids[0], b: ids[1], gdA: 0, gdB: 0 };
      if (p.equipo_local === ids[0]) {
        h2h[key].gdA += p.goles_local - p.goles_visita;
        h2h[key].gdB += p.goles_visita - p.goles_local;
      } else {
        h2h[key].gdB += p.goles_local - p.goles_visita;
        h2h[key].gdA += p.goles_visita - p.goles_local;
      }
    }

    // Sort: pts desc > gf desc > h2h > faltas asc
    for (const pos of Object.values(posiciones)) {
      pos.tabla.forEach((t) => { t.dif = t.gf - t.gc; });
      pos.tabla.sort((a, b) => {
        if (b.pts !== a.pts) return b.pts - a.pts;
        if (b.gf !== a.gf) return b.gf - a.gf;
        // Head-to-head
        const ids = [a.equipo_id, b.equipo_id].sort((x, y) => x - y);
        const key = `${ids[0]}-${ids[1]}`;
        const rec = h2h[key];
        if (rec) {
          const gdA = a.equipo_id === rec.a ? rec.gdA : rec.gdB;
          const gdB = b.equipo_id === rec.a ? rec.gdA : rec.gdB;
          if (gdA !== gdB) return gdB - gdA;
        }
        // Fewer fouls is better
        return a.faltas - b.faltas;
      });
    }

    res.json({ data: Object.values(posiciones) });
  } catch (e) { next(e); }
});

// Ranking de comodines (mejores no-clasificados-directos)
robofutRouter.get("/comodines", async (_req, res, next) => {
  try {
    const partidos = await fetchPartidos();
    const grupos = await fetchGrupos();

    const [comp] = await prisma.$queryRaw<CompetenciaRow[]>`
      SELECT id, nombre, fase_actual, formato_elegido, voluntario_kit_id
      FROM robofut_competencia WHERE id = 1
    `;
    if (!comp.formato_elegido) {
      return res.json({ data: [], cupos: 0 });
    }
    const formato = typeof comp.formato_elegido === "string"
      ? JSON.parse(comp.formato_elegido) : comp.formato_elegido;

    let clasificadosPorGrupo: Map<number, number>;
    let cuposComodines: number;

    if ("cantidadGrupos" in formato) {
      cuposComodines = formato.cuposMejoresPosicionados;
      clasificadosPorGrupo = new Map();
      for (const g of grupos) clasificadosPorGrupo.set(g.id, formato.clasificadosFijosPorGrupo);
    } else {
      cuposComodines = formato.comodines;
      clasificadosPorGrupo = new Map();
      const sorted = [...grupos].sort((a, b) => a.orden - b.orden);
      const nGrandes = formato.gruposGrandes.cantidad;
      const nPeq = formato.gruposPequeños?.cantidad ?? (sorted.length - nGrandes);
      const perGrande = Math.floor(formato.gruposGrandes.pasesDirectos / nGrandes);
      const perPeq = nPeq > 0 ? Math.floor(formato.gruposPequeños.pasesDirectos / nPeq) : 0;
      for (let i = 0; i < sorted.length; i++) {
        clasificadosPorGrupo.set(sorted[i].id, i < nGrandes ? perGrande : perPeq);
      }
    }

    const grupoNombre = new Map<number, string>(grupos.map((g: { id: number; nombre: string }) => [g.id, g.nombre]));
    type ComodinRow = {
      equipo_id: number; equipo_nombre: string;
      grupo_id: number; grupo_nombre: string;
      pos_en_grupo: number;
      pj: number; pg: number; pe: number; pp: number;
      gf: number; gc: number; dif: number; pts: number; faltas: number;
      clasifica: boolean;
    };
    const noClasificados: ComodinRow[] = [];

    for (const g of grupos) {
      const tabla = g.equipos.map((e: { id: number; nombre: string }) => ({
        equipo_id: e.id, equipo_nombre: e.nombre,
        pj: 0, pg: 0, pe: 0, pp: 0, gf: 0, gc: 0, dif: 0, pts: 0, faltas: 0,
      }));
      for (const p of partidos) {
        if (p.grupo_id !== g.id || !p.jugado || p.goles_local == null || p.goles_visita == null) continue;
        const local = tabla.find((t) => t.equipo_id === p.equipo_local);
        const visita = tabla.find((t) => t.equipo_id === p.equipo_visita);
        if (!local || !visita) continue;
        local.pj++; visita.pj++;
        local.gf += p.goles_local; local.gc += p.goles_visita;
        visita.gf += p.goles_visita; visita.gc += p.goles_local;
        local.faltas += p.faltas_local; visita.faltas += p.faltas_visita;
        if (p.goles_local > p.goles_visita) { local.pg++; local.pts += 3; visita.pp++; }
        else if (p.goles_local < p.goles_visita) { visita.pg++; visita.pts += 3; local.pp++; }
        else { local.pe++; visita.pe++; local.pts++; visita.pts++; }
      }
      tabla.forEach((t) => { t.dif = t.gf - t.gc; });
      tabla.sort((a, b) => b.pts - a.pts || b.dif - a.dif || b.gf - a.gf || a.faltas - b.faltas);
      const nDirect = clasificadosPorGrupo.get(g.id) ?? 0;
      for (let i = nDirect; i < tabla.length; i++) {
        noClasificados.push({
          ...tabla[i],
          grupo_id: g.id,
          grupo_nombre: grupoNombre.get(g.id) ?? "",
          pos_en_grupo: i + 1,
          clasifica: false,
        });
      }
    }

    noClasificados.sort((a, b) => b.pts - a.pts || b.dif - a.dif || b.gf - a.gf || a.faltas - b.faltas);
    const comodines = noClasificados.slice(0, cuposComodines);
    for (const c of comodines) c.clasifica = true;

    res.json({ data: comodines, cupos: cuposComodines });
  } catch (e) { next(e); }
});

// Simular todos los partidos no jugados con resultados aleatorios
robofutRouter.post("/partidos/simular", async (_req, res, next) => {
  try {
    const noJugados = await prisma.$queryRaw<{ id: number | bigint }[]>`
      SELECT id FROM robofut_partidos WHERE jugado = FALSE
    `;
    if (noJugados.length === 0) {
      throw new HttpError(400, "No hay partidos pendientes para simular.");
    }

    await prisma.$transaction(async (tx) => {
      for (const p of noJugados) {
        const gl = Math.floor(Math.random() * 6);
        const gv = Math.floor(Math.random() * 6);
        const fl = Math.floor(Math.random() * 4);
        const fv = Math.floor(Math.random() * 4);
        await tx.$executeRaw`
          UPDATE robofut_partidos
          SET goles_local = ${gl}, goles_visita = ${gv},
              faltas_local = ${fl}, faltas_visita = ${fv},
              jugado = TRUE, jugado_en = NOW(),
              notas = 'Simulado'
          WHERE id = ${num(p.id)}
        `;
      }
    });

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

// =============================================================================
// ELIMINATORIA (bracket)
// =============================================================================

// Clasificar equipos y generar bracket de octavos de final
robofutRouter.post("/bracket/generar", async (_req, res, next) => {
  try {
    const existingBracket = await prisma.$queryRaw<{ n: number | bigint }[]>`
      SELECT COUNT(*) AS n FROM robofut_partidos WHERE ronda_bracket IS NOT NULL
    `;
    if (num(existingBracket[0].n) > 0) {
      throw new HttpError(409, "Ya hay partidos de eliminatoria. Borrá los existentes primero.");
    }

    // Get format to know how many classify per group + wildcards
    const [comp] = await prisma.$queryRaw<CompetenciaRow[]>`
      SELECT id, nombre, fase_actual, formato_elegido, voluntario_kit_id
      FROM robofut_competencia WHERE id = 1
    `;
    if (!comp.formato_elegido) {
      throw new HttpError(400, "No hay formato elegido.");
    }
    const formato = typeof comp.formato_elegido === "string"
      ? JSON.parse(comp.formato_elegido) : comp.formato_elegido;

    // Compute classification rules from format
    let clasificadosPorGrupo: Map<number, number>;
    let comodines: number;

    // Get posiciones (reuse the logic)
    const partidos = await fetchPartidos();
    const grupos = await fetchGrupos();

    // Check all group matches are played
    const pendientes = partidos.filter((p) => !p.jugado);
    if (pendientes.length > 0) {
      throw new HttpError(400, `Faltan ${pendientes.length} partidos de fase de grupos por jugar.`);
    }

    // Build standings (same logic as /posiciones)
    type StandingRow = {
      equipo_id: number; equipo_nombre: string; grupo_id: number;
      pj: number; pg: number; pe: number; pp: number;
      gf: number; gc: number; dif: number; pts: number; faltas: number;
    };
    const allStandings: StandingRow[] = [];

    for (const g of grupos) {
      const tabla = g.equipos.map((e: { id: number; nombre: string }) => ({
        equipo_id: e.id, equipo_nombre: e.nombre, grupo_id: g.id,
        pj: 0, pg: 0, pe: 0, pp: 0, gf: 0, gc: 0, dif: 0, pts: 0, faltas: 0,
      }));

      for (const p of partidos) {
        if (p.grupo_id !== g.id || !p.jugado || p.goles_local == null || p.goles_visita == null) continue;
        const local = tabla.find((t) => t.equipo_id === p.equipo_local);
        const visita = tabla.find((t) => t.equipo_id === p.equipo_visita);
        if (!local || !visita) continue;

        local.pj++; visita.pj++;
        local.gf += p.goles_local; local.gc += p.goles_visita;
        visita.gf += p.goles_visita; visita.gc += p.goles_local;
        local.faltas += p.faltas_local; visita.faltas += p.faltas_visita;

        if (p.goles_local > p.goles_visita) { local.pg++; local.pts += 3; visita.pp++; }
        else if (p.goles_local < p.goles_visita) { visita.pg++; visita.pts += 3; local.pp++; }
        else { local.pe++; visita.pe++; local.pts += 1; visita.pts += 1; }
      }

      tabla.forEach((t) => { t.dif = t.gf - t.gc; });
      tabla.sort((a, b) => {
        if (b.pts !== a.pts) return b.pts - a.pts;
        if (b.gf !== a.gf) return b.gf - a.gf;
        return a.faltas - b.faltas;
      });
      allStandings.push(...tabla);
    }

    // Classify: direct qualifiers per group + wildcards
    if ("cantidadGrupos" in formato) {
      const perGroup = formato.clasificadosFijosPorGrupo;
      comodines = formato.cuposMejoresPosicionados;
      clasificadosPorGrupo = new Map();
      for (const g of grupos) clasificadosPorGrupo.set(g.id, perGroup);
    } else {
      comodines = formato.comodines;
      clasificadosPorGrupo = new Map();
      const sorted = [...grupos].sort((a, b) => a.orden - b.orden);
      const nGrandes = formato.gruposGrandes.cantidad;
      const perGrande = Math.floor(formato.gruposGrandes.pasesDirectos / formato.gruposGrandes.cantidad);
      const nPeq = formato.gruposPequeños?.cantidad ?? 0;
      const perPeq = nPeq > 0 ? Math.floor(formato.gruposPequeños.pasesDirectos / nPeq) : 0;
      for (let i = 0; i < sorted.length; i++) {
        const perGroup = i < nGrandes ? perGrande : perPeq;
        clasificadosPorGrupo.set(sorted[i].id, perGroup);
      }
    }

    const clasificados: number[] = [];
    const seedLabels = new Map<number, string>();
    const noClasificados: StandingRow[] = [];

    // Extract letter from group name: "Grupo A" -> "A", "Grupo C" -> "C"
    const grupoLetra = new Map<number, string>();
    for (const g of grupos) {
      const match = g.nombre.match(/[A-Z]$/);
      grupoLetra.set(g.id, match ? match[0] : g.nombre.slice(0, 2));
    }

    for (const g of grupos) {
      const gStandings = allStandings.filter((s) => s.grupo_id === g.id);
      const nDirect = clasificadosPorGrupo.get(g.id) ?? 0;
      const letra = grupoLetra.get(g.id) ?? "?";
      for (let i = 0; i < gStandings.length; i++) {
        if (i < nDirect) {
          clasificados.push(gStandings[i].equipo_id);
          seedLabels.set(gStandings[i].equipo_id, `${i + 1}${letra}`);
        } else {
          noClasificados.push(gStandings[i]);
        }
      }
    }

    // Wildcards: best non-classified
    if (comodines > 0) {
      noClasificados.sort((a, b) => {
        if (b.pts !== a.pts) return b.pts - a.pts;
        if (b.dif !== a.dif) return b.dif - a.dif;
        if (b.gf !== a.gf) return b.gf - a.gf;
        return a.faltas - b.faltas;
      });
      for (let i = 0; i < Math.min(comodines, noClasificados.length); i++) {
        clasificados.push(noClasificados[i].equipo_id);
        seedLabels.set(noClasificados[i].equipo_id, `CM${i + 1}`);
      }
    }

    if (clasificados.length < 2) {
      throw new HttpError(400, "No hay suficientes equipos clasificados para armar un bracket.");
    }

    // Determine bracket size (round to next power of 2 up to 16)
    const target = Math.min(clasificados.length, 16);
    let bracketSize = 2;
    while (bracketSize < target) bracketSize *= 2;

    // Seed matchups: 1v16, 2v15, etc. (or 1v8, 2v7 for smaller brackets)
    const seeded = clasificados.slice(0, bracketSize);

    const rondaName = bracketSize === 16 ? "octavos"
      : bracketSize === 8 ? "cuartos"
      : bracketSize === 4 ? "semi"
      : "final";

    const matchups: { local: number; visita: number; pos: number }[] = [];
    for (let i = 0; i < bracketSize / 2; i++) {
      matchups.push({
        local: seeded[i],
        visita: seeded[bracketSize - 1 - i],
        pos: i + 1,
      });
    }

    await prisma.$transaction(async (tx) => {
      for (const m of matchups) {
        const localSeed = seedLabels.get(m.local) ?? null;
        const visitaSeed = seedLabels.get(m.visita) ?? null;
        await tx.$executeRaw`
          INSERT INTO robofut_partidos
            (grupo_id, equipo_local, equipo_visita, jornada, ronda_bracket, bracket_pos, local_seed, visita_seed)
          VALUES (NULL, ${m.local}, ${m.visita}, 1, ${rondaName}, ${m.pos}, ${localSeed}, ${visitaSeed})
        `;
      }
      await tx.$executeRaw`
        UPDATE robofut_competencia SET fase_actual = 'bracket' WHERE id = 1
      `;
    });

    res.json({
      ok: true,
      clasificados: clasificados.length,
      ronda: rondaName,
      partidos: await fetchPartidos(undefined, true),
    });
  } catch (e) { next(e); }
});

// Avanzar bracket: genera la siguiente ronda con los ganadores
robofutRouter.post("/bracket/avanzar", async (_req, res, next) => {
  try {
    const bracketPartidos = await fetchPartidos(undefined, true);
    if (bracketPartidos.length === 0) {
      throw new HttpError(400, "No hay partidos de eliminatoria.");
    }

    // Find the latest round
    const rondaOrder = ["octavos", "cuartos", "semi", "final", "tercer_puesto"];
    let latestRonda = "";
    let latestIdx = -1;
    for (const p of bracketPartidos) {
      if (p.ronda_bracket) {
        const idx = rondaOrder.indexOf(p.ronda_bracket);
        if (idx > latestIdx) { latestIdx = idx; latestRonda = p.ronda_bracket; }
      }
    }

    if (latestRonda === "final" || latestRonda === "tercer_puesto") {
      throw new HttpError(400, "El bracket ya está completo.");
    }

    const currentRoundMatches = bracketPartidos.filter(
      (p) => p.ronda_bracket === latestRonda
    );
    const pendientes = currentRoundMatches.filter((p) => !p.jugado || !p.ganador_id);
    if (pendientes.length > 0) {
      throw new HttpError(400, `Faltan ${pendientes.length} partidos de ${latestRonda} por definir.`);
    }

    // Get winners sorted by bracket_pos
    const sorted = [...currentRoundMatches].sort(
      (a, b) => (a.bracket_pos ?? 0) - (b.bracket_pos ?? 0)
    );
    const winners = sorted.map((p) => p.ganador_id!);

    const nextRonda = rondaOrder[latestIdx + 1];
    if (!nextRonda || nextRonda === "tercer_puesto") {
      throw new HttpError(400, "No hay siguiente ronda.");
    }

    // Pair winners: 1st vs 2nd, 3rd vs 4th, etc.
    const matchups: { local: number; visita: number; pos: number }[] = [];
    for (let i = 0; i < winners.length; i += 2) {
      if (i + 1 < winners.length) {
        matchups.push({ local: winners[i], visita: winners[i + 1], pos: i / 2 + 1 });
      }
    }

    // If going to final (2 matches in semi), also create tercer_puesto
    const losers = sorted.map((p) => {
      const w = p.ganador_id!;
      return w === p.equipo_local ? p.equipo_visita : p.equipo_local;
    });

    await prisma.$transaction(async (tx) => {
      for (const m of matchups) {
        await tx.$executeRaw`
          INSERT INTO robofut_partidos
            (grupo_id, equipo_local, equipo_visita, jornada, ronda_bracket, bracket_pos)
          VALUES (NULL, ${m.local}, ${m.visita}, 1, ${nextRonda}, ${m.pos})
        `;
      }

      // Create 3rd place match when generating final
      if (nextRonda === "final" && losers.length === 2) {
        await tx.$executeRaw`
          INSERT INTO robofut_partidos
            (grupo_id, equipo_local, equipo_visita, jornada, ronda_bracket, bracket_pos)
          VALUES (NULL, ${losers[0]}, ${losers[1]}, 1, 'tercer_puesto', 1)
        `;
      }
    });

    res.json({
      ok: true,
      ronda: nextRonda,
      partidos: await fetchPartidos(undefined, true),
    });
  } catch (e) { next(e); }
});

// Listar partidos de bracket
robofutRouter.get("/bracket", async (_req, res, next) => {
  try {
    res.json({ data: await fetchPartidos(undefined, true) });
  } catch (e) { next(e); }
});

// Borrar todo el bracket
robofutRouter.delete("/bracket", async (_req, res, next) => {
  try {
    await prisma.$transaction(async (tx) => {
      await tx.$executeRaw`DELETE FROM robofut_partidos WHERE ronda_bracket IS NOT NULL`;
      await tx.$executeRaw`
        UPDATE robofut_competencia SET fase_actual = 'grupos' WHERE id = 1
      `;
    });
    res.json({ ok: true });
  } catch (e) { next(e); }
});

// Simular bracket: simula todos los partidos no jugados del bracket
robofutRouter.post("/bracket/simular", async (_req, res, next) => {
  try {
    const noJugados = await prisma.$queryRaw<{ id: number | bigint; equipo_local: number | bigint; equipo_visita: number | bigint }[]>`
      SELECT id, equipo_local, equipo_visita FROM robofut_partidos
      WHERE ronda_bracket IS NOT NULL AND jugado = FALSE
    `;
    if (noJugados.length === 0) {
      throw new HttpError(400, "No hay partidos de bracket pendientes.");
    }

    await prisma.$transaction(async (tx) => {
      for (const p of noJugados) {
        let gl = Math.floor(Math.random() * 5);
        let gv = Math.floor(Math.random() * 5);
        let pl: number | null = null;
        let pv: number | null = null;
        let ganador: number;
        if (gl === gv) {
          // Penales
          pl = Math.floor(Math.random() * 4) + 2;
          pv = pl === 2 ? 3 : Math.floor(Math.random() * 4) + 1;
          if (pl === pv) pv = pl + 1;
          ganador = pl > pv ? num(p.equipo_local) : num(p.equipo_visita);
        } else {
          ganador = gl > gv ? num(p.equipo_local) : num(p.equipo_visita);
        }
        await tx.$executeRaw`
          UPDATE robofut_partidos
          SET goles_local = ${gl}, goles_visita = ${gv},
              faltas_local = 0, faltas_visita = 0,
              penales_local = ${pl}, penales_visita = ${pv},
              ganador_id = ${ganador},
              jugado = TRUE, jugado_en = NOW(), notas = 'Simulado'
          WHERE id = ${num(p.id)}
        `;
      }
    });

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