import starterProfile from './offline-starter-profile.json'
import type {
  Account,
  AuthSession,
  CharacterProfile,
  DungeonReward,
  LootRoll,
  Item,
  EquipmentSlot,
} from './profile'

export type GameMode = 'online' | 'offline'

export interface GameRepository {
  loadSession(): Promise<AuthSession>
  register(username: string, password: string, characterName: string): Promise<AuthSession>
  login(username: string, password: string): Promise<AuthSession>
  logout(): Promise<void>
  loadProfile(): Promise<CharacterProfile>
  saveProfile(classId: number, abilitySlots: Array<number | null>): Promise<CharacterProfile>
  completeDungeon(
    dungeonId: number,
    difficultyId: number,
    resourceSpent: number,
    durationSeconds: number,
    completedPart?: number,
    startPart?: number,
    partDurationSeconds?: [number, number, number],
  ): Promise<DungeonReward>
  completeRoguelike(
    dungeonId: number,
    difficultyId: number,
    encountersCleared: number,
    resourceSpent: number,
    durationSeconds: number,
    options?: {
      bossesCleared?: number
      experienceMode?: 'default' | 'pvp-boss-quarter-level'
      lootSourceEncounterId?: number
      roguelikeStage?: number
    },
  ): Promise<DungeonReward>
  allocateTalent(talentId: number): Promise<CharacterProfile>
  resetTalents(): Promise<CharacterProfile>
  equipItem(itemId: number): Promise<CharacterProfile>
  discardExtraItem(itemId: number): Promise<CharacterProfile>
  breakdownItem(itemId: number): Promise<CharacterProfile>
  craftItem(recipeId: number): Promise<CharacterProfile>
  upgradeItem(itemId: number): Promise<CharacterProfile>
  rollEncounterLoot(
    encounterId: number,
    difficultyId: number,
    runToken: string,
  ): Promise<LootRoll>
}

type CharacterData = {
  level: number
  experience: number
  talentPoints: number
  abilitySlots: Array<number | null>
  talentRanks: Record<string, number>
  inventory: Item[]
}

type OfflineSave = {
  version: 3
  characterName: string
  activeClassId: number
  completedDungeonParts: number
  completedRaidPhases: number
  characters: Record<number, CharacterData>
  lootRolls: Record<string, LootRoll>
}

type OnlineCache = {
  version: 1
  account: Account
  save: OfflineSave
  dirty: boolean
}

export type CloudSyncStatus = {
  available: boolean
  dirty: boolean
}

type RepositoryMode = 'online' | 'offline-local' | 'offline-cached'

type NetworkError = Error & {
  network?: boolean
}

type LocalSaveStore = {
  readSave: () => OfflineSave | null
  writeSave: (save: OfflineSave) => void
  readAccount: () => Account | null
}

const modeKey = 'chronicle.repositoryMode'
const offlineSaveKey = 'chronicle.offlineSave.v1'
const onlineCacheKey = 'chronicle.onlineCache.v1'
const authTokenKey = 'chronicle.authToken.v1'
const offlineAccount = { id: -1, username: 'Offline' }

function clone<T>(value: T): T {
  return structuredClone(value)
}

function toGameMode(mode: RepositoryMode): GameMode {
  return mode === 'online' ? 'online' : 'offline'
}

function dispatchModeChange(mode: RepositoryMode) {
  if (typeof window === 'undefined') return
  window.dispatchEvent(new CustomEvent<GameMode>('chronicle:mode-changed', {
    detail: toGameMode(mode),
  }))
}

function readMode(): RepositoryMode {
  const stored = localStorage.getItem(modeKey)
  if (stored === 'offline-cached' || stored === 'offline-local' || stored === 'online') {
    return stored
  }
  if (stored === 'offline') return 'offline-local'
  return 'online'
}

function writeMode(mode: RepositoryMode) {
  localStorage.setItem(modeKey, mode)
  dispatchModeChange(mode)
}

function upgradeV1Save(v1: { profile: CharacterProfile; lootRolls: Record<string, LootRoll> }): OfflineSave {
  const p = v1.profile
  const classes = [1, 2, 3]
  const characters: Record<number, CharacterData> = {}
  for (const cid of classes) {
    const gameClass = p.classes.find((c) => c.id === cid)!
    const talentRanks: Record<string, number> = {}
    for (const t of gameClass.talents) {
      talentRanks[String(t.id)] = t.rank
    }
    characters[cid] = {
      level: cid === p.character.classId ? p.character.level : 1,
      experience: cid === p.character.classId ? p.character.experience : 0,
      talentPoints: cid === p.character.classId ? p.character.talentPoints : 1,
      abilitySlots: cid === p.character.classId ? [...p.abilitySlots] : [],
      talentRanks,
      inventory: cid === p.character.classId ? clone(p.inventory) : [],
    }
  }
  return {
    version: 3,
    characterName: p.character.name,
    activeClassId: p.character.classId,
    completedDungeonParts: p.completedDungeonParts,
    completedRaidPhases: p.completedRaidPhases ?? 0,
    characters,
    lootRolls: v1.lootRolls ?? {},
  }
}

function upgradeV2Save(v2: Omit<OfflineSave, 'version' | 'completedRaidPhases'> & { version: 2 }): OfflineSave {
  return {
    ...v2,
    version: 3,
    completedRaidPhases: 0,
  }
}

function normalizeOfflineSave(raw: unknown): OfflineSave | null {
  if (!raw || typeof raw !== 'object') return null
  const candidate = raw as {
    version?: number
    profile?: CharacterProfile
    lootRolls?: Record<string, LootRoll>
  }
  if (candidate.version === 3) return candidate as OfflineSave
  if (candidate.version === 2) {
    return upgradeV2Save(candidate as Omit<OfflineSave, 'version' | 'completedRaidPhases'> & { version: 2 })
  }
  if (candidate.version === 1 && candidate.profile) {
    return upgradeV1Save(candidate as { profile: CharacterProfile; lootRolls: Record<string, LootRoll> })
  }
  return null
}

function readSaveKey(key: string): OfflineSave | null {
  const serialized = localStorage.getItem(key)
  if (!serialized) return null
  try {
    const raw = JSON.parse(serialized)
    const save = normalizeOfflineSave(raw)
    if (!save) return null
    if (raw.version !== 3) {
      localStorage.setItem(key, JSON.stringify(save))
    }
    return save
  } catch {
    return null
  }
}

