
import { getSpriteRows } from "./graphics/rowEncoding";
import { isPlainObject, type JsonObject, type JsonValue } from "./contracts/json";

export type { JsonObject, JsonValue } from "./contracts/json";
export { isPlainObject } from "./contracts/json";
export { normalizeHexColor, normalizeStringList, parseCsv } from "./shared/normalization";
export { getSpriteRows } from "./graphics/rowEncoding";
export {
  buildSpritesPayloadFromImagesPayload,
  buildTilesPayloadFromImagesPayload,
  mergeImagesPayloadWithSpritesPayload,
  mergeImagesPayloadWithTilesPayload,
  normalizeImagePlayback,
  normalizeImageRecordForSave,
  normalizeImagesPayloadForSave,
  normalizeSpritePayloadForSave,
  normalizeTileRecordForSave,
  normalizeTilesPayloadForSave,
} from "./contentTransforms/graphicsPayload";

export type CatalogEntry = {
  entryId?: string;
  sourceKey?: string;
  key?: string;
  originalName?: string;
  description?: string;
  color?: string;
  sublistType?: string;
  displayKeys?: string[];
  passKeys?: string[];
};
export type CatalogMeta = {
  schemaVersion?: number;
  conditions?: CatalogEntry[];
  itemActions?: CatalogEntry[];
  systemActions?: CatalogEntry[];
  effects?: CatalogEntry[];
  colors?: CatalogEntry[];
};

export type ConfigTabLabel = "Conditions" | "Item Actions" | "System Actions" | "Effects" | "Colors";
export type ConfigCatalogKey = "conditions" | "itemActions" | "systemActions" | "effects" | "colors";

export const CONFIG_TAB_TO_KEY: Record<ConfigTabLabel, ConfigCatalogKey> = {
  Conditions: "conditions",
  "Item Actions": "itemActions",
  "System Actions": "systemActions",
  Effects: "effects",
  Colors: "colors",
};

const DEFAULT_COLOR_HEXES_ORDERED = [
  "#291814",
  "#111D35",
  "#422136",
  "#125359",
  "#742F29",
  "#49333B",
  "#A28879",
  "#F3EF7D",
  "#BE1250",
  "#FF6C24",
  "#A8E72E",
  "#00B543",
  "#065AB5",
  "#754665",
  "#FF6E59",
  "#FF9D81",
  "#000000",
  "#1D2B53",
  "#7E2553",
  "#008751",
  "#AB5236",
  "#5F574F",
  "#C2C3C7",
  "#FFF1E8",
  "#FF004D",
  "#FFA300",
  "#FFEC27",
  "#00E436",
  "#29ADFF",
  "#83769C",
  "#FF77A8",
  "#FFCCAA",
] as const;

const DEFAULT_COLOR_SYMBOLS_ORDERED = [
  "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
  "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
  "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
  "U", "V",
] as const;

const BASE_UNIFIED_COLOR_BY_SYMBOL: Record<string, string> = DEFAULT_COLOR_SYMBOLS_ORDERED.reduce((acc, symbol, index) => {
  acc[symbol] = String(DEFAULT_COLOR_HEXES_ORDERED[index] || "#000000").toUpperCase();
  return acc;
}, {} as Record<string, string>);

let unifiedColorBySymbol: Record<string, string> = { ...BASE_UNIFIED_COLOR_BY_SYMBOL };

export function setUnifiedColorEntries(entries: CatalogEntry[] | undefined): void {
  const next: Record<string, string> = { ...BASE_UNIFIED_COLOR_BY_SYMBOL };
  if (Array.isArray(entries)) {
    entries.forEach((entry) => {
      const symbol = String(entry.key || entry.sourceKey || entry.originalName || "").trim().charAt(0).toUpperCase();
      const color = String(entry.color || "").trim().toUpperCase();
      if (!symbol || !/^[0-9A-V]$/.test(symbol) || !/^#[0-9A-F]{6}$/.test(color)) {
        return;
      }
      next[symbol] = color;
    });
  }
  unifiedColorBySymbol = next;
}

export function resolveUnifiedColorSymbol(symbol: string, fallback = "#00000000"): string {
  const normalized = String(symbol || "").trim().charAt(0).toUpperCase();
  if (!normalized) {
    return fallback;
  }
  if (normalized === ".") {
    return "#00000000";
  }
  return unifiedColorBySymbol[normalized] || fallback;
}

export function createDefaultColorEntries(seed: number): CatalogEntry[] {
  return DEFAULT_COLOR_HEXES_ORDERED.map((hex, index) => {
    const symbol = DEFAULT_COLOR_SYMBOLS_ORDERED[index] || `X${index}`;
    return {
      entryId: `colors-${seed}-${index}`,
      sourceKey: symbol,
      key: symbol,
      originalName: symbol,
      description: `Palette color ${index + 1}`,
      color: String(hex).toUpperCase(),
      sublistType: "",
      displayKeys: [],
      passKeys: [],
    };
  });
}
export type DialogueChoice = {
  text: string;
  nextId: string;
  reactionType: string;
  reactionValue: string;
};
export type DialogueSandbox = {
  questStarted: number[];
  questCompleted: number[];
  inventory: Record<string, number>;
};
export type DialogueFlowKind = "condition" | "action" | "jump" | "choice" | "text" | "end";
export type DialogueFlowStep = {
  id: string;
  kind: DialogueFlowKind;
  text?: string;
  conditionType?: string;
  conditionValue?: string;
  conditionStepId?: string;
  conditionNot?: boolean;
  nextId?: string;
  reactionType?: string;
  reactionValue?: string;
};

