/**
 * Round question selection (ARCHITECTURE §2.1, §2.7). Deterministic for a given seed and pool,
 * independent of the pool's order (candidates are sorted by id before any seeded choice).
 * `exclude` holds ids already used tonight: they are avoided while fresh questions remain and
 * reused only when a category (or the pool) runs dry, so a long always-on night never stalls.
 */
import { createRng, pick, shuffle } from './rng';
import { CATEGORY_COUNT, ROUND_RULES, type CategoryDef, type Id, type QuestionInput, type RoundKind } from './types';

export interface SelectRoundQuestionsInput {
  kind: RoundKind;
  pool: readonly QuestionInput[];
  /** The pack's categories (board rounds need exactly 9). */
  categories: readonly CategoryDef[];
  seed: string;
  /** Question ids to avoid if possible (e.g. used earlier in the session). */
  exclude?: Iterable<Id>;
}

const TABLE_WARS_MAX_PER_CATEGORY = 2;

const byId = (a: QuestionInput, b: QuestionInput): number => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);

/** Questions in schedule order for a round of `kind`. Throws a descriptive Error if impossible. */
export function selectRoundQuestions(input: SelectRoundQuestionsInput): QuestionInput[] {
  const excluded = new Set(input.exclude ?? []);
  const categoryKeys = new Set(input.categories.map((c) => c.key));
  // Duplicate ids in a pool would make "used" tracking ambiguous; keep the first occurrence.
  const seen = new Set<Id>();
  const pool = input.pool
    .filter((q) => categoryKeys.has(q.category) && !seen.has(q.id) && seen.add(q.id))
    .sort(byId);
  const rng = createRng(`${input.seed}:questions:${input.kind}`);
  const rules = ROUND_RULES[input.kind];

  if (rules.hasBoard) return selectBoard(input, pool, excluded, rng);
  if (input.kind === 'TABLE_WARS') return selectTableWars(pool, excluded, rng, rules.questionCount);
  return selectFinalRush(pool, excluded, rng, rules.questionCount);
}

function selectBoard(
  input: SelectRoundQuestionsInput,
  pool: QuestionInput[],
  excluded: ReadonlySet<Id>,
  rng: () => number,
): QuestionInput[] {
  if (input.categories.length !== CATEGORY_COUNT || new Set(input.categories.map((c) => c.key)).size !== CATEGORY_COUNT) {
    throw new Error(`${input.kind} needs exactly ${CATEGORY_COUNT} distinct categories, got ${input.categories.length}`);
  }
  const order = shuffle(input.categories, rng);
  const chosen: QuestionInput[] = [];
  for (const cat of order) {
    const candidates = pool.filter((q) => q.category === cat.key);
    if (candidates.length === 0) throw new Error(`No questions available for category "${cat.key}"`);
    const fresh = candidates.filter((q) => !excluded.has(q.id));
    chosen.push(pick(fresh.length > 0 ? fresh : candidates, rng));
  }
  const used = new Set(chosen.map((q) => q.id));
  const unused = pool.filter((q) => !used.has(q.id));
  if (unused.length === 0) throw new Error('No question left for the wildcard');
  const eligible = unused.filter((q) => q.wildcardEligible === true);
  // Preference: fresh wildcard-eligible → any wildcard-eligible → fresh anything → anything unused.
  const tiers = [
    eligible.filter((q) => !excluded.has(q.id)),
    eligible,
    unused.filter((q) => !excluded.has(q.id)),
    unused,
  ];
  const tier = tiers.find((t) => t.length > 0) as QuestionInput[];
  chosen.push(pick(tier, rng));
  return chosen;
}

function selectTableWars(pool: QuestionInput[], excluded: ReadonlySet<Id>, rng: () => number, count: number): QuestionInput[] {
  const ordered = [...shuffle(pool.filter((q) => !excluded.has(q.id)), rng), ...shuffle(pool.filter((q) => excluded.has(q.id)), rng)];
  const perCategory = new Map<string, number>();
  const chosen: QuestionInput[] = [];
  for (const q of ordered) {
    if (chosen.length === count) break;
    const n = perCategory.get(q.category) ?? 0;
    if (n >= TABLE_WARS_MAX_PER_CATEGORY) continue;
    perCategory.set(q.category, n + 1);
    chosen.push(q);
  }
  if (chosen.length < count) {
    throw new Error(`TABLE_WARS needs ${count} questions with at most ${TABLE_WARS_MAX_PER_CATEGORY} per category; only ${chosen.length} possible`);
  }
  return chosen;
}

function selectFinalRush(pool: QuestionInput[], excluded: ReadonlySet<Id>, rng: () => number, count: number): QuestionInput[] {
  if (pool.length < count) throw new Error(`FINAL_RUSH needs ${count} questions; the pool has ${pool.length}`);
  const isFresh = (q: QuestionInput): boolean => !excluded.has(q.id);
  const isHard = (q: QuestionInput): boolean => q.difficulty === 'DIFFICULT';
  // Freshness first, then difficulty; within a tier prefer categories not used yet.
  const tiers = [
    pool.filter((q) => isFresh(q) && isHard(q)),
    pool.filter((q) => isFresh(q) && !isHard(q)),
    pool.filter((q) => !isFresh(q) && isHard(q)),
    pool.filter((q) => !isFresh(q) && !isHard(q)),
  ];
  const chosen: QuestionInput[] = [];
  const usedCategories = new Set<string>();
  for (const tier of tiers) {
    const shuffled = shuffle(tier, rng);
    const varied = shuffled.filter((q) => !usedCategories.has(q.category));
    const variedIds = new Set<Id>();
    for (const q of varied) {
      if (chosen.length === count) break;
      if (usedCategories.has(q.category)) continue;
      usedCategories.add(q.category);
      variedIds.add(q.id);
      chosen.push(q);
    }
    for (const q of shuffled) {
      if (chosen.length === count) break;
      if (variedIds.has(q.id)) continue;
      usedCategories.add(q.category);
      chosen.push(q);
    }
    if (chosen.length === count) break;
  }
  return chosen;
}