function readOfflineSave(): OfflineSave | null {
  return readSaveKey(offlineSaveKey)
}

function writeOfflineSave(save: OfflineSave) {
  localStorage.setItem(offlineSaveKey, JSON.stringify(save))
}

function readOnlineCache(): OnlineCache | null {
  const serialized = localStorage.getItem(onlineCacheKey)
  if (!serialized) return null
  try {
    const raw = JSON.parse(serialized) as {
      version?: number
      account?: Account
      save?: unknown
      dirty?: boolean
    }
    if (
      raw.version !== 1
      || !raw.account
      || typeof raw.account.id !== 'number'
      || typeof raw.account.username !== 'string'
    ) {
      return null
    }
    const save = normalizeOfflineSave(raw.save)
    if (!save) return null
    const cache: OnlineCache = {
      version: 1,
      account: raw.account,
      save,
      dirty: Boolean(raw.dirty),
    }
    if ((raw.save as { version?: number } | undefined)?.version !== 3) {
      writeOnlineCache(cache)
    }
    return cache
  } catch {
    return null
  }
}

function writeOnlineCache(cache: OnlineCache) {
  localStorage.setItem(onlineCacheKey, JSON.stringify(cache))
}

function clearOnlineCache() {
  localStorage.removeItem(onlineCacheKey)
}

function buildProfile(save: OfflineSave): CharacterProfile {
  const static_ = clone(starterProfile) as CharacterProfile
  const cd = save.characters[save.activeClassId]
  const gameClass = static_.classes.find((c) => c.id === save.activeClassId)!

  static_.character.name = save.characterName
  static_.character.level = cd.level
  static_.character.experience = cd.experience
  static_.character.talentPoints = cd.talentPoints
  static_.character.classId = gameClass.id
  static_.character.classSlug = gameClass.slug
  static_.character.className = gameClass.name
  static_.character.resourceName = gameClass.resourceName
  static_.character.maxResource = gameClass.maxResource
  static_.character.themeColor = gameClass.themeColor
  static_.character.classDescription = gameClass.description
  static_.character.currentLevelExperience = experienceForLevel(cd.level)
  static_.character.nextLevelExperience = cd.level >= static_.maxLevel
    ? experienceForLevel(static_.maxLevel)
    : experienceForLevel(cd.level + 1)

  static_.abilitySlots = cd.abilitySlots

  for (const c of static_.classes) {
    for (const t of c.talents) {
      t.rank = cd.talentRanks[String(t.id)] ?? 0
    }
  }

  static_.allocatedTalentPoints = gameClass.talents.reduce((s, t) => s + t.rank, 0)

  static_.inventory = cd.inventory
  updateGearStats(static_)
  updateSetBonuses(static_)
  updateCraftingRecipes(static_)
  static_.completedDungeonParts = save.completedDungeonParts
  static_.completedRaidPhases = save.completedRaidPhases

  return static_
}

function updateGearStats(profile: CharacterProfile) {
  const equipped = profile.inventory.filter((item) => item.equipped)
  profile.gearStats = {
    averageItemLevel: profile.equipmentSlots.length === 0
      ? 0
      : equipped.reduce((total, item) => total + item.itemLevel, 0)
        / profile.equipmentSlots.length,
    healingPower: equipped.reduce((total, item) => total + item.healingPower, 0),
    maxResourceBonus: equipped.reduce(
      (total, item) => total + item.maxResourceBonus,
      0,
    ),
  }
}

function updateSetBonuses(profile: CharacterProfile) {
  const equippedSetCounts = new Map<number, number>()
  for (const item of profile.inventory) {
    if (!item.equipped || !item.setId) continue
    equippedSetCounts.set(item.setId, (equippedSetCounts.get(item.setId) ?? 0) + 1)
  }
  profile.setBonuses = (profile.setBonuses ?? []).map((bonus) => {
    const equippedPieces = equippedSetCounts.get(bonus.setId) ?? 0
    return {
      ...bonus,
      equippedPieces,
      active: equippedPieces >= bonus.requiredPieces,
    }
  })
}

function updateCraftingRecipes(profile: CharacterProfile) {
  const owned = new Map(profile.inventory.map((item) => [item.id, item.quantity]))
  profile.craftingRecipes = (profile.craftingRecipes ?? []).map((recipe) => {
    const components = recipe.components.map((component) => ({
      ...component,
      owned: owned.get(component.item.id) ?? 0,
    }))
    return {
      ...recipe,
      components,
      canCraft: components.every((component) => component.owned >= component.quantity),
    }
  })
}

function addInventoryItem(inventory: Item[], item: Omit<Item, 'quantity' | 'equipped'>, quantity: number) {
  const existing = inventory.find((candidate) => candidate.id === item.id)
  if (existing) {
    existing.quantity += quantity
    return { duplicate: true, quantityAfter: existing.quantity }
  }
  inventory.push({
    ...item,
    quantity,
    equipped: false,
  })
  return { duplicate: false, quantityAfter: quantity }
}

function experienceForLevel(level: number) {
  return (level - 1) * (level - 1) * 100
}

function scaledPvpBossExperience(
  startingExperience: number,
  startingLevel: number,
  bossesCleared: number,
  maxLevel: number,
) {
  let experience = startingExperience
  let level = startingLevel
  const maxExperience = experienceForLevel(maxLevel)
  for (let bossIndex = 0; bossIndex < bossesCleared && experience < maxExperience; bossIndex += 1) {
    const currentLevelFloor = experienceForLevel(level)
    const nextLevelExperience = level >= maxLevel
      ? maxExperience
      : experienceForLevel(level + 1)
    const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
    experience = Math.min(maxExperience, experience + Math.round(levelBand * 0.25))
    while (level < maxLevel && experienceForLevel(level + 1) <= experience) {
      level += 1
    }
  }
  return { experience, level }
}