// Generates a collision-resistant instance id: inst_<10 hex chars>
function genInstanceId(): string {
  try {
    const arr = new Uint8Array(5);
    crypto.getRandomValues(arr);
    return 'inst_' + Array.from(arr).map((b) => b.toString(16).padStart(2, '0')).join('');
  } catch {
    return `inst_${Math.random().toString(16).slice(2, 12)}`;
  }
}

function genRandomId(): string {
  try {
    const arr = new Uint8Array(5);
    crypto.getRandomValues(arr);
    return Array.from(arr).map((b) => b.toString(16).padStart(2, '0')).join('');
  } catch {
    return Math.random().toString(16).slice(2, 12);
  }
}

export const ROOT_KEY_BY_TYPE: Record<string, string> = {
  npcs: "npcs",
  npc_templates: "npcTemplates",
  dialogues: "dialogues",
  monsters: "monsters",
  items: "items",
  abilities: "abilities",
  loot_tables: "lootTables",
  quests: "quests",
  images: "images",
  sprites: "sprites",
  tiles: "tiles",
  factions: "factions",
};

export const TYPE_LABELS: Record<string, string> = {
  npcs: "NPCs",
  npc_templates: "NPCs",
  dialogues: "Dialogues",
  monsters: "Monsters",
  items: "Items",
  abilities: "Abilities",
  loot_tables: "Loot Tables",
  quests: "Quests",
  images: "Graphics",
  sprites: "Sprites",
  tiles: "Tiles",
  factions: "Factions",
};

export const FIELD_ORDER_BY_TYPE: Record<string, string[]> = {
  npcs: ["id", "position", "name", "faction", "spriteId", "dialogueId", "description", "enabled", "flags"],
  npc_templates: ["id", "name", "description", "title", "faction", "spriteId", "defaultDialogueId", "shopInventoryId", "flags"],
  dialogues: ["id", "name", "description", "dialogueNodes"],
  monsters: ["id", "name", "description", "level", "hp", "attack", "defense"],
  items: ["id", "name", "description", "itemType", "rarity", "stackable", "maxStack"],
  abilities: ["id", "name", "description", "cost", "cooldown"],
  loot_tables: ["id", "name", "description"],
  quests: ["questId", "name", "description", "requirements", "steps"],
  images: ["id", "name", "description", "roles", "tileSymbol", "width", "height", "pixelScale", "opacity", "defaultFrame", "speed", "playback", "tags", "frames"],
  sprites: ["id", "name", "width", "height", "pixelScale", "opacity", "rows"],
  tiles: ["id", "symbol", "name", "description", "width", "height", "pixelScale", "opacity", "rows"],
  factions: ["id", "name", "description", "color"],
};

export const EDIT_TABS_BY_TYPE: Record<string, string[]> = {
  npcs: ["General", "Overrides"],
  npc_templates: ["General", "Loot", "Abilities"],
  dialogues: ["General", "Dialogue"],
  monsters: ["General", "Combat", "Drops"],
  items: ["General", "Actions"],
  factions: ["General", "Actions"],
  abilities: ["General", "Targeting", "Cost", "Power"],
  loot_tables: ["General", "Entries"],
  quests: ["Information", "Conditions", "Steps"],
  images: ["Information", "Data"],
  sprites: ["Information", "Data"],
  tiles: ["Information", "Data"],
};

