/** Versioned, JSON-only local profile data for an authenticated Tera member. */

export const LOCAL_PROFILE_VERSION = 2 as const;

export const SKIN_TONES = ["espresso", "umber", "sienna", "sand", "peach"] as const;
export const OUTFITS = ["ink", "navy", "slate", "sage", "clay"] as const;
export const ACCENTS = ["aqua", "amber", "coral", "violet", "lime"] as const;
export const HAIR_COLORS = ["black", "espresso", "auburn", "silver", "platinum"] as const;
export const BODY_SHAPES = ["slim", "average", "broad"] as const;

export type SkinToneChoice = (typeof SKIN_TONES)[number];
export type OutfitChoice = (typeof OUTFITS)[number];
export type AccentChoice = (typeof ACCENTS)[number];
export type HairColorChoice = (typeof HAIR_COLORS)[number];
export type BodyShapeChoice = (typeof BODY_SHAPES)[number];

export interface HumanoidAppearanceChoices {
  skinTone: SkinToneChoice;
  outfit: OutfitChoice;
  accent: AccentChoice;
  hair: HairColorChoice;
  bodyShape: BodyShapeChoice;
}

/**
 * This intentionally contains no identity id, auth credential, webcam state,
 * face URL, image, device id, or arbitrary metadata bucket.
 */
export interface LocalProfileV2 {
  version: typeof LOCAL_PROFILE_VERSION;
  displayName: string;
  appearance: HumanoidAppearanceChoices;
}

/** The only supported legacy shape: V1 kept appearance choices flat. */
export interface LocalProfileV1 {
  version: 1;
  displayName: string;
  skinTone: SkinToneChoice;
  outfit: OutfitChoice;
  accent: AccentChoice;
  hair: HairColorChoice;
  bodyShape: BodyShapeChoice;
}

export type LocalProfile = LocalProfileV2;

export type ProfileDecodeResult =
  | { ok: true; profile: LocalProfile; migrated: false }
  | { ok: true; profile: LocalProfile; migrated: true; fromVersion: 1 }
  | { ok: false; error: string };

export interface ResolvedHumanoidAppearance {
  skinTone: string;
  outfitColor: string;
  accentColor: string;
  hairColor: string;
  bodyShape: BodyShapeChoice;
}

export type PresenceContext = "outdoors" | "office";
export type PresenceActorKind = "humanoid" | "dog" | "crow";

const SKIN_COLORS: Readonly<Record<SkinToneChoice, string>> = Object.freeze({
  espresso: "#4b2d25",
  umber: "#704534",
  sienna: "#9a6248",
  sand: "#c58d68",
  peach: "#e6b28e",
});

const OUTFIT_COLORS: Readonly<Record<OutfitChoice, string>> = Object.freeze({
  ink: "#18222d",
  navy: "#1d3b58",
  slate: "#53616d",
  sage: "#566c5d",
  clay: "#80594a",
});

const ACCENT_COLORS: Readonly<Record<AccentChoice, string>> = Object.freeze({
  aqua: "#55b8c8",
  amber: "#d99b39",
  coral: "#db6d62",
  violet: "#826faf",
  lime: "#86a94a",
});

const HAIR_VALUES: Readonly<Record<HairColorChoice, string>> = Object.freeze({
  black: "#171719",
  espresso: "#2d211d",
  auburn: "#65372b",
  silver: "#8b9298",
  platinum: "#d0c5ac",
});

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function exactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
  const actual = Object.keys(value).sort();
  const wanted = [...expected].sort();
  return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
}

function oneOf<T extends string>(value: unknown, choices: readonly T[]): value is T {
  return typeof value === "string" && (choices as readonly string[]).includes(value);
}

function validDisplayName(value: unknown): value is string {
  return (
    typeof value === "string" &&
    value === value.trim() &&
    value.length >= 1 &&
    value.length <= 64 &&
    !/[\u0000-\u001f\u007f]/u.test(value)
  );
}

export function isHumanoidAppearance(value: unknown): value is HumanoidAppearanceChoices {
  return (
    isRecord(value) &&
    exactKeys(value, ["skinTone", "outfit", "accent", "hair", "bodyShape"]) &&
    oneOf(value.skinTone, SKIN_TONES) &&
    oneOf(value.outfit, OUTFITS) &&
    oneOf(value.accent, ACCENTS) &&
    oneOf(value.hair, HAIR_COLORS) &&
    oneOf(value.bodyShape, BODY_SHAPES)
  );
}

export function isLocalProfile(value: unknown): value is LocalProfile {
  return (
    isRecord(value) &&
    exactKeys(value, ["version", "displayName", "appearance"]) &&
    value.version === LOCAL_PROFILE_VERSION &&
    validDisplayName(value.displayName) &&
    isHumanoidAppearance(value.appearance)
  );
}