type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string }
const COMPONENT_ITEMS: Record<number, ComponentTemplate> = {
  1: { id: 600, slug: 'minor-component', name: 'Minor Component', itemLevel: 1, glyph: '◆', description: 'A basic crafting component.' },
  5: { id: 601, slug: 'basic-component', name: 'Basic Component', itemLevel: 5, glyph: '◇', description: 'A standard crafting component.' },
  10: { id: 602, slug: 'refined-component', name: 'Refined Component', itemLevel: 10, glyph: '◈', description: 'A refined crafting component.' },
  15: { id: 603, slug: 'advanced-component', name: 'Advanced Component', itemLevel: 15, glyph: '◉', description: 'An advanced crafting component.' },
  20: { id: 604, slug: 'superior-component', name: 'Superior Component', itemLevel: 20, glyph: '◎', description: 'A superior crafting component.' },
  25: { id: 605, slug: 'primal-component', name: 'Primal Component', itemLevel: 25, glyph: '✦', description: 'A primal crafting component.' },
}

type WindowWithApiBase = Window & {
  CAPACITOR_API_BASE_URL?: string
  CAPACITOR_AUTH_API_BASE_URL?: string
}

function componentForItemLevel(itemLevel: number): ComponentTemplate | undefined {
  const levels = Object.keys(COMPONENT_ITEMS).map(Number).sort((a, b) => a - b)
  let best = levels[0]
  for (const level of levels) {
    if (level <= itemLevel) best = level
  }
  return COMPONENT_ITEMS[best]
}

function mergeProfileIntoSave(profile: CharacterProfile, existingSave?: OfflineSave): OfflineSave {
  const characters = clone(existingSave?.characters ?? {})
  for (const gameClass of profile.classes) {
    if (!characters[gameClass.id]) {
      characters[gameClass.id] = emptyCharacterData(gameClass.id)
    }
  }
  const activeClass = profile.classes.find((candidate) => candidate.id === profile.character.classId)
  if (!activeClass) {
    throw new Error('The active class does not exist in the cached profile.')
  }
  const talentRanks: Record<string, number> = {}
  for (const talent of activeClass.talents) {
    talentRanks[String(talent.id)] = talent.rank
  }
  characters[profile.character.classId] = {
    level: profile.character.level,
    experience: profile.character.experience,
    talentPoints: profile.character.talentPoints,
    abilitySlots: [...profile.abilitySlots],
    talentRanks,
    inventory: clone(profile.inventory),
  }
  return {
    version: 3,
    characterName: profile.character.name,
    activeClassId: profile.character.classId,
    completedDungeonParts: profile.completedDungeonParts,
    completedRaidPhases: profile.completedRaidPhases,
    characters,
    lootRolls: clone(existingSave?.lootRolls ?? {}),
  }
}

function rollWeightedLootEntry<T extends { dropWeight: number }>(entries: T[]): T {
  const totalWeight = entries.reduce((total, entry) => total + entry.dropWeight, 0)
  let weightedRoll = Math.random() * totalWeight
  for (const entry of entries) {
    weightedRoll -= entry.dropWeight
    if (weightedRoll < 0) return entry
  }
  return entries[entries.length - 1]
}

function coinDropQuantity() {
  const roll = Math.random()
  if (roll < 0.15) return 3
  if (roll < 0.5) return 2
  return 1
}

function roguelikeCoinItemLevel(stage: number) {
  return Math.min(25, 5 + Math.max(0, Math.floor(stage / 5)) * 5)
}

function awardRoguelikeCoin(
  profile: CharacterProfile,
  sourceEncounterId: number | undefined,
  stage: number | undefined,
): DungeonReward['bonusItem'] {
  if (!sourceEncounterId || !stage) return null
  const targetItemLevel = roguelikeCoinItemLevel(stage)
  const sourceEncounter = profile.dungeons
    .flatMap((dungeon) => dungeon.encounters)
    .find((encounter) => encounter.id === sourceEncounterId)
  const coin = sourceEncounter?.lootTables
    .filter((entry) => entry.itemLevel === targetItemLevel)
    .sort((left, right) => left.difficultyId - right.difficultyId)[0]
  if (!coin) return null
  const {
    encounterId: _encounterId,
    difficultyId: _difficultyId,
    dropWeight: _dropWeight,
    dropChance: _dropChance,
    ...coinItem
  } = coin
  void _encounterId
  void _difficultyId
  void _dropWeight
  void _dropChance
  const quantity = coinDropQuantity()
  const added = addInventoryItem(profile.inventory, {
    ...coinItem,
    slot: coinItem.slot as EquipmentSlot,
    rarity: coinItem.rarity as Item['rarity'],
  }, quantity)
  return {
    ...coinItem,
    quantity,
    duplicate: added.duplicate,
    quantityAfter: added.quantityAfter,
  }
}

function readAuthToken(): string {
  return localStorage.getItem(authTokenKey) ?? ''
}

function writeAuthToken(token: string) {
  localStorage.setItem(authTokenKey, token)
}

function clearAuthToken() {
  localStorage.removeItem(authTokenKey)
}

function configuredBaseUrl(value: string | undefined): string {
  return value ? value.replace(/\/+$/, '') : ''
}

function getApiBaseUrl(path: string): string {
  const browserWindow = typeof window === 'undefined'
    ? undefined
    : window as WindowWithApiBase
  if (path.startsWith('/api/auth/')) {
    if (browserWindow?.CAPACITOR_AUTH_API_BASE_URL) {
      return configuredBaseUrl(browserWindow.CAPACITOR_AUTH_API_BASE_URL)
    }
    if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_AUTH_API_BASE_URL) {
      return configuredBaseUrl(import.meta.env.VITE_AUTH_API_BASE_URL)
    }
  }
  if (browserWindow?.CAPACITOR_API_BASE_URL) {
    return configuredBaseUrl(browserWindow.CAPACITOR_API_BASE_URL)
  }
  if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_API_BASE_URL) {
    return configuredBaseUrl(import.meta.env.VITE_API_BASE_URL)
  }
  return ''
}

async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
  const baseUrl = getApiBaseUrl(path)
  const url = baseUrl ? `${baseUrl}${path}` : path
  const headers = new Headers(init?.headers)
  const token = readAuthToken()
  if (token && !headers.has('Authorization')) {
    headers.set('Authorization', `Bearer ${token}`)
  }
  let response: Response
  try {
    response = await fetch(url, {
      ...init,
      headers,
    })
  } catch (reason) {
    const networkError = new Error('Unable to reach the game server.') as NetworkError
    networkError.network = true
    networkError.cause = reason
    throw networkError
  }
  const body = await response.json()
  if (!response.ok) throw new Error(body.error ?? 'Unable to reach the game server.')
  return body
}