export const EDIT_TAB_FIELDS_BY_TYPE: Record<string, Record<string, string[]>> = {
  npcs: {
    General: ["id", "position", "enabled", "name", "faction"],
    Overrides: ["id", "spriteId", "dialogueId", "description", "flags"],
  },
  npc_templates: {
    General: ["id", "name", "description", "title", "faction", "spriteId", "defaultDialogueId"],
    Loot: ["id", "shopInventoryId", "lootTableId"],
    Abilities: ["id", "abilities", "passiveAbilities"],
  },
  dialogues: {
    General: ["id", "name", "description"],
    Dialogue: ["id", "dialogueNodes"],
  },
  monsters: {
    General: ["id", "name", "description", "level", "faction", "sprite", "aiProfileId"],
    Combat: ["id", "abilities", "stats", "hp", "attack", "defense", "speed", "mp"],
    Drops: ["id", "lootTableId", "tags"],
  },
  items: {
    General: ["id", "name", "description", "itemType", "rarity", "stackable", "maxStack", "value", "icon", "questItem", "destroyable", "sellable", "unique"],
    Actions: ["id", "actions", "actionsList", "effects", "triggers"],
  },
  factions: {
    General: ["id", "name", "description", "color"],
    Actions: ["id", "description"],
  },
  abilities: {
    General: ["id", "name", "description", "cooldown", "icon"],
    Targeting: ["id", "targeting"],
    Cost: ["id", "cost"],
    Power: ["id", "power", "statusEffectsOnHit"],
  },
  loot_tables: {
    General: ["id", "name", "description", "rollsMin", "rollsMax"],
    Entries: ["id", "entries"],
  },
  quests: {
    Information: ["questId", "name", "description", "reward"],
    Conditions: ["questId", "requirements", "reward"],
    Steps: ["questId", "steps"],
  },
  images: {
    Information: ["id", "name", "description", "roles", "tileSymbol", "height", "width", "pixelScale", "opacity", "defaultFrame", "speed", "playback", "tags"],
    Data: ["frames"],
  },
  sprites: {
    Information: ["id", "name", "height", "width", "pixelScale", "opacity"],
    Data: ["rows"],
  },
  tiles: {
    Information: ["id", "symbol", "name", "description", "height", "width", "pixelScale", "opacity"],
    Data: ["rows"],
  },
};

export const DIALOGUE_NODE_FIELD_ORDER = ["id", "description", "text", "nextNodeId", "order"];
export const FALLBACK_CONDITION_TYPES = ["always", "item", "flag", "quest_started", "quest_completed", "quest_step_completed"];
export const DIALOGUE_REACTION_TYPES = ["none", "grant_item", "grant_flag", "start_quest", "complete_quest", "remove_item"];
export const FLOW_KIND_LABELS: Record<DialogueFlowKind, string> = {
  condition: "When",
  action: "Do",
  jump: "Jump",
  choice: "Choice",
  text: "Text Screen",
  end: "End",
};

export async function fetchJsonOrThrow<T>(url: string): Promise<T> {
  const res = await fetch(url);
  const raw = await res.text();
  if (!res.ok) {
    throw new Error(`${url} failed (${res.status}): ${raw.slice(0, 180)}`);
  }
  try {
    return JSON.parse(raw) as T;
  } catch {
    throw new Error(`${url} returned invalid JSON`);
  }
}

export function formatTypeLabel(type: string): string {
  return TYPE_LABELS[type] || type.replaceAll("_", " ");
}

export function buildDefaultRecord(activeType: string, records: JsonObject[]): JsonObject {
  if (activeType === "quests") {
    const maxQuestId = records.reduce((acc, entry) => {
      const next = Number(entry.questId);
      if (Number.isInteger(next) && next > acc) {
        return next;
      }
      return acc;
    }, 0);
    return {
      questId: maxQuestId + 1,
      name: "New Quest",
      description: "",
      steps: [],
      requirements: [],
    };
  }

  if (activeType === "npcs") {
    return {
      id: genInstanceId(),
      position: { x: 10, y: 10 },
      name: "",
      faction: "",
      spriteId: "",
      dialogueId: "",
      description: "",
      enabled: true,
      flags: [],
    };
  }

  if (activeType === "npc_templates") {
      return {
        id: `npc_template_${genRandomId()}`,
      name: "New NPC Template",
      description: "",
      title: "",
      faction: "",
      spriteId: "npc_default",
      defaultDialogueId: "",
      shopInventoryId: "",
      flags: [],
    };
  }

  if (activeType === "dialogues") {
      return {
        id: `dlg_${genRandomId()}`,
      name: "New Dialogue",
      description: "",
      dialogueNodes: [],
    };
  }

  if (activeType === "factions") {
      return {
        id: `faction_${genRandomId()}`,
      name: "New Faction",
      description: "",
      color: "#7aa2ff",
    };
  }

  if (activeType === "tiles") {
    return {
      id: `tile_${genRandomId()}`,
      symbol: "T",
      name: "New Tile",
      description: "",
      width: 16,
      height: 16,
      pixelScale: 2,
      rows: Array.from({ length: 16 }, () => ".".repeat(16)),
    };
  }

  if (activeType === "images") {
    return {
      id: `image_${genRandomId()}`,
      name: "New Graphic",
      description: "",
      roles: ["sprite"],
      tileSymbol: "",
      width: 16,
      height: 16,
      pixelScale: 2,
      defaultFrame: "frame_0",
      speed: 0,
      playback: "normal",
      tags: [],
      rows: Array.from({ length: 16 }, () => ".".repeat(16)),
      frames: [{
        id: "frame_0",
        enabled: true,
        index: 0,
        rows: Array.from({ length: 16 }, () => ".".repeat(16)),
      }],
    };
  }

  return {
      id: `new_${genRandomId()}`,
    name: "New Record",
  };
}

export function getRecordLabel(record: JsonObject, index: number): string {
  const id = String(record.id || record.questId || "").trim();
  const name = String(record.name || "").trim();
  if (id && name) {
    return `${id} - ${name}`;
  }
  if (id) {
    return id;
  }
  if (name) {
    return name;
  }
  return `Record ${index + 1}`;
}

