// Loads the dictionary reader for a (variant, version) pair for the local move
// preview, three tiers deep: an in-memory reader instance, then the persistent
// IndexedDB blob cache, then a session-gated network fetch (which repopulates the
// cache). Every tier is best-effort: on any failure getDawg resolves to null and
// the caller falls back to the network `evaluate`. Concurrent requests for the
// same dictionary share one in-flight load.

import { Dawg } from './dawg';
import { dictKey, idbGetDawg, idbPutDawg, idbDelDawg, requestPersist } from './store';
import { gateway } from '../gateway';
import { clientChannel } from '../channel';
import { noteDictFetched, noteDictCacheHit, noteDictMiss as noteDictMissMetric } from '../localeval-metrics';
import type { Variant } from '../model';

// Loaded readers by key. A dictionary is immutable, so an instance is reused for
// the whole app session once loaded.
const instances = new Map<string, Dawg>();
// In-flight loads by key, so overlapping callers (prefetch + a game open) share one.
const inflight = new Map<string, Promise<Dawg | null>>();

// Bad-connection breaker: after a few dictionaries fail to become available in a session
// (a load that failed or a warm-up that hit its cap), stop attempting them so the player
// is not stuck re-watching the warm-up overlay when switching games. Session-scoped (in
// memory) — an app relaunch is a natural retry, when the connection may have recovered.
// Cached dictionaries still load.
const MISS_LIMIT = 3;
let misses = 0;
let dictDisabled = false;

/**
 * noteDictMiss records that a dictionary did not become available in time — a load that
 * failed or a warm-up that hit its cap. After MISS_LIMIT in a session the breaker trips.
 */
export function noteDictMiss(): void {
  if (!dictDisabled && ++misses >= MISS_LIMIT) dictDisabled = true;
  noteDictMissMetric();
}

/** dictLoadingDisabled reports whether the bad-connection breaker has tripped for this
 *  session, so callers can skip the warm-up overlay and go straight to the network. */
export function dictLoadingDisabled(): boolean {
  return dictDisabled;
}

// One-shot: the debug reset sets this so the next network fetch bypasses the browser HTTP
// cache. The immutable dict blob is otherwise cached for a year, so clearing IndexedDB alone
// still reloads it instantly from the HTTP cache — this forces a genuinely cold load.
let bustNext = false;

/** bustDictHttpCache makes the next dictionary download bypass the browser HTTP cache. */
export function bustDictHttpCache(): void {
  bustNext = true;
}

/**
 * getDawg resolves the reader for the (variant, version) dictionary, or null when
 * it cannot be obtained (no session, offline, storage or decode failure) — the
 * caller then previews over the network instead. Successful loads are cached in
 * memory and, when freshly fetched, in IndexedDB.
 */
export function getDawg(variant: Variant, version: string, signal?: AbortSignal): Promise<Dawg | null> {
  const key = dictKey(variant, version);
  const have = instances.get(key);
  if (have) return Promise.resolve(have);
  const pending = inflight.get(key);
  if (pending) return pending;
  const p = load(variant, version, key, signal).finally(() => inflight.delete(key));
  inflight.set(key, p);
  return p;
}

async function load(variant: Variant, version: string, key: string, signal?: AbortSignal): Promise<Dawg | null> {
  // Tier 1: the persistent cache.
  try {
    const cached = await idbGetDawg(key);
    if (cached) {
      const reader = new Dawg(new Uint8Array(cached));
      instances.set(key, reader);
      noteDictCacheHit();
      return reader;
    }
  } catch {
    // A cached blob the reader rejected (a stale or partial entry): evict it so the next
    // load re-fetches instead of failing on it forever, then fall through to the bundled/network tiers.
    void idbDelDawg(key);
  }

  // Tier 2: a dictionary bundled in a native app's assets (offline-first). Attempted only on a native
  // channel, where a packaged app serves ./dict/<key>.dawg from its own assets. Skipped on the web and
  // in the Mini Apps — there are no bundled dicts, and the relative path would otherwise hit the
  // gateway's own session-gated /dict/ route — so those go straight to the network tier. On a hit, seed
  // the persistent cache so later loads take tier 1. A bundled hit is a local asset, not a network
  // fetch, so it records no fetch metric.
  const channel = clientChannel();
  if (channel === 'android' || channel === 'ios') {
    try {
      const resp = await fetch(`./dict/${key}.dawg`);
      if (resp.ok) {
        const buf = await resp.arrayBuffer();
        const reader = new Dawg(new Uint8Array(buf));
        instances.set(key, reader);
        void idbPutDawg(key, buf);
        void requestPersist();
        return reader;
      }
    } catch {
      // A network-off fetch error or a decode failure — fall through to the network tier.
    }
  }

  // Tier 3: the session-gated download — gated by the bad-connection breaker. A caller's
  // signal aborts it (the warm-up giving up at its cap, or the game being left) so a large
  // download does not keep starving the channel on a slow link; the caller counts the miss.
  if (dictDisabled) return null;
  let buf: ArrayBuffer;
  const reload = bustNext;
  bustNext = false;
  try {
    buf = await gateway.fetchDict(variant, version, { signal, reload });
  } catch {
    return null; // network error or aborted
  }

  let reader: Dawg;
  try {
    reader = new Dawg(new Uint8Array(buf));
  } catch {
    return null; // not a dawg — do not cache it
  }
  instances.set(key, reader);
  noteDictFetched();
  void idbPutDawg(key, buf);
  void requestPersist();
  return reader;
}

/**
 * hasDawg reports whether the (variant, version) reader is already loaded in
 * memory (a warm hit), so the UI can decide instantly whether to preview locally
 * or show the warm-up state while the dictionary loads.
 */
export function hasDawg(variant: Variant, version: string): boolean {
  return instances.has(dictKey(variant, version));
}

/**
 * peekDawg returns the already-loaded reader for (variant, version), or null. It is
 * a synchronous in-memory lookup (no load), so the move preview can validate locally
 * without awaiting when the dictionary is already warm.
 */
export function peekDawg(variant: Variant, version: string): Dawg | null {
  return instances.get(dictKey(variant, version)) ?? null;
}

/**
 * clearDictInstances drops the in-memory readers (e.g. on logout). The persistent
 * IndexedDB cache is left intact — a dictionary version is immutable and reusable
 * by the next account on the device.
 */
export function clearDictInstances(): void {
  instances.clear();
  misses = 0;
  dictDisabled = false;
}