function isNetworkError(reason: unknown): reason is NetworkError {
  return reason instanceof Error && Boolean((reason as NetworkError).network)
}

function cachedOnlineSession(): AuthSession | null {
  const cache = readOnlineCache()
  if (!cache) return null
  return {
    account: cache.account,
    profile: buildProfile(cache.save),
  }
}

function resumeCachedOnlineSession(): AuthSession | null {
  const session = cachedOnlineSession()
  if (!session) return null
  writeMode('offline-cached')
  return session
}

function cacheActiveOnlineProfile(profile: CharacterProfile, dirty = false) {
  const cache = readOnlineCache()
  if (!cache) return
  writeOnlineCache({
    ...cache,
    save: mergeProfileIntoSave(profile, cache.save),
    dirty,
  })
}

async function loadServerSyncSave(): Promise<OfflineSave> {
  return requestJson('/api/profile/sync-save')
}

async function pushServerSyncSave(save: OfflineSave): Promise<{ profile: CharacterProfile; save: OfflineSave }> {
  return requestJson('/api/profile/sync-save', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ save }),
  })
}

async function finalizeOnlineSession(session: AuthSession): Promise<AuthSession> {
  const cache = readOnlineCache()
  if (session.token) writeAuthToken(session.token)
  if (!session.account || !session.profile) {
    if (session.account && cache?.account.id === session.account.id) {
      return {
        account: session.account,
        profile: buildProfile(cache.save),
        token: session.token,
      }
    }
    return session
  }
  if (cache?.account.id === session.account.id && cache.dirty) {
    writeOnlineCache({
      ...cache,
      account: session.account,
    })
    return {
      account: session.account,
      profile: buildProfile(cache.save),
      token: session.token,
    }
  }
  try {
    const save = await loadServerSyncSave()
    writeOnlineCache({
      version: 1,
      account: session.account,
      save,
      dirty: false,
    })
  } catch {
    writeOnlineCache({
      version: 1,
      account: session.account,
      save: mergeProfileIntoSave(session.profile, cache?.account.id === session.account.id ? cache.save : undefined),
      dirty: false,
    })
  }
  return session
}

async function fallbackToCachedOnline<T>(reason: unknown, localAction: () => Promise<T>): Promise<T> {
  if (!isNetworkError(reason) || !readOnlineCache()) throw reason
  writeMode('offline-cached')
  return localAction()
}

async function withOnlineFallback<T>(
  onlineAction: () => Promise<T>,
  localAction: () => Promise<T>,
  onSuccess?: (result: T) => void | Promise<void>,
): Promise<T> {
  try {
    const result = await onlineAction()
    await onSuccess?.(result)
    return result
  } catch (reason) {
    return fallbackToCachedOnline(reason, localAction)
  }
}

async function loadOnlineSessionFromServer(): Promise<AuthSession> {
  const session = await requestJson<AuthSession>('/api/auth/session')
  return finalizeOnlineSession(session)
}