export function getSpritePalette(record?: JsonObject): Record<string, string> {
  void record;
  const palette: Record<string, string> = {
    ".": "#00000000",
  };
  Object.keys(unifiedColorBySymbol).forEach((symbol) => {
    palette[symbol] = unifiedColorBySymbol[symbol];
  });
  return palette;
}

type SpritePreviewCacheEntry = {
  signature: string;
  dataUrl: string | null;
};

const SPRITE_PREVIEW_CACHE_LIMIT = 800;
const spritePreviewCache = new Map<string, SpritePreviewCacheEntry>();

function hashText(value: string): string {
  let hash = 2166136261;
  for (let index = 0; index < value.length; index += 1) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return (hash >>> 0).toString(16);
}

function setSpritePreviewCache(hashKey: string, entry: SpritePreviewCacheEntry): void {
  if (spritePreviewCache.size >= SPRITE_PREVIEW_CACHE_LIMIT) {
    const firstKey = spritePreviewCache.keys().next().value;
    if (firstKey) {
      spritePreviewCache.delete(firstKey);
    }
  }
  spritePreviewCache.set(hashKey, entry);
}

export function buildSpritePreviewDataUrl(record: JsonObject, pixelSize: number): string | null {
  const rows = getSpriteRows(record);
  if (rows.length === 0) {
    return null;
  }
  const parsedWidth = Number(record.width);
  const parsedHeight = Number(record.height);
  const rowWidth = rows.reduce((acc, row) => Math.max(acc, row.length), 0);
  const width = Number.isFinite(parsedWidth) && parsedWidth > 0 ? Math.floor(parsedWidth) : rowWidth;
  const height = Number.isFinite(parsedHeight) && parsedHeight > 0 ? Math.floor(parsedHeight) : rows.length;
  if (width <= 0 || height <= 0) {
    return null;
  }

  const safePixelSize = Math.max(1, Math.floor(pixelSize));

  const palette = getSpritePalette(record);
  const paletteToken = Object.keys(palette)
    .sort((left, right) => left.localeCompare(right))
    .map((symbol) => `${symbol}:${palette[symbol]}`)
    .join("|");
  const signature = `${safePixelSize}|${width}|${height}|${rows.join("\n")}|${paletteToken}`;
  const cacheHash = hashText(signature);
  const cached = spritePreviewCache.get(cacheHash);
  if (cached && cached.signature === signature) {
    return cached.dataUrl;
  }

  const fallbackColor = palette["."] || "#00000000";
  const rects: string[] = [];

  for (let y = 0; y < height; y += 1) {
    const row = rows[y] || "";
    for (let x = 0; x < width; x += 1) {
      const symbol = row.charAt(x) || ".";
      const color = palette[symbol] || fallbackColor;
      rects.push(`<rect x="${x}" y="${y}" width="1" height="1" fill="${color}" />`);
    }
  }

  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width * safePixelSize}" height="${height * safePixelSize}" shape-rendering="crispEdges">${rects.join("")}</svg>`;
  const dataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
  setSpritePreviewCache(cacheHash, { signature, dataUrl });
  return dataUrl;
}

export function getSpriteEditorSize(record: JsonObject): { width: number; height: number } {
  const rows = getSpriteRows(record);
  const parsedWidth = Number(record.width);
  const parsedHeight = Number(record.height);
  const rowWidth = rows.reduce((acc, row) => Math.max(acc, row.length), 0);
  const width = Number.isFinite(parsedWidth) && parsedWidth > 0 ? Math.floor(parsedWidth) : rowWidth;
  const height = Number.isFinite(parsedHeight) && parsedHeight > 0 ? Math.floor(parsedHeight) : rows.length;
  return {
    width: Math.max(1, width),
    height: Math.max(1, height),
  };
}

export function getSpriteCellSymbol(record: JsonObject, x: number, y: number): string {
  const rows = getSpriteRows(record);
  const row = rows[y] || "";
  const symbol = row.charAt(x) || ".";
  return symbol || ".";
}

export function paintSpriteCell(record: JsonObject, x: number, y: number, symbol: string): JsonObject {
  const nextSymbol = String(symbol || ".").charAt(0) || ".";
  const size = getSpriteEditorSize(record);
  const rows = getSpriteRows(record);
  const nextRows = Array.from({ length: size.height }, (_, rowIndex) => {
    const base = rows[rowIndex] || "";
    const padded = base.padEnd(size.width, ".").slice(0, size.width);
    return padded;
  });
  const targetRow = nextRows[y] || "".padEnd(size.width, ".");
  nextRows[y] = `${targetRow.slice(0, x)}${nextSymbol}${targetRow.slice(x + 1)}`;
  return {
    ...record,
    rows: nextRows,
  };
}

export function toFieldLabel(rawKey: string): string {
  const withSpaces = rawKey
    .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
    .replaceAll("_", " ")
    .trim();
  if (!withSpaces) {
    return rawKey;
  }
  return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
}