function isLocalProfileV1(value: unknown): value is LocalProfileV1 {
  return (
    isRecord(value) &&
    exactKeys(value, ["version", "displayName", "skinTone", "outfit", "accent", "hair", "bodyShape"]) &&
    value.version === 1 &&
    validDisplayName(value.displayName) &&
    oneOf(value.skinTone, SKIN_TONES) &&
    oneOf(value.outfit, OUTFITS) &&
    oneOf(value.accent, ACCENTS) &&
    oneOf(value.hair, HAIR_COLORS) &&
    oneOf(value.bodyShape, BODY_SHAPES)
  );
}

function clone(profile: LocalProfile): LocalProfile {
  return { ...profile, appearance: { ...profile.appearance } };
}

export function migrateLocalProfileV1(profile: LocalProfileV1): LocalProfile {
  if (!isLocalProfileV1(profile)) throw new RangeError("profile: invalid V1 profile");
  return {
    version: LOCAL_PROFILE_VERSION,
    displayName: profile.displayName,
    appearance: {
      skinTone: profile.skinTone,
      outfit: profile.outfit,
      accent: profile.accent,
      hair: profile.hair,
      bodyShape: profile.bodyShape,
    },
  };
}

/** Strictly decode current data or the one documented legacy version. */
export function decodeLocalProfile(payload: string | unknown): ProfileDecodeResult {
  let value: unknown = payload;
  if (typeof payload === "string") {
    try {
      value = JSON.parse(payload) as unknown;
    } catch {
      return { ok: false, error: "profile: data is not valid JSON" };
    }
  }
  if (!isRecord(value) || !Number.isInteger(value.version)) {
    return { ok: false, error: "profile: missing version" };
  }
  if (value.version === LOCAL_PROFILE_VERSION) {
    if (!isLocalProfile(value)) return { ok: false, error: "profile: invalid V2 profile" };
    return { ok: true, profile: clone(value), migrated: false };
  }
  if (value.version === 1) {
    if (!isLocalProfileV1(value)) return { ok: false, error: "profile: invalid V1 profile" };
    return { ok: true, profile: migrateLocalProfileV1(value), migrated: true, fromVersion: 1 };
  }
  return { ok: false, error: "profile: unsupported version" };
}

export function encodeLocalProfile(profile: LocalProfile): string {
  if (!isLocalProfile(profile)) throw new RangeError("profile: cannot encode invalid profile");
  return JSON.stringify(profile);
}

/**
 * Stable per-id defaults without retaining the id or its hash. Salted hashes
 * keep each appearance axis independent and reveal nothing in the output that
 * was not already one of the public palette choices.
 */
export function createDefaultLocalProfile(identityId: string, displayName: string): LocalProfile {
  if (typeof identityId !== "string" || identityId.length === 0) {
    throw new RangeError("profile: identity id is required for deterministic defaults");
  }
  const name = displayName.trim();
  if (!validDisplayName(name)) throw new RangeError("profile: display name is invalid");
  function choose<T>(items: readonly T[], salt: string): T {
    const item = items[stableHash(`${salt}\u0000${identityId}`) % items.length];
    if (item === undefined) throw new Error("profile: empty choice catalogue");
    return item;
  }
  return {
    version: LOCAL_PROFILE_VERSION,
    displayName: name,
    appearance: {
      skinTone: choose(SKIN_TONES, "skin"),
      outfit: choose(OUTFITS, "outfit"),
      accent: choose(ACCENTS, "accent"),
      hair: choose(HAIR_COLORS, "hair"),
      bodyShape: choose(BODY_SHAPES, "body"),
    },
  };
}

function stableHash(value: string): number {
  let hash = 0x811c9dc5;
  for (let index = 0; index < value.length; index++) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 0x01000193);
  }
  return hash >>> 0;
}

/** Material-ready values for `buildHumanoid`, still containing no render objects. */
export function resolveHumanoidAppearance(
  appearance: HumanoidAppearanceChoices,
): ResolvedHumanoidAppearance {
  if (!isHumanoidAppearance(appearance)) throw new RangeError("profile: invalid humanoid appearance");
  return {
    skinTone: SKIN_COLORS[appearance.skinTone],
    outfitColor: OUTFIT_COLORS[appearance.outfit],
    accentColor: ACCENT_COLORS[appearance.accent],
    hairColor: HAIR_VALUES[appearance.hair],
    bodyShape: appearance.bodyShape,
  };
}

/** Signed-in members use their humanoid; anonymous presence follows environment role. */
export function actorKindForPresence(signedIn: boolean, context: PresenceContext): PresenceActorKind {
  if (signedIn) return "humanoid";
  return context === "office" ? "dog" : "crow";
}
