import { createHunterSave } from "./data";
import { createClassInventory } from "../game/healers";
import { AVAILABLE_BOSS_IDS, BOSS_DEFINITIONS } from "../game/bossCatalog";
import { createDefaultGearProgress, GEAR_OWNER_ORDER, GEAR_SLOT_ORDER, MAX_GEAR_LEVEL, type GearProgress } from "../game/progression/gear";
import { normalizeActiveInfusionId, normalizePassiveInfusionId } from "../game/progression/infusions";
import { GROUP_DROP_TABLES, type CollectionLog, type MaterialStack } from "../game/progression/loot";
import type { BossId, HealerClassId } from "../game/types";
import type { HunterSave, SaveSlotId, SaveSlotState } from "./types";

export interface StorageAdapter {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
}

type SaveMap = Partial<Record<SaveSlotId, HunterSave>>;

const LOCAL_KEY = "i-want-to-heal:saves:local:v1";
const SLOT_IDS: SaveSlotId[] = [1, 2, 3];

const fallbackMemory = new Map<string, string>();
const fallbackStorage: StorageAdapter = {
  getItem: (key) => fallbackMemory.get(key) ?? null,
  setItem: (key, value) => { fallbackMemory.set(key, value); },
};

function browserStorage(): StorageAdapter {
  try {
    if (typeof localStorage !== "undefined") return localStorage;
  } catch {
    // Android WebView can deny storage before its host is ready.
  }
  return fallbackStorage;
}

interface LegacyHunterSave {
  schemaVersion?: number;
  slotId?: SaveSlotId;
  hunterName?: string;
  activeClassId?: HealerClassId;
  healers?: HunterSave["healers"];
  level?: number;
  location?: string;
  playSeconds?: number;
  updatedAt?: string;
  stats?: HunterSave["stats"];
  materials?: MaterialStack[];
  collectionLog?: CollectionLog;
  gearProgress?: GearProgress;
}

const HEALER_IDS: HealerClassId[] = ["priest", "druid", "shaman"];

function positiveCounts(value: unknown): Record<string, number> {
  if (!value || typeof value !== "object") return {};
  return Object.fromEntries(Object.entries(value as Record<string, unknown>).flatMap(([id, rawQuantity]) => {
    const quantity = Math.max(0, Math.floor(Number(rawQuantity) || 0));
    return quantity > 0 ? [[id, quantity]] : [];
  }));
}

function normalizeCollectionLog(candidate: LegacyHunterSave): CollectionLog {
  return {
    dropsFound: positiveCounts(candidate.collectionLog?.dropsFound),
    petsFound: positiveCounts(candidate.collectionLog?.petsFound),
  };
}

function knownMaterial(id: string) {
  for (const table of Object.values(GROUP_DROP_TABLES)) {
    const drop = Object.values(table.drops).find((candidate) => candidate.id === id);
    if (drop) return drop;
  }
  return undefined;
}

function normalizeMaterials(value: unknown, collectionLog: CollectionLog): MaterialStack[] {
  const quantities = new Map<string, number>();
  if (Array.isArray(value)) {
    for (const raw of value) {
      if (!raw || typeof raw !== "object") continue;
      const item = raw as Partial<MaterialStack>;
      if (!item.id) continue;
      const quantity = Math.max(0, Math.floor(Number(item.quantity) || 0));
      if (quantity > 0) quantities.set(item.id, (quantities.get(item.id) ?? 0) + quantity);
    }
  } else {
    for (const [id, quantity] of Object.entries(collectionLog.dropsFound)) quantities.set(id, quantity);
  }
  return [...quantities].flatMap(([id, quantity]) => {
    const drop = knownMaterial(id);
    return drop ? [{ id, quantity, name: drop.name, rarity: drop.rarity, itemLevel: drop.itemLevel, glyph: drop.glyph }] : [];
  });
}

function normalizeGearProgress(value: unknown): GearProgress {
  const defaults = createDefaultGearProgress();
  if (!value || typeof value !== "object") return defaults;
  const candidate = value as Partial<GearProgress>;
  for (const ownerId of GEAR_OWNER_ORDER) {
    for (const slotId of GEAR_SLOT_ORDER) {
      const level = Math.max(0, Math.min(MAX_GEAR_LEVEL, Math.floor(Number(candidate[ownerId]?.slots?.[slotId]?.level) || 0)));
      defaults[ownerId].slots[slotId].level = level as GearProgress[typeof ownerId]["slots"][typeof slotId]["level"];
    }
    defaults[ownerId].infusionAbilityId = normalizeActiveInfusionId(ownerId, candidate[ownerId]?.infusionAbilityId);
    defaults[ownerId].passiveInfusionId = normalizePassiveInfusionId(ownerId, candidate[ownerId]?.passiveInfusionId);
  }
  return defaults;
}

function normalizeBossKills(value: unknown): Record<string, number> {
  const source = positiveCounts(value);
  const result: Record<string, number> = {};
  for (const [key, quantity] of Object.entries(source)) {
    const bossId = AVAILABLE_BOSS_IDS.find((id) => id === key || BOSS_DEFINITIONS[id].name === key);
    result[bossId ?? key] = (result[bossId ?? key] ?? 0) + quantity;
  }
  return result;
}

