import type { LeaderboardEntry, LeaderboardResult } from "./onlineRepository";
import type { ProfileStatId, SaveSlotId } from "./types";

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

export interface CachedLeaderboard {
  accountId: string;
  hunterName: string;
  slotId: SaveSlotId;
  statId: ProfileStatId;
  updatedAt: string;
  result: LeaderboardResult;
}

const CACHE_KEY = "i-want-to-heal:leaderboards:cache:v1";
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;
}

function cacheId(slotId: SaveSlotId, statId: ProfileStatId) {
  return `${slotId}:${statId}`;
}

function leaderboardEntry(value: unknown): LeaderboardEntry | null {
  if (!value || typeof value !== "object") return null;
  const candidate = value as Partial<LeaderboardEntry>;
  if (!Number.isInteger(candidate.rank) || Number(candidate.rank) < 1) return null;
  if (typeof candidate.username !== "string" || typeof candidate.hunterName !== "string") return null;
  if (candidate.slotId !== 1 && candidate.slotId !== 2 && candidate.slotId !== 3) return null;
  if (!Number.isFinite(candidate.value) || Number(candidate.value) < 0) return null;
  if (candidate.secondaryValue !== undefined && (!Number.isFinite(candidate.secondaryValue) || Number(candidate.secondaryValue) < 0)) return null;
  return {
    rank: Number(candidate.rank),
    username: candidate.username,
    hunterName: candidate.hunterName,
    slotId: candidate.slotId,
    value: Number(candidate.value),
    ...(candidate.secondaryValue === undefined ? {} : { secondaryValue: Number(candidate.secondaryValue) }),
  };
}

function leaderboardResult(value: unknown): LeaderboardResult | null {
  if (!value || typeof value !== "object") return null;
  const candidate = value as Partial<LeaderboardResult>;
  if (candidate.kind !== "boss"
    && candidate.kind !== "roguelike"
    && candidate.kind !== "rogue-trials-endless"
    && candidate.kind !== "hockey-healing"
    && candidate.kind !== "hockey-pvp-wins"
    && candidate.kind !== "hockey-pvp-boss-kills"
    && candidate.kind !== "blockbreaker-bricks"
    && candidate.kind !== "blockbreaker-time"
    && candidate.kind !== "blockbreaker-score"
    && candidate.kind !== "aether-assault") return null;
  if (!Array.isArray(candidate.top)) return null;
  const top = candidate.top.map(leaderboardEntry);
  if (top.some((entry) => !entry)) return null;
  const current = candidate.current === null ? null : leaderboardEntry(candidate.current);
  if (candidate.current !== null && !current) return null;
  return {
    kind: candidate.kind,
    ...(candidate.kind === "boss" && typeof candidate.bossId === "string" ? { bossId: candidate.bossId } : {}),
    top: top as LeaderboardEntry[],
    current,
  };
}

function cachedLeaderboard(value: unknown): CachedLeaderboard | null {
  if (!value || typeof value !== "object") return null;
  const candidate = value as Partial<CachedLeaderboard>;
  if (typeof candidate.accountId !== "string" || typeof candidate.hunterName !== "string") return null;
  if (candidate.slotId !== 1 && candidate.slotId !== 2 && candidate.slotId !== 3) return null;
  if (typeof candidate.statId !== "string" || typeof candidate.updatedAt !== "string") return null;
  if (Number.isNaN(Date.parse(candidate.updatedAt))) return null;
  const result = leaderboardResult(candidate.result);
  if (!result) return null;
  return { ...candidate, result } as CachedLeaderboard;
}

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

  read(slotId: SaveSlotId, statId: ProfileStatId, hunterName: string, accountId: string | null): CachedLeaderboard | null {
    const entry = this.readAll()[cacheId(slotId, statId)];
    if (!entry || entry.slotId !== slotId || entry.statId !== statId || entry.hunterName !== hunterName) return null;
    if (accountId && entry.accountId !== accountId) return null;
    return structuredClone(entry);
  }

  write(accountId: string, hunterName: string, slotId: SaveSlotId, statId: ProfileStatId, result: LeaderboardResult): CachedLeaderboard {
    const entries = this.readAll();
    const entry: CachedLeaderboard = {
      accountId,
      hunterName,
      slotId,
      statId,
      updatedAt: this.now(),
      result: structuredClone(result),
    };
    entries[cacheId(slotId, statId)] = entry;
    this.storage.setItem(CACHE_KEY, JSON.stringify(entries));
    return structuredClone(entry);
  }

  clearSlot(slotId: SaveSlotId) {
    const entries = this.readAll();
    for (const key of Object.keys(entries)) {
      if (entries[key].slotId === slotId) delete entries[key];
    }
    this.storage.setItem(CACHE_KEY, JSON.stringify(entries));
  }

  private readAll(): Record<string, CachedLeaderboard> {
    try {
      const raw = this.storage.getItem(CACHE_KEY);
      const parsed = raw ? JSON.parse(raw) as Record<string, unknown> : {};
      if (!parsed || typeof parsed !== "object") return {};
      return Object.fromEntries(Object.entries(parsed).flatMap(([key, value]) => {
        const entry = cachedLeaderboard(value);
        return entry ? [[key, entry]] : [];
      }));
    } catch {
      return {};
    }
  }
}

export const leaderboardCache = new LeaderboardCache();
