// Motor puro de STEM SR. Sin BD, sin Express: solo la lógica del torneo.
// Toda la persistencia vive en stemsr.routes.ts; este archivo solo calcula.
// Referencia: PLAN_MODULO_STEMSR.md

// ─── Constantes de la fase de grupos ─────────────────────────────────────────
export const ALLIANCE_COUNT = 9; // A1..A9
export const ALLIANCE_SIZE = 6; // kits por alianza en grupos
export const BLOCKS = 2; // bloque 1 y 2
export const PARTIDAS_POR_RONDA = 3;
export const TOTAL_KITS = ALLIANCE_COUNT * ALLIANCE_SIZE; // 54

// Multiplicador de canje (fijado por el usuario).
export const MULTIPLICADORES = {
  basico: 0.6,
  intermedio: 0.8,
  completo: 1.0,
} as const;
export type Multiplicador = keyof typeof MULTIPLICADORES;

export const COOPERTITION_BONUS = 5;
export const FACTOR_CREDITOS = 0.3; // créditos × 0.3 (dentro del core)
export const FACTOR_CO2_ECO = 0.15; // CO₂ ecosistema, se resta dentro del core (escalado)
export const FACTOR_CO2_SR = 0.15; // CO₂ sin recoger, se resta al final (sin escalar)
export const ESCALA = 100;

// Rotación dentro de una ronda de grupos: qué posiciones (1..6) juegan en
// cada partida. Cada posición juega 2 y descansa 1.
export const ROTACION_GRUPOS: Record<number, number[]> = {
  1: [1, 2, 3, 4],
  2: [5, 6, 1, 2],
  3: [3, 4, 5, 6],
};

// ─── Rebaraje determinista ───────────────────────────────────────────────────
// Para el kit con índice n (0..53) en el bloque b (0 = bloque 1, 1 = bloque 2),
// devuelve el índice de alianza 0..8. Mantiene alianzas de 6 exactas.
export function allianceOf(n: number, b: number): number {
  return (Math.floor(n / ALLIANCE_SIZE) + ((n % ALLIANCE_SIZE) + 1) * b) % ALLIANCE_COUNT;
}

export function allianceCode(idx0: number): string {
  return `A${idx0 + 1}`;
}

export type Miembro = { codigo: string; posicion: number; seedIndex: number };

// Membresía de un bloque: para cada alianza, sus kits ordenados en posiciones 1..6.
// El número de bloque es 1 o 2 (se traduce a b = bloque - 1 internamente).
export function blockMembership(bloque: number): Miembro[] {
  const b = bloque - 1;
  const grupos = new Map<number, number[]>();
  for (let n = 0; n < TOTAL_KITS; n++) {
    const a = allianceOf(n, b);
    if (!grupos.has(a)) grupos.set(a, []);
    grupos.get(a)!.push(n);
  }
  const out: Miembro[] = [];
  for (const [a, seeds] of grupos) {
    seeds.sort((x, y) => x - y);
    seeds.forEach((seedIndex, i) => {
      out.push({ codigo: allianceCode(a), posicion: i + 1, seedIndex });
    });
  }
  return out;
}

// ─── Calendario de un bloque ─────────────────────────────────────────────────
// Ciclo A1–A2 · A2–A3 · … · A9–A1. 9 rondas, cada alianza aparece 2 veces.
export type RondaPlan = { indice: number; a: string; b: string };

export function buildBlockSchedule(): RondaPlan[] {
  const rondas: RondaPlan[] = [];
  for (let i = 0; i < ALLIANCE_COUNT; i++) {
    rondas.push({
      indice: i + 1,
      a: allianceCode(i),
      b: allianceCode((i + 1) % ALLIANCE_COUNT),
    });
  }
  return rondas;
}

// ─── Orden de juego interleaved ──────────────────────────────────────────────
// Dentro de un bloque se juega la partida 1 de todas las rondas, luego la 2,
// luego la 3. orden_juego es 1-based dentro del bloque.
export function playOrder(rondaIndice: number, indicePartida: number): number {
  return (indicePartida - 1) * ALLIANCE_COUNT + rondaIndice;
}

// Posiciones que juegan en una partida de grupos.
export function rotationPositions(indicePartida: number): number[] {
  return ROTACION_GRUPOS[indicePartida] ?? [];
}