const serverRepository: GameRepository = {
  loadSession: async () => {
    try {
      return await loadOnlineSessionFromServer()
    } catch (reason) {
      if (isNetworkError(reason)) {
        const fallback = resumeCachedOnlineSession()
        if (fallback) return fallback
      }
      throw reason
    }
  },
  register: async (username, password, characterName) =>
    finalizeOnlineSession(await requestJson('/api/auth/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password, characterName }),
    })),
  login: async (username, password) =>
    finalizeOnlineSession(await requestJson('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    })),
  logout: async () => {
    try {
      await requestJson('/api/auth/logout', { method: 'POST' })
    } catch (reason) {
      if (!isNetworkError(reason)) throw reason
    }
    clearAuthToken()
    clearOnlineCache()
    writeMode('online')
  },
  loadProfile: () =>
    readOnlineCache()
      ? cachedOnlineLocalRepository.loadProfile()
      : withOnlineFallback(
        () => requestJson('/api/profile'),
        () => cachedOnlineLocalRepository.loadProfile(),
        (profile) => {
          cacheActiveOnlineProfile(profile)
        },
      ),
  saveProfile: (classId, abilitySlots) =>
    cachedOnlineLocalRepository.saveProfile(classId, abilitySlots),
  completeDungeon: (dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds) =>
    cachedOnlineLocalRepository.completeDungeon(
      dungeonId,
      difficultyId,
      resourceSpent,
      durationSeconds,
      completedPart,
      startPart,
      partDurationSeconds,
    ),
  completeRoguelike: (dungeonId, difficultyId, encountersCleared, resourceSpent, durationSeconds, options) =>
    cachedOnlineLocalRepository.completeRoguelike(
      dungeonId,
      difficultyId,
      encountersCleared,
      resourceSpent,
      durationSeconds,
      options,
    ),
  allocateTalent: (talentId) =>
    cachedOnlineLocalRepository.allocateTalent(talentId),
  resetTalents: () =>
    cachedOnlineLocalRepository.resetTalents(),
  equipItem: (itemId) =>
    cachedOnlineLocalRepository.equipItem(itemId),
  discardExtraItem: (itemId) =>
    cachedOnlineLocalRepository.discardExtraItem(itemId),
  breakdownItem: (itemId) =>
    cachedOnlineLocalRepository.breakdownItem(itemId),
  craftItem: (recipeId) =>
    cachedOnlineLocalRepository.craftItem(recipeId),
  upgradeItem: (itemId) =>
    cachedOnlineLocalRepository.upgradeItem(itemId),
  rollEncounterLoot: (encounterId, difficultyId, runToken) =>
    cachedOnlineLocalRepository.rollEncounterLoot(encounterId, difficultyId, runToken),
}

function emptyCharacterData(classId: number): CharacterData {
  const static_ = clone(starterProfile) as CharacterProfile
  const gc = static_.classes.find((c) => c.id === classId)!
  const talentRanks: Record<string, number> = {}
  for (const t of gc.talents) talentRanks[String(t.id)] = 0
  const inventory: Item[] = []
  const startingAbilitySlots: Array<number | null> = gc.spells
    .filter((s) => s.unlockLevel === 1)
    .slice(0, 5)
    .map((s) => s.id)
  while (startingAbilitySlots.length < 6) startingAbilitySlots.push(null)
  return {
    level: 1,
    experience: 0,
    talentPoints: 1,
    abilitySlots: startingAbilitySlots,
    talentRanks,
    inventory,
  }
}

function requireStoredSave(store: LocalSaveStore): OfflineSave {
  const save = store.readSave()
  if (!save) throw new Error('No local character exists yet.')
  return save
}

function createLocalRepository(store: LocalSaveStore): GameRepository {
  return {
    async loadSession() {
      const save = store.readSave()
      return {
        account: save ? (store.readAccount() ?? offlineAccount) : null,
        profile: save ? buildProfile(save) : null,
      }
    },
    async register() {
      throw new Error('Account registration requires online mode.')
    },
    async login() {
      throw new Error('Account login requires online mode.')
    },
    async logout() {
      writeMode('online')
    },
    async loadProfile() {
      return buildProfile(requireStoredSave(store))
    },
    async saveProfile(classId, abilitySlots) {
      const save = requireStoredSave(store)
    const static_ = clone(starterProfile) as CharacterProfile
    const gameClass = static_.classes.find((candidate) => candidate.id === classId)
    if (!gameClass) throw new Error('Selected class does not exist.')

    const slots = abilitySlots.slice(0, 6)
    while (slots.length < 6) slots.push(null)
    const selectedIds = slots.filter((id): id is number => id !== null)
    if (new Set(selectedIds).size !== selectedIds.length) {
      throw new Error('The same ability cannot be equipped twice.')
    }
    const activeChar = save.characters[save.activeClassId]
    const validIds = new Set(
      gameClass.spells
        .filter((spell) => spell.unlockLevel <= activeChar.level)
        .map((spell) => spell.id),
    )
    if (selectedIds.some((id) => !validIds.has(id))) {
      throw new Error('One or more abilities are locked or belong to another class.')
    }

    if (!save.characters[classId]) {
      save.characters[classId] = emptyCharacterData(classId)
    }
    save.characters[classId].abilitySlots = slots
    save.activeClassId = classId
      store.writeSave(save)
      return buildProfile(save)
    },
    async completeDungeon(dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds) {
    void startPart
    void partDurationSeconds
    if (!Number.isInteger(resourceSpent) || resourceSpent < 0) {
      throw new Error('The run resource total is invalid.')
    }
    if (!Number.isInteger(durationSeconds) || durationSeconds < 1) {
      throw new Error('The run duration is invalid.')
    }
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const dungeon = profile.dungeons.find((candidate) => candidate.id === dungeonId)
    const difficulty = dungeon?.difficulties.find(
      (candidate) => candidate.id === difficultyId,
    )
    if (!dungeon || !difficulty) {
      throw new Error('That difficulty is not available for this dungeon.')
    }
    const cd = save.characters[save.activeClassId]
    if (cd.level < difficulty.unlockLevel) {
      throw new Error(`${difficulty.name} unlocks at level ${difficulty.unlockLevel}.`)
    }

    const previousLevel = cd.level
    const previousExperience = cd.experience
    const partCount = completedPart ?? 1
    const experienceReward = Math.round(
      dungeon.experienceReward * difficulty.experienceMultiplier * partCount,
    )
    const maxExperience = experienceForLevel(profile.maxLevel)
    const newExperience = Math.min(previousExperience + experienceReward, maxExperience)
    let newLevel = previousLevel
    while (
      newLevel < profile.maxLevel
      && experienceForLevel(newLevel + 1) <= newExperience
    ) {
      newLevel += 1
    }
    const levelsGained = newLevel - previousLevel

    const gameClass = profile.classes.find(
      (candidate) => candidate.id === save.activeClassId,
    )!
    const unlockedAbilities = gameClass.spells
      .filter(
        (spell) =>
          spell.unlockLevel > previousLevel && spell.unlockLevel <= newLevel,
      )
      .map(({ id, name, unlockLevel, glyph }) => ({ id, name, unlockLevel, glyph }))

    cd.experience = newExperience
    cd.level = newLevel
    cd.talentPoints = Math.min(
      profile.maxTalentPoints,
      cd.talentPoints + levelsGained,
    )

    if (dungeon.contentType === 'raid') {
      save.completedRaidPhases = Math.max(save.completedRaidPhases, partCount)
    } else {
      save.completedDungeonParts = Math.max(save.completedDungeonParts, partCount)
    }

    let bonusItem: DungeonReward['bonusItem'] = null
    if ((startPart ?? 1) === 1 && partCount >= 3 && dungeon.completionLoot.length > 0) {
      const targetItemLevel = dungeon.completionItemLevel ?? difficulty.droppedItemLevel + 3
      const eligibleLoot = dungeon.completionLoot.filter(
        (item) => item.itemLevel >= targetItemLevel,
      )
      if (eligibleLoot.length > 0) {
        const rewardItemLevel = Math.min(...eligibleLoot.map((item) => item.itemLevel))
        const rewardPool = eligibleLoot.filter((item) => item.itemLevel === rewardItemLevel)
        const selected = rewardPool[Math.floor(Math.random() * rewardPool.length)]
        const existing = profile.inventory.find((item) => item.id === selected.id)
        const duplicate = Boolean(existing)
        let quantityAfter = 1
        if (existing) {
          existing.quantity += 1
          quantityAfter = existing.quantity
        } else {
          profile.inventory.push({
            ...selected,
            quantity: 1,
            equipped: false,
          })
        }
        cd.inventory = profile.inventory
        bonusItem = { ...selected, quantity: 1, duplicate, quantityAfter }
      }
    }

      store.writeSave(save)
    const updatedProfile = buildProfile(save)

    return {
      dungeonName: dungeon.name,
      difficultyName: difficulty.name,
      droppedItemLevel: difficulty.droppedItemLevel,
      experienceGained: newExperience - previousExperience,
      previousLevel,
      newLevel,
      levelsGained,
      talentPointsGained: levelsGained,
      resourceSpent,
      durationSeconds,
      averageItemLevel: updatedProfile.gearStats.averageItemLevel,
      unlockedAbilities,
      bonusItem,
      profile: updatedProfile,
    }
    },
    async completeRoguelike(dungeonId, difficultyId, encountersCleared, resourceSpent, durationSeconds, options) {
    if (!Number.isInteger(encountersCleared) || encountersCleared < 0) {
      throw new Error('The roguelike progress total is invalid.')
    }
    if (!Number.isInteger(resourceSpent) || resourceSpent < 0) {
      throw new Error('The run resource total is invalid.')
    }
    if (!Number.isInteger(durationSeconds) || durationSeconds < 1) {
      throw new Error('The run duration is invalid.')
    }
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const dungeon = profile.dungeons.find((candidate) => candidate.id === dungeonId)
    const difficulty = dungeon?.difficulties.find(
      (candidate) => candidate.id === difficultyId,
    )
    if (!dungeon || !difficulty) {
      throw new Error('That difficulty is not available for this roguelike.')
    }
    const cd = save.characters[save.activeClassId]
    if (cd.level < difficulty.unlockLevel) {
      throw new Error(`${difficulty.name} unlocks at level ${difficulty.unlockLevel}.`)
    }

    const previousLevel = cd.level
    const previousExperience = cd.experience
    const maxExperience = experienceForLevel(profile.maxLevel)
    const bossesCleared = Math.max(0, Math.floor(options?.bossesCleared ?? encountersCleared / 3))
    const scaledReward = options?.experienceMode === 'pvp-boss-quarter-level'
      ? scaledPvpBossExperience(previousExperience, previousLevel, bossesCleared, profile.maxLevel)
      : null
    const newExperience = scaledReward
      ? scaledReward.experience
      : Math.min(
        previousExperience
        + Math.round(dungeon.experienceReward * difficulty.experienceMultiplier * (encountersCleared / 3)),
        maxExperience,
      )
    let newLevel = scaledReward?.level ?? previousLevel
    while (
      newLevel < profile.maxLevel
      && experienceForLevel(newLevel + 1) <= newExperience
    ) {
      newLevel += 1
    }
    const levelsGained = newLevel - previousLevel

    const gameClass = profile.classes.find(
      (candidate) => candidate.id === save.activeClassId,
    )!
    const unlockedAbilities = gameClass.spells
      .filter(
        (spell) =>
          spell.unlockLevel > previousLevel && spell.unlockLevel <= newLevel,
      )
      .map(({ id, name, unlockLevel, glyph }) => ({ id, name, unlockLevel, glyph }))

    cd.experience = newExperience
    cd.level = newLevel
    cd.talentPoints = Math.min(
      profile.maxTalentPoints,
      cd.talentPoints + levelsGained,
    )
    const bonusItem = awardRoguelikeCoin(
      profile,
      options?.lootSourceEncounterId,
      options?.roguelikeStage,
    )
    cd.inventory = profile.inventory

      store.writeSave(save)
    const updatedProfile = buildProfile(save)

    return {
      dungeonName: `${dungeon.name} Roguelike`,
      difficultyName: difficulty.name,
      droppedItemLevel: difficulty.droppedItemLevel,
      experienceGained: newExperience - previousExperience,
      previousLevel,
      newLevel,
      levelsGained,
      talentPointsGained: levelsGained,
      resourceSpent,
      durationSeconds,
      averageItemLevel: updatedProfile.gearStats.averageItemLevel,
      unlockedAbilities,
      bonusItem,
      profile: updatedProfile,
    }
    },
    async allocateTalent(talentId) {
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const cd = save.characters[save.activeClassId]
    const gameClass = profile.classes.find(
      (candidate) => candidate.id === save.activeClassId,
    )!
    const talent = gameClass.talents.find((candidate) => candidate.id === talentId)
    if (!talent) throw new Error('That talent does not belong to the active class.')
    if (cd.talentPoints <= 0) {
      throw new Error('No talent points are available.')
    }
    if (talent.rank >= talent.maxRank) {
      throw new Error('That talent is already at maximum rank.')
    }
    const lowerTierPoints = gameClass.talents
      .filter((candidate) => candidate.tier < talent.tier)
      .reduce((total, candidate) => total + candidate.rank, 0)
    const requiredTierPoints = (talent.tier - 1) * 5
    if (lowerTierPoints < requiredTierPoints) {
      throw new Error(`Spend ${requiredTierPoints} points in earlier tiers first.`)
    }
    if (talent.prerequisiteTalentId) {
      const prerequisite = gameClass.talents.find(
        (candidate) => candidate.id === talent.prerequisiteTalentId,
      )
      if ((prerequisite?.rank ?? 0) < talent.prerequisiteRank) {
        throw new Error(
          `The prerequisite talent requires rank ${talent.prerequisiteRank}.`,
        )
      }
    }
    cd.talentRanks[String(talentId)] = (cd.talentRanks[String(talentId)] ?? 0) + 1
    cd.talentPoints -= 1
      store.writeSave(save)
      return buildProfile(save)
    },
    async resetTalents() {
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const cd = save.characters[save.activeClassId]
    const gameClass = profile.classes.find(
      (candidate) => candidate.id === save.activeClassId,
    )!
    const refunded = gameClass.talents.reduce(
      (total, talent) => total + talent.rank,
      0,
    )
    for (const talent of gameClass.talents) {
      cd.talentRanks[String(talent.id)] = 0
    }
    cd.talentPoints = Math.min(
      profile.maxTalentPoints,
      cd.talentPoints + refunded,
    )
      store.writeSave(save)
      return buildProfile(save)
    },
    async equipItem(itemId) {
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const item = profile.inventory.find((candidate) => candidate.id === itemId)
    if (!item) throw new Error('That item is not in the character inventory.')
    for (const candidate of profile.inventory) {
      if (candidate.slot === item.slot) candidate.equipped = candidate.id === item.id
    }
    save.characters[save.activeClassId].inventory = profile.inventory
      store.writeSave(save)
      return buildProfile(save)
    },
    async discardExtraItem(itemId) {
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const item = profile.inventory.find((candidate) => candidate.id === itemId)
    if (!item) throw new Error('That item is not in the character inventory.')
    if (item.quantity <= 1) throw new Error('Only extra copies can be discarded.')
    item.quantity -= 1
    save.characters[save.activeClassId].inventory = profile.inventory
      store.writeSave(save)
      return buildProfile(save)
    },
    async breakdownItem(itemId) {
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const item = profile.inventory.find((candidate) => candidate.id === itemId)
    if (!item) throw new Error('That item is not in the character inventory.')
    if (item.slot === 'component') throw new Error('Components cannot be broken down.')
    if (item.equipped && item.quantity <= 1) {
      throw new Error('Equipped items cannot be broken down.')
    }
    const componentTemplate = componentForItemLevel(item.itemLevel)
    if (!componentTemplate) throw new Error('No component type exists for this item level.')

    if (item.quantity <= 1) {
      profile.inventory.splice(profile.inventory.indexOf(item), 1)
    } else {
      item.quantity -= 1
    }

    const existing = profile.inventory.find((c) => c.id === componentTemplate.id)
    const count = Math.floor(Math.random() * 3) + 1
    if (existing) {
      existing.quantity += count
    } else {
      profile.inventory.push({
        id: componentTemplate.id,
        slug: componentTemplate.slug,
        name: componentTemplate.name,
        slot: 'component' as EquipmentSlot,
        rarity: 'common' as const,
        itemLevel: componentTemplate.itemLevel,
        healingPower: 0,
        maxResourceBonus: 0,
        glyph: componentTemplate.glyph,
        description: componentTemplate.description,
        quantity: count,
        equipped: false,
      })
    }

    save.characters[save.activeClassId].inventory = profile.inventory
      store.writeSave(save)
      return buildProfile(save)
    },
    async craftItem(recipeId) {
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const recipe = profile.craftingRecipes.find((candidate) => candidate.id === recipeId)
    if (!recipe) throw new Error('That crafting recipe does not exist.')
    const requiresUpgrade = profile.craftingRecipes.some((candidate) =>
      candidate.sourceEncounterId === recipe.sourceEncounterId
      && candidate.item.slot === recipe.item.slot
      && candidate.item.itemLevel < recipe.item.itemLevel,
    )
    if (requiresUpgrade) throw new Error('Upgrade the previous item tier instead.')
    const missing = recipe.components.find((component) => component.owned < component.quantity)
    if (missing) {
      throw new Error(`Need ${missing.quantity} ${missing.item.name} to craft this item.`)
    }

    for (const component of recipe.components) {
      const owned = profile.inventory.find((candidate) => candidate.id === component.item.id)
      if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to craft this item.`)
      owned.quantity -= component.quantity
    }
    for (let index = profile.inventory.length - 1; index >= 0; index -= 1) {
      if (profile.inventory[index].quantity <= 0) profile.inventory.splice(index, 1)
    }

    addInventoryItem(profile.inventory, recipe.item, 1)
    save.characters[save.activeClassId].inventory = profile.inventory
      store.writeSave(save)
      return buildProfile(save)
    },
    async upgradeItem(itemId) {
      const save = requireStoredSave(store)
    const profile = buildProfile(save)
    const item = profile.inventory.find((candidate) => candidate.id === itemId)
    if (!item) throw new Error('That item is not in the character inventory.')
    if (item.slot === 'component') throw new Error('Components cannot be upgraded.')
    const currentRecipe = profile.craftingRecipes.find((recipe) => recipe.item.id === item.id)
    const targetRecipe = currentRecipe
      ? profile.craftingRecipes
        .filter((recipe) =>
          recipe.sourceEncounterId === currentRecipe.sourceEncounterId
          && recipe.item.slot === item.slot
          && recipe.item.itemLevel > item.itemLevel,
        )
        .sort((left, right) => left.item.itemLevel - right.item.itemLevel)[0]
      : null
    if (!targetRecipe) throw new Error('No upgrade is available for this item.')
    const missing = targetRecipe.components.find((component) => component.owned < component.quantity)
    if (missing) {
      throw new Error(`Need ${missing.quantity} ${missing.item.name} to upgrade this item.`)
    }

    for (const component of targetRecipe.components) {
      const owned = profile.inventory.find((candidate) => candidate.id === component.item.id)
      if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to upgrade this item.`)
      owned.quantity -= component.quantity
    }
    const wasEquipped = item.equipped
    item.quantity -= 1
    item.equipped = false
    for (let index = profile.inventory.length - 1; index >= 0; index -= 1) {
      if (profile.inventory[index].quantity <= 0) profile.inventory.splice(index, 1)
    }
    if (wasEquipped) {
      for (const candidate of profile.inventory) {
        if (candidate.slot === targetRecipe.item.slot) candidate.equipped = false
      }
    }
    addInventoryItem(profile.inventory, targetRecipe.item, 1)
    if (wasEquipped) {
      const upgraded = profile.inventory.find((candidate) => candidate.id === targetRecipe.item.id)
      if (upgraded) upgraded.equipped = true
    }
    save.characters[save.activeClassId].inventory = profile.inventory
      store.writeSave(save)
      return buildProfile(save)
    },
    async rollEncounterLoot(encounterId, difficultyId, runToken) {
    if (runToken.length < 8 || runToken.length > 100) {
      throw new Error('A valid dungeon run token is required.')
    }
      const save = requireStoredSave(store)
    const rollKey = `${runToken}:${encounterId}:${difficultyId}`
    if (save.lootRolls[rollKey]) return clone(save.lootRolls[rollKey])

    const profile = buildProfile(save)
    const encounter = profile.dungeons
      .flatMap((dungeon) => dungeon.encounters)
      .find((candidate) => candidate.id === encounterId)
    const difficulty = profile.dungeons
      .flatMap((dungeon) => dungeon.difficulties)
      .find((candidate) => candidate.id === difficultyId)
    const entries = encounter?.lootTables.filter(
      (entry) => entry.difficultyId === difficultyId,
    ) ?? []
    if (!encounter || !difficulty || entries.length === 0) {
      throw new Error('This encounter has no configured loot.')
    }
    const dropChance = entries[0].dropChance
    const items: LootRoll['items'] = []

    const selectedQuantities = new Map<number, { entry: typeof entries[number]; quantity: number }>()
    if (Math.random() < dropChance) {
      const selected = rollWeightedLootEntry(entries)
      selectedQuantities.set(selected.id, {
        entry: selected,
        quantity: coinDropQuantity(),
      })
    }

    for (const { entry, quantity } of selectedQuantities.values()) {
        const {
          encounterId: _encounterId,
          difficultyId: _difficultyId,
          dropWeight: _dropWeight,
          dropChance: _dropChance,
          ...rolledItem
        } = entry
        void _encounterId
        void _difficultyId
        void _dropWeight
        void _dropChance
        const added = addInventoryItem(profile.inventory, {
          ...rolledItem,
          slot: rolledItem.slot as EquipmentSlot,
          rarity: rolledItem.rarity as Item['rarity'],
        }, quantity)
        items.push({
          ...rolledItem,
          slot: rolledItem.slot as EquipmentSlot,
          rarity: rolledItem.rarity as Item['rarity'],
          quantity,
          duplicate: added.duplicate,
          quantityAfter: added.quantityAfter,
        })
    }

    const item = items[0] ?? null
    const result: LootRoll = {
      encounterId,
      encounterName: encounter.enemyName,
      difficultyId,
      difficultyName: difficulty.name,
      dropChance,
      dropped: items.length > 0,
      item,
      items,
      awarded: Boolean(item),
      duplicate: items.some((candidate) => candidate.duplicate),
      quantityAfter: item?.quantityAfter ?? 0,
    }
    save.lootRolls[rollKey] = result
    save.characters[save.activeClassId].inventory = profile.inventory
      store.writeSave(save)
      return clone(result)
    },
  }
}