export function getCatalogEntryIdValue(entry: CatalogEntry | null | undefined, fallback = ""): string {
  return String(entry?.key || entry?.sourceKey || entry?.originalName || fallback).trim();
}

export function normalizeCatalogEntryIdentity(entry: CatalogEntry, fallbackId = ""): CatalogEntry {
  const id = getCatalogEntryIdValue(entry, fallbackId);
  return {
    ...entry,
    sourceKey: id,
    key: id,
    originalName: id,
  };
}

export function normalizeCatalogMetaIdentity(meta: CatalogMeta): CatalogMeta {
  const normalizeList = (entries: CatalogEntry[] | undefined, prefix: string) => {
    if (!Array.isArray(entries)) {
      return [] as CatalogEntry[];
    }
    return entries.map((entry, index) => normalizeCatalogEntryIdentity(entry || {}, `${prefix}_${index + 1}`));
  };
  return {
    ...meta,
    conditions: normalizeList(meta.conditions, "condition"),
    itemActions: normalizeList(meta.itemActions, "item_action"),
    systemActions: normalizeList(meta.systemActions, "system_action"),
    effects: normalizeList(meta.effects, "effect"),
    colors: normalizeList(meta.colors, "color"),
  };
}

export function createDefaultSystemActionEntries(seed: number): CatalogEntry[] {
  const defaults = [
    { id: "grant_item", description: "Give itemId[:quantity] to player", sublistType: "items", displayKeys: ["id", "name"], passKeys: ["id"] },
    { id: "grant_money", description: "Give money amount to player", sublistType: "", displayKeys: [], passKeys: [] },
    { id: "remove_item", description: "Remove itemId[:quantity] from player", sublistType: "items", displayKeys: ["id", "name"], passKeys: ["id"] },
    { id: "start_quest", description: "Mark quest as started", sublistType: "quests", displayKeys: ["questId", "name"], passKeys: ["questId"] },
    { id: "complete_quest", description: "Mark quest as completed", sublistType: "quests", displayKeys: ["questId", "name"], passKeys: ["questId"] },
  ];
  return defaults.map((entry, index) => ({
    entryId: `systemActions-${seed}-${index}`,
    sourceKey: entry.id,
    key: entry.id,
    originalName: entry.id,
    description: entry.description,
    sublistType: entry.sublistType,
    displayKeys: entry.displayKeys,
    passKeys: entry.passKeys,
  }));
}

export function getConditionBaseType(type: string): string {
  const value = String(type || "").trim();
  return value || "always";
}

export function getDialogueNodes(record: JsonObject | null): JsonObject[] {
  if (!record) {
    return [];
  }
  const raw = record.dialogueNodes;
  if (!Array.isArray(raw)) {
    return [];
  }
  return raw.filter((entry) => isPlainObject(entry));
}

export function normalizeDialogueNodesForSave(nodes: JsonObject[]): JsonObject[] {
  return nodes.map((node, index) => {
    const next: JsonObject = { ...node };
    next.order = index + 1;
    return next;
  });
}

export function normalizeNpcRecordForLoad(record: JsonObject): JsonObject {
  const nextRecord: JsonObject = { ...record };
  Object.entries(record).forEach(([rawKey, rawValue]) => {
    const key = String(rawKey || "").toLowerCase();
    if (key === "position" && isPlainObject(rawValue as JsonValue)) {
      nextRecord.position = rawValue as JsonObject;
    } else if (key === "mapid") {
      nextRecord.mapId = String(rawValue ?? "");
    } else if (key === "nameoverride") {
      nextRecord.name = String(rawValue ?? "");
    } else if (key === "factionoverride") {
      nextRecord.faction = String(rawValue ?? "");
    } else if (key === "spriteidoverride") {
      nextRecord.spriteId = String(rawValue ?? "");
    } else if (key === "dialogueidoverride") {
      nextRecord.dialogueId = String(rawValue ?? "");
    }
  });

  Object.keys(nextRecord).forEach((rawKey) => {
    const lower = String(rawKey || "").toLowerCase();
    if (lower === "position" && rawKey !== "position") {
      delete nextRecord[rawKey];
    }
    if (lower === "role" || lower === "nameoverride" || lower === "titleoverride" || lower === "factionoverride" || lower === "spriteidoverride" || lower === "dialogueidoverride") {
      delete nextRecord[rawKey];
    }
  });

  const pos = isPlainObject(nextRecord.position) ? (nextRecord.position as JsonObject) : {};
  const parsedX = Number(nextRecord.x ?? pos.x ?? 10);
  const parsedY = Number(nextRecord.y ?? pos.y ?? 10);
  nextRecord.position = {
    x: Number.isFinite(parsedX) ? parsedX : 10,
    y: Number.isFinite(parsedY) ? parsedY : 10,
  };
  delete nextRecord.x;
  delete nextRecord.y;
  if (typeof nextRecord.id !== "string") {
    nextRecord.id = genInstanceId();
  }
  if (typeof nextRecord.name !== "string") nextRecord.name = "";
  if (typeof nextRecord.faction !== "string") nextRecord.faction = "";
  if (typeof nextRecord.spriteId !== "string") nextRecord.spriteId = "";
  if (typeof nextRecord.dialogueId !== "string") nextRecord.dialogueId = "";
  if (typeof nextRecord.description !== "string") nextRecord.description = "";
  if (typeof nextRecord.enabled !== "boolean") {
    nextRecord.enabled = true;
  }
  if (!Array.isArray(nextRecord.flags)) {
    nextRecord.flags = [];
  }
  delete nextRecord.role;
  delete nextRecord.templateId;
  delete nextRecord.nameOverride;
  delete nextRecord.title;
  delete nextRecord.titleOverride;
  delete nextRecord.factionOverride;
  delete nextRecord.spriteIdOverride;
  delete nextRecord.dialogueIdOverride;
  return nextRecord;
}