// ─── Cálculo de puntaje ──────────────────────────────────────────────────────
export type ScoreInput = {
  creditosCarbono: number;
  multiplicador: Multiplicador;
  coopertition: boolean; // compartido por partida
  co2SinRecoger: number; // compartido por partida
  co2Ecosistema: number; // por alianza
  penalizaciones: { puntos: number; cantidad: number }[]; // puntos negativos
};

export function round2(x: number): number {
  return Math.round((x + Number.EPSILON) * 100) / 100;
}

export function scoreAlianza(input: ScoreInput): number {
  const mult = MULTIPLICADORES[input.multiplicador] ?? 0;
  // ((créditos × 0.3) − (CO₂ ecosistema × 0.15)) × multiplicador × 100
  const core =
    (input.creditosCarbono * FACTOR_CREDITOS - input.co2Ecosistema * FACTOR_CO2_ECO) *
    mult *
    ESCALA;
  const co2sr = -(input.co2SinRecoger * FACTOR_CO2_SR); // se resta al final
  const coop = input.coopertition ? COOPERTITION_BONUS : 0;
  const penal = input.penalizaciones.reduce(
    (acc, p) => acc + p.puntos * p.cantidad,
    0
  );
  const total = round2(core + co2sr + coop + penal);
  // El puntaje total no puede ser negativo: si lo es, queda en 0.
  return total < 0 ? 0 : total;
}

// ─── Orden por colegio para el bloque 1 ──────────────────────────────────────
// Reparte los kits en `alianzas` cubetas de `size` minimizando que coincidan
// kits del mismo colegio en la misma alianza. Reparte round-robin entre
// alianzas, procesando los colegios más grandes primero, con un puntero global
// que arrastra el offset entre colegios (así no todos empiezan en A1).
// Devuelve los items reordenados: las primeras `size` posiciones son la alianza
// A1, las siguientes la A2, etc. (que es lo que consume el rebaraje, b=0).
export function balancedSchoolOrder<T>(
  items: T[],
  schoolOf: (it: T) => string | null | undefined,
  size = ALLIANCE_SIZE,
  alianzas = ALLIANCE_COUNT
): T[] {
  const groups = new Map<string, T[]>();
  for (const it of items) {
    const k = schoolOf(it) || "__sin_colegio__";
    if (!groups.has(k)) groups.set(k, []);
    groups.get(k)!.push(it);
  }
  // colegios más grandes primero; desempate estable por nombre
  const schools = [...groups.entries()].sort(
    (a, b) => b[1].length - a[1].length || (a[0] < b[0] ? -1 : 1)
  );
  const buckets: T[][] = Array.from({ length: alianzas }, () => []);
  let ptr = 0;
  for (const [, kits] of schools) {
    for (const kit of kits) {
      let guard = 0;
      while (buckets[ptr].length >= size && guard < alianzas) {
        ptr = (ptr + 1) % alianzas;
        guard++;
      }
      buckets[ptr].push(kit);
      ptr = (ptr + 1) % alianzas;
    }
  }
  return buckets.flat();
}

// Cruces sembrados de la ronda de 10 (por número de seed/alianza):
// A1–A10 · A2–A9 · A3–A8 · A4–A7 · A5–A6. El primero de cada par es el
// mejor sembrado (alianza_a).
export const CRUCES_R10: [number, number][] = [
  [1, 10],
  [2, 9],
  [3, 8],
  [4, 7],
  [5, 6],
];

// ─── Reparto serpentina (fase de eliminación) ───────────────────────────────
// Recibe los ids/seed de los 40 kits en orden de ranking (mejor primero) y
// devuelve 10 alianzas de 4. A1:1,20,21,40 · … · A10:10,11,30,31.
export function snakeDistribution<T>(ranked: T[], numAlianzas = 10): T[][] {
  const alianzas: T[][] = Array.from({ length: numAlianzas }, () => []);
  ranked.forEach((item, i) => {
    const vuelta = Math.floor(i / numAlianzas); // 0..3
    const pos = i % numAlianzas;
    const idx = vuelta % 2 === 0 ? pos : numAlianzas - 1 - pos; // serpentina
    alianzas[idx].push(item);
  });
  return alianzas;
}