const offlineRepository = createLocalRepository({
  readSave: readOfflineSave,
  writeSave: writeOfflineSave,
  readAccount: () => offlineAccount,
})

const cachedOnlineLocalRepository = createLocalRepository({
  readSave: () => readOnlineCache()?.save ?? null,
  writeSave: (save) => {
    const cache = readOnlineCache()
    if (!cache) throw new Error('No cached account save exists yet.')
    writeOnlineCache({
      ...cache,
      save,
      dirty: true,
    })
  },
  readAccount: () => readOnlineCache()?.account ?? null,
})

const cachedOnlineRepository: GameRepository = {
  async loadSession() {
    try {
      const session = await loadOnlineSessionFromServer()
      if (session.account && session.profile) {
        writeMode('online')
        return session
      }
    } catch {
      // Fall through to local cached mirror.
    }
    return cachedOnlineLocalRepository.loadSession()
  },
  async register() {
    throw new Error('Account registration requires online mode.')
  },
  async login() {
    throw new Error('Account login requires online mode.')
  },
  async logout() {
    clearAuthToken()
    clearOnlineCache()
    writeMode('online')
  },
  loadProfile: () => cachedOnlineLocalRepository.loadProfile(),
  saveProfile: (classId, abilitySlots) => cachedOnlineLocalRepository.saveProfile(classId, abilitySlots),
  completeDungeon: (dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds) =>
    cachedOnlineLocalRepository.completeDungeon(
      dungeonId,
      difficultyId,
      resourceSpent,
      durationSeconds,
      completedPart,
      startPart,
      partDurationSeconds,
    ),
  completeRoguelike: (dungeonId, difficultyId, encountersCleared, resourceSpent, durationSeconds, options) =>
    cachedOnlineLocalRepository.completeRoguelike(
      dungeonId,
      difficultyId,
      encountersCleared,
      resourceSpent,
      durationSeconds,
      options,
    ),
  allocateTalent: (talentId) => cachedOnlineLocalRepository.allocateTalent(talentId),
  resetTalents: () => cachedOnlineLocalRepository.resetTalents(),
  equipItem: (itemId) => cachedOnlineLocalRepository.equipItem(itemId),
  discardExtraItem: (itemId) => cachedOnlineLocalRepository.discardExtraItem(itemId),
  breakdownItem: (itemId) => cachedOnlineLocalRepository.breakdownItem(itemId),
  craftItem: (recipeId) => cachedOnlineLocalRepository.craftItem(recipeId),
  upgradeItem: (itemId) => cachedOnlineLocalRepository.upgradeItem(itemId),
  rollEncounterLoot: (encounterId, difficultyId, runToken) =>
    cachedOnlineLocalRepository.rollEncounterLoot(encounterId, difficultyId, runToken),
}

