export interface OrderableMatch {
  groupId: string;
  teamA: string;
  teamB: string;
}

const BYE = "__BYE__";

function pairKey(a: string, b: string): string {
  return a < b ? `${a}|${b}` : `${b}|${a}`;
}

function circleMethodRoundIndex(teams: string[]): Map<string, number> {
  const sorted = [...teams].sort();
  const arr = [...sorted];
  if (arr.length % 2 === 1) arr.push(BYE);

  const n = arr.length;
  const result = new Map<string, number>();
  if (n < 2) return result;

  const rounds = n - 1;
  const half = n / 2;
  let list = [...arr];

  for (let r = 0; r < rounds; r++) {
    for (let i = 0; i < half; i++) {
      const a = list[i];
      const b = list[n - 1 - i];
      if (a !== BYE && b !== BYE) {
        result.set(pairKey(a, b), r);
      }
    }
    list = [list[0], list[n - 1], ...list.slice(1, n - 1)];
  }

  return result;
}

interface AnnotatedMatch {
  idx: number;
  groupId: string;
  teamA: string;
  teamB: string;
  roundIndex: number;
  key: string;
}

function isBetter(a: number[], aKey: string, b: number[], bKey: string): boolean {
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return a[i] > b[i];
  }
  return aKey < bKey;
}

export function computeGroupStageOrder(matches: OrderableMatch[]): number[] {
  const n = matches.length;
  if (n <= 1) return matches.map((_, i) => i);

  const groupTeams = new Map<string, Set<string>>();
  for (const m of matches) {
    let set = groupTeams.get(m.groupId);
    if (!set) {
      set = new Set<string>();
      groupTeams.set(m.groupId, set);
    }
    set.add(m.teamA);
    set.add(m.teamB);
  }

  const roundIndexByGroup = new Map<string, Map<string, number>>();
  for (const [groupId, teams] of groupTeams) {
    roundIndexByGroup.set(groupId, circleMethodRoundIndex([...teams]));
  }

  const items: AnnotatedMatch[] = matches.map((m, idx) => {
    const round = roundIndexByGroup.get(m.groupId)?.get(pairKey(m.teamA, m.teamB));
    return {
      idx,
      groupId: m.groupId,
      teamA: m.teamA,
      teamB: m.teamB,
      roundIndex: round ?? Number.MAX_SAFE_INTEGER,
      key: `${m.groupId}|${pairKey(m.teamA, m.teamB)}`,
    };
  });

  const remaining = new Set<number>(items.map((it) => it.idx));
  const itemByIdx = new Map<number, AnnotatedMatch>(items.map((it) => [it.idx, it]));

  const order: number[] = [];
  const lastPlayed = new Map<string, number>();
  const NEVER = -1 - n;
  let prevTeams = new Set<string>();
  let prevGroup: string | null = null;

  for (let pos = 0; pos < n; pos++) {
    let best: AnnotatedMatch | null = null;
    let bestScore: number[] | null = null;

    for (const idx of remaining) {
      const it = itemByIdx.get(idx)!;

      const adjacent = prevTeams.has(it.teamA) || prevTeams.has(it.teamB) ? 1 : 0;
      const gapA = pos - (lastPlayed.get(it.teamA) ?? NEVER);
      const gapB = pos - (lastPlayed.get(it.teamB) ?? NEVER);
      const gap = Math.min(gapA, gapB);
      const sameGroup = prevGroup !== null && it.groupId === prevGroup ? 1 : 0;
      const score = [-adjacent, gap, -sameGroup, -it.roundIndex];

      if (best === null || isBetter(score, it.key, bestScore!, best.key)) {
        best = it;
        bestScore = score;
      }
    }

    order.push(best!.idx);
    remaining.delete(best!.idx);
    lastPlayed.set(best!.teamA, pos);
    lastPlayed.set(best!.teamB, pos);
    prevTeams = new Set([best!.teamA, best!.teamB]);
    prevGroup = best!.groupId;
  }

  return order;
}

export function orderGroupStageMatches<T>(
  items: T[],
  accessor: (item: T) => OrderableMatch,
): T[] {
  const orderable = items.map(accessor);
  const order = computeGroupStageOrder(orderable);
  return order.map((idx) => items[idx]);
}
