import type { MemberId } from "./types";
import {
  CHARACTER_WEAPON_MODEL_IDS,
  heldItemForModel,
  resolveCharacterWeaponModelId,
  weaponDefinition,
  weaponSupportsSlot,
  type CharacterHeldItemVisual,
  type CharacterWeaponGrip,
  type CharacterWeaponModelId,
} from "./weaponCatalog";

export type {
  CharacterHeldItemVisual,
  CharacterWeaponGrip,
  CharacterWeaponModelId,
} from "./weaponCatalog";

/** Compatibility map for callers that still need direct model-to-grip lookup. */
export const CHARACTER_WEAPON_GRIP_BY_MODEL = Object.fromEntries(
  CHARACTER_WEAPON_MODEL_IDS.map((modelId) => [modelId, weaponDefinition(modelId).grip]),
) as Record<CharacterWeaponModelId, CharacterWeaponGrip>;

export type CharacterPartSlot = "head" | "upper-body" | "lower-body" | "headwear" | "back";
export type CharacterMaterialVariant = "priest-vestments";

export interface CharacterPartDefinition {
  slot: CharacterPartSlot;
  sourceMemberId: MemberId;
  nodeNames: readonly string[];
  materialVariant?: CharacterMaterialVariant;
}

export const CHARACTER_PART_CATALOG = {
  "priest-head": { slot: "head", sourceMemberId: "orin", nodeNames: ["Mage_Head"] },
  "druid-head": { slot: "head", sourceMemberId: "aelia", nodeNames: ["Druid_Head"] },
  "mage-head": { slot: "head", sourceMemberId: "orin", nodeNames: ["Mage_Head"] },
  "ranger-head": { slot: "head", sourceMemberId: "nia", nodeNames: ["Ranger_Head"] },
  "knight-head": { slot: "head", sourceMemberId: "brann", nodeNames: ["Knight_Head"] },
  "rogue-head": { slot: "head", sourceMemberId: "vale", nodeNames: ["Rogue_Head"] },

  "priest-upper": { slot: "upper-body", sourceMemberId: "orin", nodeNames: ["Mage_ArmLeft", "Mage_ArmRight", "Mage_Body"], materialVariant: "priest-vestments" },
  "druid-upper": { slot: "upper-body", sourceMemberId: "aelia", nodeNames: ["Druid_ArmLeft", "Druid_ArmRight", "Druid_Body"] },
  "mage-upper": { slot: "upper-body", sourceMemberId: "orin", nodeNames: ["Mage_ArmLeft", "Mage_ArmRight", "Mage_Body"] },
  "ranger-upper": { slot: "upper-body", sourceMemberId: "nia", nodeNames: ["Ranger_ArmLeft", "Ranger_ArmRight", "Ranger_Body"] },
  "knight-upper": { slot: "upper-body", sourceMemberId: "brann", nodeNames: ["Knight_ArmLeft", "Knight_ArmRight", "Knight_Body"] },
  "rogue-upper": { slot: "upper-body", sourceMemberId: "vale", nodeNames: ["Rogue_ArmLeft", "Rogue_ArmRight", "Rogue_Body"] },

  "priest-lower": { slot: "lower-body", sourceMemberId: "orin", nodeNames: ["Mage_LegLeft", "Mage_LegRight"], materialVariant: "priest-vestments" },
  "druid-lower": { slot: "lower-body", sourceMemberId: "aelia", nodeNames: ["Druid_LegLeft", "Druid_LegRight"] },
  "mage-lower": { slot: "lower-body", sourceMemberId: "orin", nodeNames: ["Mage_LegLeft", "Mage_LegRight"] },
  "ranger-lower": { slot: "lower-body", sourceMemberId: "nia", nodeNames: ["Ranger_LegLeft", "Ranger_LegRight"] },
  "knight-lower": { slot: "lower-body", sourceMemberId: "brann", nodeNames: ["Knight_LegLeft", "Knight_LegRight"] },
  "rogue-lower": { slot: "lower-body", sourceMemberId: "vale", nodeNames: ["Rogue_LegLeft", "Rogue_LegRight"] },

  "mage-hat": { slot: "headwear", sourceMemberId: "orin", nodeNames: ["Mage_Hat"] },
  "knight-helmet": { slot: "headwear", sourceMemberId: "brann", nodeNames: ["Knight_Helmet", "Knight_HelmetVisor"] },

  "priest-cape": { slot: "back", sourceMemberId: "orin", nodeNames: ["Mage_Cape"], materialVariant: "priest-vestments" },
  "druid-backpack": { slot: "back", sourceMemberId: "aelia", nodeNames: ["Druid_Backpack"] },
  "mage-cape": { slot: "back", sourceMemberId: "orin", nodeNames: ["Mage_Cape"] },
  "ranger-cape": { slot: "back", sourceMemberId: "nia", nodeNames: ["Ranger_Cape"] },
  "ranger-quiver": { slot: "back", sourceMemberId: "nia", nodeNames: ["Ranger_Quiver"] },
  "knight-cape": { slot: "back", sourceMemberId: "brann", nodeNames: ["Knight_Cape"] },
  "rogue-cape": { slot: "back", sourceMemberId: "vale", nodeNames: ["Rogue_Cape"] },
} as const satisfies Record<string, CharacterPartDefinition>;

export type CharacterPartId = keyof typeof CHARACTER_PART_CATALOG;
export type CharacterPartIdFor<Slot extends CharacterPartSlot> = {
  [PartId in CharacterPartId]: typeof CHARACTER_PART_CATALOG[PartId]["slot"] extends Slot ? PartId : never;
}[CharacterPartId];