export function normalizeNpcPayloadForLoad(payload: JsonValue): JsonValue {
  if (!isPlainObject(payload)) {
    return payload;
  }
  const root = ROOT_KEY_BY_TYPE.npcs;
  const records = payload[root];
  if (!Array.isArray(records)) {
    return payload;
  }
  return {
    ...payload,
    [root]: records.map((entry) => (isPlainObject(entry) ? normalizeNpcRecordForLoad(entry) : entry)),
  };
}

export function normalizeNpcRecordForSave(record: JsonObject): JsonObject {
  const nextRecord: JsonObject = { ...record };
  const pos = isPlainObject(nextRecord.position) ? (nextRecord.position as JsonObject) : {};
  const parsedX = Number(pos.x ?? 10);
  const parsedY = Number(pos.y ?? 10);
  nextRecord.position = {
    x: Number.isFinite(parsedX) ? parsedX : 10,
    y: Number.isFinite(parsedY) ? parsedY : 10,
  };
  nextRecord.id = typeof nextRecord.id === "string" ? nextRecord.id : genInstanceId();
  nextRecord.name = typeof nextRecord.name === "string" ? nextRecord.name : "";
  nextRecord.faction = typeof nextRecord.faction === "string" ? nextRecord.faction : "";
  nextRecord.spriteId = typeof nextRecord.spriteId === "string" ? nextRecord.spriteId : "";
  nextRecord.dialogueId = typeof nextRecord.dialogueId === "string" ? nextRecord.dialogueId : "";
  nextRecord.description = typeof nextRecord.description === "string" ? nextRecord.description : "";
  nextRecord.enabled = typeof nextRecord.enabled === "boolean" ? nextRecord.enabled : true;
  nextRecord.flags = Array.isArray(nextRecord.flags) ? nextRecord.flags : [];
  delete nextRecord.x;
  delete nextRecord.y;
  delete nextRecord.role;
  delete nextRecord.dialogueNodes;
  delete nextRecord.defaultDialogueNodeId;
  delete nextRecord.templateId;
  delete nextRecord.nameOverride;
  delete nextRecord.title;
  delete nextRecord.titleOverride;
  delete nextRecord.factionOverride;
  delete nextRecord.spriteIdOverride;
  delete nextRecord.dialogueIdOverride;
  return nextRecord;
}

export function normalizeNpcPayloadForSave(payload: JsonValue): JsonValue {
  if (!isPlainObject(payload)) {
    return payload;
  }
  const root = ROOT_KEY_BY_TYPE.npcs;
  const records = payload[root];
  if (!Array.isArray(records)) {
    return payload;
  }
  return {
    ...payload,
    [root]: records.map((entry) => (isPlainObject(entry) ? normalizeNpcRecordForSave(entry) : entry)),
  };
}

export function createFlowStep(kind: DialogueFlowKind, namespace = "flow"): DialogueFlowStep {
  const base: DialogueFlowStep = {
    id: `${namespace}_${kind}_${Date.now()}_${Math.floor(Math.random() * 10000)}`,
    kind,
  };

  if (kind === "condition") {
    return {
      ...base,
      conditionType: "always",
      conditionValue: "",
      conditionStepId: "",
      conditionNot: false,
      text: "",
      nextId: "",
    };
  }
  if (kind === "choice") {
    return {
      ...base,
      text: "",
      nextId: "",
      conditionType: "always",
      conditionValue: "",
      conditionStepId: "",
      conditionNot: false,
      reactionType: "none",
      reactionValue: "",
    };
  }
  if (kind === "action") {
    return {
      ...base,
      reactionType: "none",
      reactionValue: "",
    };
  }
  if (kind === "jump") {
    return {
      ...base,
      nextId: "",
    };
  }
  if (kind === "text") {
    return {
      ...base,
      text: "",
    };
  }
  return base;
}

export function getPlainObjectArray(value: JsonValue | undefined): JsonObject[] {
  return Array.isArray(value) ? value.filter((entry) => isPlainObject(entry)) : [];
}

export function normalizePlainObjectArray(entries: JsonObject[]): JsonObject[] {
  return entries.map((entry) => ({ ...entry }));
}

