/**
 * Ordenação e desempate — **portados** do `ranking.service.ts` do NestJS.
 *
 * Cópia deliberada, pelo mesmo motivo da pontuação: a regra já era estática e pura,
 * com 11 casos do plano `PR-5` cobrindo empate, critério exibido e posição
 * compartilhada. Um deles nasceu de um defeito real — `tiebreakOrder` era
 * persistido, editável e herdado na recriação, e o cálculo **ignorava**,
 * classificando e explicando pela regra errada em silêncio.
 */

export type CriterioDesempate = 'EXACT_SCORES' | 'WINNERS';
export type CriterioAplicado =
  | 'PONTOS'
  | CriterioDesempate
  | 'EMPATE_PERSISTE';

export interface Agregado {
  userId: string;
  displayName: string;
  points: number;
  exactScores: number;
  winners: number;
}

export interface LinhaDaClassificacao extends Agregado {
  position: number;
  decidedBy: CriterioAplicado;
  sharedPosition: boolean;
}

const ORDEM_PADRAO: CriterioDesempate[] = ['EXACT_SCORES', 'WINNERS'];

const COMPARADOR: Record<
  CriterioDesempate,
  (a: Agregado, b: Agregado) => number
> = {
  EXACT_SCORES: (a, b) => b.exactScores - a.exactScores,
  WINNERS: (a, b) => b.winners - a.winners,
};

/**
 * Classifica os agregados aplicando a **ordem de desempate do bolão**.
 *
 * Três decisões que vieram de defeito real e não devem ser "simplificadas":
 *
 * 1. **Ordem configurada, com queda para o padrão.** Bolão com ordem vazia ou
 *    critério desconhecido não pode ficar sem desempate nenhum.
 * 2. **O critério é olhado para os dois lados.** Comparar só com o anterior
 *    deixava quem está **na frente** do empate sem explicação — e é justamente
 *    quem é acusado de estar na frente sem motivo.
 * 3. **Empate que resiste a tudo divide a posição**, e a posição seguinte pula:
 *    1º, 1º, 3º. Quem ficou atrás de dois empatados está em terceiro.
 */
export function classificar(
  agregados: Agregado[],
  ordem: CriterioDesempate[] = ORDEM_PADRAO,
): LinhaDaClassificacao[] {
  const criterios = ordem.filter((c) => c in COMPARADOR);
  const usados = criterios.length > 0 ? criterios : ORDEM_PADRAO;

  const ordenado = [...agregados].sort((a, b) => {
    if (b.points !== a.points) return b.points - a.points;
    for (const criterio of usados) {
      const diff = COMPARADOR[criterio](a, b);
      if (diff !== 0) return diff;
    }
    // Último recurso: nome. Determinismo importa — ranking que muda de ordem a
    // cada leitura, com os mesmos pontos, parece defeito de apuração.
    return a.displayName.localeCompare(b.displayName);
  });

  const empatadoCom = (a: Agregado, b: Agregado) =>
    a.points === b.points &&
    usados.every((criterio) => COMPARADOR[criterio](a, b) === 0);

  const linhas: LinhaDaClassificacao[] = ordenado.map((atual, i) => {
    const anterior = i > 0 ? ordenado[i - 1]! : null;
    const dividindo = anterior !== null && empatadoCom(anterior, atual);
    const posicao = dividindo ? -1 : i + 1;

    const proximo = i + 1 < ordenado.length ? ordenado[i + 1]! : null;
    const vizinho =
      anterior && anterior.points === atual.points
        ? anterior
        : proximo && proximo.points === atual.points
          ? proximo
          : anterior;

    let decidedBy: CriterioAplicado = 'PONTOS';
    if (vizinho && vizinho.points === atual.points) {
      // O critério exibido tem de ser o **primeiro da ordem do bolão** que
      // separou os dois. Com a ordem invertida, dizer "placares exatos" seria
      // explicar a classificação pela regra errada.
      const decisivo = usados.find(
        (criterio) => COMPARADOR[criterio](atual, vizinho) !== 0,
      );
      decidedBy = decisivo ?? 'EMPATE_PERSISTE';
    }

    return { ...atual, position: posicao, decidedBy, sharedPosition: false };
  });

  // Segunda passada para a posição herdada do empate: `-1` marca "igual ao
  // anterior", e só aqui dá para resolver em cadeia (três empatados seguidos).
  linhas.forEach((linha, i) => {
    if (linha.position === -1) linha.position = linhas[i - 1]!.position;
  });

  const contagem = new Map<number, number>();
  linhas.forEach((l) => contagem.set(l.position, (contagem.get(l.position) ?? 0) + 1));
  linhas.forEach((l) => {
    l.sharedPosition = (contagem.get(l.position) ?? 0) > 1;
  });

  return linhas;
}

/**
 * Campeões: posição 1 **com ponto**.
 *
 * O `points > 0` veio de defeito real (`CV-1-09`): bolão em que ninguém acertou
 * nada declarava todo mundo co-campeão — pior que não ter campeão, porque inventa
 * um título de zero ponto para a galera inteira.
 */
export function campeoes(
  linhas: LinhaDaClassificacao[],
): LinhaDaClassificacao[] {
  return linhas.filter((l) => l.position === 1 && l.points > 0);
}