export interface CharacterAppearanceV1 {
  version: 1;
  rigId: "medium";
  scaleSourceMemberId: MemberId;
  headPartId: CharacterPartIdFor<"head">;
  upperBodyPartId: CharacterPartIdFor<"upper-body">;
  lowerBodyPartId: CharacterPartIdFor<"lower-body">;
  headwearPartId: CharacterPartIdFor<"headwear"> | null;
  backPartId: CharacterPartIdFor<"back"> | null;
  mainHand: CharacterHeldItemVisual;
  offHand?: CharacterHeldItemVisual;
}

const MEMBER_IDS: readonly MemberId[] = ["aelia", "brann", "nia", "orin", "vale"];

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

function isMemberId(value: unknown): value is MemberId {
  return typeof value === "string" && MEMBER_IDS.includes(value as MemberId);
}

function canonicalHeldItem(
  value: unknown,
  fallback: CharacterHeldItemVisual,
  slot: "main" | "off",
): CharacterHeldItemVisual {
  const requestedModelId = isRecord(value)
    ? resolveCharacterWeaponModelId(value.modelId)
    : null;
  const modelId = requestedModelId && weaponSupportsSlot(requestedModelId, slot)
    ? requestedModelId
    : fallback.modelId;
  return heldItemForModel(modelId);
}

function canonicalOptionalOffHand(
  value: unknown,
  fallback?: CharacterHeldItemVisual,
): CharacterHeldItemVisual | undefined {
  if (value === undefined || value === null) return undefined;
  const requestedModelId = isRecord(value)
    ? resolveCharacterWeaponModelId(value.modelId)
    : null;
  if (requestedModelId && weaponSupportsSlot(requestedModelId, "off")) {
    return heldItemForModel(requestedModelId);
  }
  return fallback ? heldItemForModel(fallback.modelId) : undefined;
}

function partForSlot<Slot extends CharacterPartSlot>(
  value: unknown,
  slot: Slot,
): CharacterPartIdFor<Slot> | null {
  if (typeof value !== "string"
    || !Object.prototype.hasOwnProperty.call(CHARACTER_PART_CATALOG, value)) return null;
  const partId = value as CharacterPartId;
  return CHARACTER_PART_CATALOG[partId].slot === slot
    ? partId as CharacterPartIdFor<Slot>
    : null;
}

function optionalPartForSlot<Slot extends "headwear" | "back">(
  value: unknown,
  slot: Slot,
  fallback: CharacterPartIdFor<Slot> | null,
): CharacterPartIdFor<Slot> | null {
  if (value === null) return null;
  return partForSlot(value, slot) ?? fallback;
}

export function cloneCharacterAppearance(appearance: CharacterAppearanceV1): CharacterAppearanceV1 {
  return {
    ...appearance,
    mainHand: heldItemForModel(appearance.mainHand.modelId),
    ...(appearance.offHand
      ? { offHand: heldItemForModel(appearance.offHand.modelId) }
      : {}),
  };
}

/**
 * Repairs untrusted save/API data before it can select runtime model assets.
 * Appearance has its own version so this backward-compatible save field does not
 * require a hunter-save schema bump.
 */
export function normalizeCharacterAppearance(
  value: unknown,
  fallback: CharacterAppearanceV1,
): CharacterAppearanceV1 {
  if (!isRecord(value) || value.version !== 1 || value.rigId !== "medium") {
    return cloneCharacterAppearance(fallback);
  }

  const mainHand = canonicalHeldItem(value.mainHand, fallback.mainHand, "main");
  const offHand = canonicalOptionalOffHand(value.offHand, fallback.offHand);

  return {
    version: 1,
    rigId: "medium",
    scaleSourceMemberId: isMemberId(value.scaleSourceMemberId)
      ? value.scaleSourceMemberId
      : fallback.scaleSourceMemberId,
    headPartId: partForSlot(value.headPartId, "head") ?? fallback.headPartId,
    upperBodyPartId: partForSlot(value.upperBodyPartId, "upper-body") ?? fallback.upperBodyPartId,
    lowerBodyPartId: partForSlot(value.lowerBodyPartId, "lower-body") ?? fallback.lowerBodyPartId,
    headwearPartId: optionalPartForSlot(value.headwearPartId, "headwear", fallback.headwearPartId),
    backPartId: optionalPartForSlot(value.backPartId, "back", fallback.backPartId),
    mainHand,
    ...(offHand ? { offHand } : {}),
  };
}

export type CharacterModelMode = "modular" | "legacy";

export function resolveCharacterModelMode({
  envMode,
  search,
}: {
  envMode?: string;
  search?: string;
}): CharacterModelMode {
  if (envMode?.toLowerCase() === "legacy") return "legacy";
  const query = new URLSearchParams(search ?? "");
  return query.get("characterModels") === "legacy" || query.has("legacyCharacterModels")
    ? "legacy"
    : "modular";
}

export const CHARACTER_MODEL_MODE = resolveCharacterModelMode({
  envMode: import.meta.env.VITE_CHARACTER_MODEL_MODE,
  search: typeof window === "undefined" ? "" : window.location.search,
});

export function characterAppearancePartIds(appearance: CharacterAppearanceV1): CharacterPartId[] {
  return [
    appearance.headPartId,
    appearance.upperBodyPartId,
    appearance.lowerBodyPartId,
    ...(appearance.headwearPartId ? [appearance.headwearPartId] : []),
    ...(appearance.backPartId ? [appearance.backPartId] : []),
  ];
}
