/**
 * Avatar library selection.
 *
 * Deterministically maps a character to a curated, pre-generated avatar (hosted
 * on GCS). Selection is pure + instant — no runtime image generation.
 *
 * Two layers, both keyed by a stable FNV-1a hash of the character id:
 *   - slot    (ethnicity x sex)         -> stable for life (who they are)
 *   - variant (0..N-1 within the slot)  -> stable for life (their distinct
 *                                          persona: hairstyle / facial hair /
 *                                          accessories)
 *   - ageStage (child/teen/adult/senior) -> follows their current age
 *
 * A given variant index shares the same look family across ages, so a character
 * keeps a consistent persona and simply ages through life stages. The pool of
 * variants per slot is what delivers within-type variety (many distinct-looking
 * people per ethnicity).
 *
 * Manifest entries may be either:
 *   - population pool: carry a 0-based `variant` (many per slot+age)
 *   - legacy baseline: no `variant` (one per slot+age) -> treated as variant 0
 *
 * Variant counts can be ragged (some slot+age stages have fewer variants than
 * others, e.g. credits ran out). Selection clamps the desired variant into the
 * available range PER age stage so a persona stays as stable as the data allows
 * and never resolves to a missing file.
 */
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';

interface AvatarEntry {
  file: string;
  slot: string;
  ethnicity: string;
  sex: 'M' | 'F';
  ageStage: 'child' | 'teen' | 'adult' | 'senior';
  variant?: number;
}
interface Manifest { avatars: AvatarEntry[]; }

const CDN = (process.env.AVATAR_CDN_URL ?? 'https://storage.googleapis.com/baolife-avatars').replace(/\/$/, '');

function loadManifest(): Manifest {
  try {
    const p = fileURLToPath(new URL('./avatarLibrary.manifest.json', import.meta.url));
    return JSON.parse(readFileSync(p, 'utf8')) as Manifest;
  } catch {
    return { avatars: [] };
  }
}

const manifest = loadManifest();

// slot list per sex (sorted for deterministic indexing)
const slotsBySex: Record<string, string[]> = { M: [], F: [] };
// slot -> ageStage -> variant index -> file
const bySlot: Record<string, Record<string, Record<number, string>>> = {};
// slot -> max variant count across all its age stages (persona modulus)
const slotVariantSpan: Record<string, number> = {};

for (const a of manifest.avatars) {
  (slotsBySex[a.sex] ??= []);
  if (!slotsBySex[a.sex].includes(a.slot)) slotsBySex[a.sex].push(a.slot);
  const v = a.variant ?? 0;
  ((bySlot[a.slot] ??= {})[a.ageStage] ??= {})[v] = a.file;
}
for (const s of Object.keys(slotsBySex)) slotsBySex[s].sort();
// Persona modulus = the widest variant set the slot offers at any age. Using one
// modulus for the whole slot (not per-age) keeps a character on the SAME variant
// number across ages whenever that number exists, so the persona stays stable;
// ragged stages only diverge for the few personas whose number is absent there.
for (const slot of Object.keys(bySlot)) {
  let span = 1;
  for (const st of Object.keys(bySlot[slot])) {
    span = Math.max(span, Object.keys(bySlot[slot][st]).length);
  }
  slotVariantSpan[slot] = span;
}

export function hasLibrary(): boolean {
  return manifest.avatars.length > 0;
}

// FNV-1a — stable across runs, unlike Math.random (the old avatar bug).
function hash(s: string): number {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
  return h >>> 0;
}
function ageStageOf(years: number): 'child' | 'teen' | 'adult' | 'senior' {
  if (years < 13) return 'child';
  if (years < 20) return 'teen';
  if (years >= 60) return 'senior';
  return 'adult';
}

// Graceful fallback chain if a slot is missing a given stage entirely.
const AGE_FALLBACK: Record<string, string[]> = {
  child: ['child', 'teen', 'adult', 'senior'],
  teen: ['teen', 'adult', 'child', 'senior'],
  adult: ['adult', 'teen', 'senior', 'child'],
  senior: ['senior', 'adult', 'teen', 'child'],
};

// Resolve a concrete variant index to a file for one age stage. Prefer the exact
// persona index; if this (ragged) stage lacks it, fall back deterministically to
// the nearest available index so the result is stable and never a miss.
function pickVariant(variants: Record<number, string>, want: number): string | null {
  if (variants[want] !== undefined) return variants[want];
  const keys = Object.keys(variants).map(Number).sort((a, b) => a - b);
  if (!keys.length) return null;
  return variants[keys[want % keys.length]];
}

export function selectAvatarFile(person: { id: string; sex: string; ageYears: number }): string | null {
  const sexKey = person.sex === 'Male' ? 'M' : 'F';
  const slots = slotsBySex[sexKey]?.length ? slotsBySex[sexKey] : Object.values(slotsBySex).flat();
  if (!slots.length) return null;

  const h = hash(person.id || 'x');
  const slot = slots[h % slots.length];
  const ages = bySlot[slot] ?? {};
  const stage = ageStageOf(person.ageYears ?? 0);

  // The character's persona: a stable variant index drawn from a DIFFERENT slice
  // of the hash than the slot (so slot and variant vary independently), reduced
  // by the slot's full variant span. Because the modulus is the same at every
  // age, the same persona maps to the same variant number across life stages
  // wherever that number exists — keeping identity stable as they age.
  const wantVariant = Math.floor(h / 7) % (slotVariantSpan[slot] ?? 1);

  for (const st of AGE_FALLBACK[stage] ?? ['adult']) {
    const variants = ages[st];
    if (!variants) continue;
    const file = pickVariant(variants, wantVariant);
    if (file) return file;
  }
  return null;
}

export function avatarUrlFor(person: { id: string; sex: string; ageYears: number }): string | null {
  const file = selectAvatarFile(person);
  return file ? `${CDN}/${file}` : null;
}