export function getGameMode(): GameMode {
  return toGameMode(readMode())
}

export function getCloudSyncStatus(): CloudSyncStatus {
  const cache = readOnlineCache()
  return {
    available: Boolean(cache),
    dirty: Boolean(cache?.dirty),
  }
}

export async function syncCloudSave(): Promise<CharacterProfile> {
  const cache = readOnlineCache()
  if (!cache) {
    throw new Error('No signed-in save is available for cloud sync.')
  }
  const synced = await pushServerSyncSave(cache.save)
  writeOnlineCache({
    version: 1,
    account: cache.account,
    save: synced.save,
    dirty: false,
  })
  writeMode('online')
  return synced.profile
}

export function selectOnlineMode() {
  writeMode('online')
}

export function createOfflineCharacter(characterName: string): AuthSession {
  const name = characterName.trim() || 'Mira'
  if (!/^[A-Za-z][A-Za-z0-9 '-]{1,19}$/.test(name)) {
    throw new Error('Character name must be 2-20 characters and start with a letter.')
  }
  const characters: Record<number, CharacterData> = {}
  for (const cid of [1, 2, 3]) {
    characters[cid] = emptyCharacterData(cid)
  }
  const save: OfflineSave = {
    version: 3,
    characterName: name,
    activeClassId: 1,
    completedDungeonParts: 0,
    completedRaidPhases: 0,
    characters,
    lootRolls: {},
  }
  writeOfflineSave(save)
  writeMode('offline-local')
  return { account: offlineAccount, profile: buildProfile(save) }
}

export function resumeOfflineCharacter(): AuthSession | null {
  const save = readOfflineSave()
  if (!save) return null
  writeMode('offline-local')
  return { account: offlineAccount, profile: buildProfile(save) }
}

export function hasOfflineCharacter(): boolean {
  return readOfflineSave() !== null
}

export function activeGameRepository(): GameRepository {
  const mode = readMode()
  if (mode === 'offline-local') return offlineRepository
  if (mode === 'offline-cached') return cachedOnlineRepository
  return serverRepository
}