export function nodeToFlowSteps(node: JsonObject): DialogueFlowStep[] {
  const existing = node.flowSteps;
  if (Array.isArray(existing)) {
    const normalized = existing
      .filter((entry) => isPlainObject(entry))
      .map((entry) => {
        const kind = String(entry.kind || "").trim() as DialogueFlowKind;
        if (!kind) {
          return null;
        }
        return {
          id: String(entry.id || `${kind}_${Date.now()}`),
          kind,
          text: String(entry.text || ""),
          conditionType: String(entry.conditionType || ""),
          conditionValue: String(entry.conditionValue || ""),
          conditionStepId: String(entry.conditionStepId || ""),
          conditionNot: Boolean(entry.conditionNot),
          nextId: String(entry.nextId || ""),
          reactionType: String(entry.reactionType || ""),
          reactionValue: String(entry.reactionValue || ""),
        };
      })
      .filter(Boolean) as DialogueFlowStep[];
    if (normalized.length > 0) {
      return normalized;
    }
  }

  const steps: DialogueFlowStep[] = [];
  const conditions = Array.isArray(node.conditions) ? node.conditions.filter((entry) => isPlainObject(entry)) : [];
  conditions.forEach((entry) => {
    steps.push({
      id: `condition_${Date.now()}_${steps.length}`,
      kind: "condition",
      text: String(entry.text || ""),
      conditionType: String(entry.conditionType || "always"),
      conditionValue: String(entry.conditionValue || ""),
      conditionStepId: String(entry.conditionStepId || ""),
      conditionNot: Boolean(entry.conditionNot),
      nextId: String(entry.nextId || ""),
    });
  });

  const reactions = Array.isArray(node.reactions) ? node.reactions.filter((entry) => isPlainObject(entry)) : [];
  reactions.forEach((entry) => {
    steps.push({
      id: `action_${Date.now()}_${steps.length}`,
      kind: "action",
      reactionType: String(entry.reactionType || "none"),
      reactionValue: String(entry.reactionValue || ""),
    });
  });

  if (String(node.text || "").trim()) {
    steps.push({
      id: `text_${Date.now()}_${steps.length}`,
      kind: "text",
      text: String(node.text || ""),
    });
  }

  if (String(node.nextNodeId || "").trim()) {
    steps.push({
      id: `jump_${Date.now()}_${steps.length}`,
      kind: "jump",
      nextId: String(node.nextNodeId || ""),
    });
  }

  const choices = Array.isArray(node.choices) ? node.choices.filter((entry) => isPlainObject(entry)) : [];
  choices.forEach((entry) => {
    steps.push({
      id: `choice_${Date.now()}_${steps.length}`,
      kind: "choice",
      text: String(entry.text || ""),
      nextId: String(entry.nextId || ""),
      conditionType: String(entry.conditionType || "always"),
      conditionValue: String(entry.conditionValue || ""),
      conditionStepId: String(entry.conditionStepId || ""),
      conditionNot: Boolean(entry.conditionNot),
      reactionType: String(entry.reactionType || "none"),
      reactionValue: String(entry.reactionValue || ""),
    });
  });

  return steps;
}

export function flowStepsToNode(node: JsonObject, steps: DialogueFlowStep[]): JsonObject {
  const conditions = steps
    .filter((step) => step.kind === "condition")
    .map((step) => ({
      text: String(step.text || ""),
      conditionType: String(step.conditionType || "always"),
      conditionValue: String(step.conditionValue || ""),
      conditionStepId: String(step.conditionStepId || ""),
      conditionNot: Boolean(step.conditionNot),
      nextId: String(step.nextId || ""),
    }));

  const reactions = steps
    .filter((step) => step.kind === "action")
    .map((step) => ({
      reactionType: String(step.reactionType || "none"),
      reactionValue: String(step.reactionValue || ""),
    }));

  const choices = steps
    .filter((step) => step.kind === "choice")
    .map((step) => ({
      text: String(step.text || ""),
      nextId: String(step.nextId || ""),
      conditionType: String(step.conditionType || "always"),
      conditionValue: String(step.conditionValue || ""),
      conditionStepId: String(step.conditionStepId || ""),
      conditionNot: Boolean(step.conditionNot),
      reactionType: String(step.reactionType || "none"),
      reactionValue: String(step.reactionValue || ""),
    }));

  const firstText = steps.find((step) => step.kind === "text");
  const firstJump = steps.find((step) => step.kind === "jump");
  const hasEnd = steps.some((step) => step.kind === "end");

  return {
    ...node,
    flowSteps: steps,
    text: String(firstText?.text || node.text || ""),
    nextNodeId: hasEnd ? "" : String(firstJump?.nextId || node.nextNodeId || ""),
    conditions,
    reactions,
    choices,
  };
}

export function cloneSandbox(sandbox: DialogueSandbox): DialogueSandbox {
  return {
    questStarted: [...sandbox.questStarted],
    questCompleted: [...sandbox.questCompleted],
    inventory: { ...sandbox.inventory },
  };
}