function normalizeSave(value: unknown): HunterSave | null {
  if (!value || typeof value !== "object") return null;
  const candidate = value as LegacyHunterSave;
  if (!candidate.slotId || !candidate.hunterName) return null;
  if (candidate.schemaVersion !== 5) {
    try {
      return createHunterSave(candidate.slotId, typeof candidate.updatedAt === "string" ? candidate.updatedAt : new Date(0).toISOString(), candidate.hunterName);
    } catch {
      return null;
    }
  }
  const collectionLog = normalizeCollectionLog(candidate);
  const bossKills = normalizeBossKills(candidate.stats?.bossKills);
  const activeClassId = HEALER_IDS.includes(candidate.activeClassId as HealerClassId) ? candidate.activeClassId as HealerClassId : "priest";
  return {
    schemaVersion: 5,
    slotId: candidate.slotId,
    hunterName: candidate.hunterName,
    activeClassId,
    healers: Object.fromEntries(HEALER_IDS.map((classId) => [classId, {
      level: Math.max(1, candidate.healers?.[classId]?.level ?? (classId === "priest" ? candidate.level ?? 1 : 1)),
      inventory: candidate.healers?.[classId]?.inventory ?? createClassInventory(classId),
    }])) as HunterSave["healers"],
    location: candidate.location ?? "Ember Vault Approach",
    playSeconds: Math.max(0, candidate.playSeconds ?? 0),
    updatedAt: candidate.updatedAt ?? new Date(0).toISOString(),
    stats: {
      totalBossKills: Math.max(0, candidate.stats?.totalBossKills ?? Object.values(bossKills).reduce((sum, count) => sum + count, 0)),
      flawlessClears: Math.max(0, candidate.stats?.flawlessClears ?? 0),
      alliesSaved: Math.max(0, candidate.stats?.alliesSaved ?? 0),
      healingDone: Math.max(0, candidate.stats?.healingDone ?? 0),
      bossKills,
      highestRoguelikeRound: Math.max(0, Math.floor(candidate.stats?.highestRoguelikeRound ?? 0)),
      highestRogueTrialsEndlessKills: Math.max(0, Math.floor(candidate.stats?.highestRogueTrialsEndlessKills ?? 0)),
    },
    materials: normalizeMaterials(candidate.materials, collectionLog),
    collectionLog,
    gearProgress: normalizeGearProgress(candidate.gearProgress),
  };
}

function parseSaveMap(raw: string | null): SaveMap {
  if (!raw) return {};
  try {
    const parsed = JSON.parse(raw) as Record<string, unknown>;
    if (!parsed || typeof parsed !== "object") return {};
    return Object.fromEntries(Object.entries(parsed).flatMap(([id, value]) => {
      const save = normalizeSave(value);
      return save ? [[id, save]] : [];
    })) as SaveMap;
  } catch {
    return {};
  }
}

function cloneSave(save: HunterSave): HunterSave {
  return structuredClone(save);
}

export class SaveRepository {
  constructor(
    private readonly storage: StorageAdapter = browserStorage(),
    private readonly now: () => string = () => new Date().toISOString(),
  ) {}

  listLocal(): SaveSlotState[] {
    const local = this.read(LOCAL_KEY);
    return SLOT_IDS.map((id) => ({ id, local: local[id] ?? null, online: null }));
  }

  create(slotId: SaveSlotId, hunterName: string): HunterSave {
    const save = createHunterSave(slotId, this.now(), hunterName);
    this.setLocal(save);
    return save;
  }

  touch(slotId: SaveSlotId): HunterSave | null {
    return this.updateLocal(slotId, (save) => ({ ...save, updatedAt: this.now() }));
  }

  updateLocal(slotId: SaveSlotId, update: (save: HunterSave) => HunterSave): HunterSave | null {
    const saves = this.read(LOCAL_KEY);
    const source = saves[slotId];
    if (!source) return null;
    const next = { ...update(cloneSave(source)), slotId, updatedAt: this.now() };
    saves[slotId] = next;
    this.write(LOCAL_KEY, saves);
    return next;
  }

  deleteLocal(slotId: SaveSlotId): void {
    const saves = this.read(LOCAL_KEY);
    delete saves[slotId];
    this.write(LOCAL_KEY, saves);
  }

  copyLocal(sourceId: SaveSlotId, targetId: SaveSlotId): HunterSave | null {
    const saves = this.read(LOCAL_KEY);
    const source = saves[sourceId];
    if (!source || sourceId === targetId) return null;
    const copy = { ...cloneSave(source), slotId: targetId, updatedAt: this.now() };
    saves[targetId] = copy;
    this.write(LOCAL_KEY, saves);
    return copy;
  }

  replaceLocal(save: HunterSave): HunterSave {
    const normalized = { ...cloneSave(save), updatedAt: this.now() };
    this.setLocal(normalized);
    return normalized;
  }

  private setLocal(save: HunterSave): void {
    const saves = this.read(LOCAL_KEY);
    saves[save.slotId] = save;
    this.write(LOCAL_KEY, saves);
  }

  private read(key: string): SaveMap {
    const raw = this.storage.getItem(key);
    const saves = parseSaveMap(raw);
    const normalized = JSON.stringify(saves);
    if (raw !== normalized) this.storage.setItem(key, normalized);
    return saves;
  }

  private write(key: string, saves: SaveMap): void {
    this.storage.setItem(key, JSON.stringify(saves));
  }
}

export function formatSaveTimestamp(value: string): string {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "Unknown time";
  return new Intl.DateTimeFormat(undefined, {
    month: "short",
    day: "numeric",
    year: "numeric",
    hour: "numeric",
    minute: "2-digit",
  }).format(date);
}

export function formatPlayTime(seconds: number): string {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  return `${hours}h ${String(minutes).padStart(2, "0")}m`;
}