export function parseItemValue(raw: string): { itemId: string; quantity: number } {
  const text = String(raw || "").trim();
  if (!text) {
    return { itemId: "", quantity: 1 };
  }
  const [itemIdRaw, qtyRaw] = text.split(":");
  const itemId = String(itemIdRaw || "").trim();
  const quantity = Math.max(1, Number(qtyRaw) || 1);
  return { itemId, quantity };
}

export function checkCondition(type: string, value: string, notFlag: boolean, sandbox: DialogueSandbox): boolean {
  const normalizedType = String(type || "always").trim() || "always";
  let pass;
  if (normalizedType === "always") {
    pass = true;
  } else if (normalizedType === "quest_started") {
    const questId = Number(value);
    pass = Number.isInteger(questId) && sandbox.questStarted.includes(questId);
  } else if (normalizedType === "quest_completed") {
    const questId = Number(value);
    pass = Number.isInteger(questId) && sandbox.questCompleted.includes(questId);
  } else if (normalizedType === "item") {
    const parsed = parseItemValue(value);
    pass = Boolean(parsed.itemId) && (sandbox.inventory[parsed.itemId] || 0) >= parsed.quantity;
  } else {
    pass = true;
  }
  return notFlag ? !pass : pass;
}

export function resolveNodeConditionTextAndNext(node: JsonObject, sandbox: DialogueSandbox): { text: string; nextId: string } {
  const conditionsRaw = Array.isArray(node.conditions) ? node.conditions : [];
  const conditions = conditionsRaw.filter((entry) => isPlainObject(entry));
  for (const conditionEntry of conditions) {
    const pass = checkCondition(
      String(conditionEntry.conditionType || "always"),
      String(conditionEntry.conditionValue || ""),
      Boolean(conditionEntry.conditionNot),
      sandbox,
    );
    if (!pass) {
      continue;
    }
    const text = String(conditionEntry.text || node.text || node.description || "");
    const nextId = String(conditionEntry.nextId || node.nextNodeId || "").trim();
    return { text, nextId };
  }

  return {
    text: String(node.text || node.description || ""),
    nextId: String(node.nextNodeId || "").trim(),
  };
}

export function getVisibleChoices(node: JsonObject, sandbox: DialogueSandbox): DialogueChoice[] {
  const choicesRaw = Array.isArray(node.choices) ? node.choices : [];
  const choices = choicesRaw.filter((entry) => isPlainObject(entry));
  return choices
    .filter((choiceEntry) => {
      return checkCondition(
        String(choiceEntry.conditionType || "always"),
        String(choiceEntry.conditionValue || ""),
        Boolean(choiceEntry.conditionNot),
        sandbox,
      );
    })
    .map((choiceEntry) => ({
      text: String(choiceEntry.text || "(empty choice)"),
      nextId: String(choiceEntry.nextId || "").trim(),
      reactionType: String(choiceEntry.reactionType || "none").trim(),
      reactionValue: String(choiceEntry.reactionValue || "").trim(),
    }));
}

export function applyReactionToSandbox(sandbox: DialogueSandbox, reactionType: string, reactionValue: string): DialogueSandbox {
  const next = cloneSandbox(sandbox);
  const normalizedType = String(reactionType || "none").trim();
  if (normalizedType === "none" || !normalizedType) {
    return next;
  }

  if (normalizedType === "start_quest") {
    const questId = Number(reactionValue);
    if (Number.isInteger(questId) && !next.questStarted.includes(questId)) {
      next.questStarted.push(questId);
    }
  } else if (normalizedType === "complete_quest") {
    const questId = Number(reactionValue);
    if (Number.isInteger(questId) && !next.questCompleted.includes(questId)) {
      next.questCompleted.push(questId);
    }
  } else if (normalizedType === "grant_item") {
    const parsed = parseItemValue(reactionValue);
    if (parsed.itemId) {
      next.inventory[parsed.itemId] = (next.inventory[parsed.itemId] || 0) + parsed.quantity;
    }
  } else if (normalizedType === "remove_item") {
    const parsed = parseItemValue(reactionValue);
    if (parsed.itemId) {
      next.inventory[parsed.itemId] = Math.max(0, (next.inventory[parsed.itemId] || 0) - parsed.quantity);
    }
  }

  return next;
}

export function applyNodeReactionsToSandbox(node: JsonObject, sandbox: DialogueSandbox): DialogueSandbox {
  let next = cloneSandbox(sandbox);
  const reactionsRaw = Array.isArray(node.reactions) ? node.reactions : [];
  const reactions = reactionsRaw.filter((entry) => isPlainObject(entry));

  if (reactions.length > 0) {
    reactions.forEach((reactionEntry) => {
      next = applyReactionToSandbox(next, String(reactionEntry.reactionType || "none"), String(reactionEntry.reactionValue || ""));
    });
    return next;
  }

  if (Object.prototype.hasOwnProperty.call(node, "reactionType")) {
    next = applyReactionToSandbox(next, String(node.reactionType || "none"), String(node.reactionValue || ""));
  }

  return next;
}

