/**
 * Dictionary provider registry.
 *
 * Returns the ordered list of {@link DictionaryProvider} instances visible
 * in the lookup popup, given the user's current `dictionarySettings` (order,
 * enable flags), the imported-dictionary metadata, and the filesystem
 * accessor used by import-backed providers (StarDict / MDict) to lazily open
 * their bundle files.
 *
 * Provider instances are cached at module scope (keyed by id) so subsequent
 * lookups within the same session reuse parsed indexes / object URLs. This
 * cache is **not** in zustand: provider instances and their object URLs are
 * runtime-only and non-serializable; storing them alongside metadata would
 * pollute the synced settings shape.
 */
import type {
  DictionaryProvider,
  DictionarySettings,
  ImportedDictionary,
  WebSearchEntry,
} from './types';
import { BUILTIN_PROVIDER_IDS } from './types';
import { isSystemDictionarySupported } from './systemDictionary';
import { wiktionaryProvider } from './providers/wiktionaryProvider';
import { wikipediaProvider } from './providers/wikipediaProvider';
import { createStarDictProvider, type DictionaryFileOpener } from './providers/starDictProvider';
import { createMdictProvider } from './providers/mdictProvider';
import { createDictProvider } from './providers/dictProvider';
import { createSlobProvider } from './providers/slobProvider';
import { createWebSearchProvider } from './providers/webSearchProvider';
import { getBuiltinWebSearch } from './webSearchTemplates';

const instanceCache = new Map<string, DictionaryProvider>();

interface RegistryArgs {
  settings: DictionarySettings;
  dictionaries: ImportedDictionary[];
  /**
   * Required when the provider order contains imported (stardict / mdict)
   * dictionaries — their providers open files via this accessor on first
   * lookup. Builtin-only callers (e.g. tests) may omit it.
   */
  fs?: DictionaryFileOpener;
}

const builtinFor = (id: string): DictionaryProvider | undefined => {
  if (id === BUILTIN_PROVIDER_IDS.wiktionary) return wiktionaryProvider;
  if (id === BUILTIN_PROVIDER_IDS.wikipedia) return wikipediaProvider;
  // System dictionary is a sentinel — it has no in-popup UI. The
  // annotator handles it before reaching the popup; the registry
  // filters it out of `getEnabledProviders` so no empty tab appears.
  return undefined;
};

/**
 * Resolve a `web:*` id to its template — built-in if id starts with
 * `web:builtin:`, else look it up in `settings.webSearches`.
 */
const findWebTemplate = (id: string, settings: DictionarySettings): WebSearchEntry | undefined => {
  if (id.startsWith('web:builtin:')) return getBuiltinWebSearch(id);
  const list = settings.webSearches ?? [];
  const tpl = list.find((t) => t.id === id);
  if (!tpl || tpl.deletedAt) return undefined;
  return tpl;
};

const getOrCreate = (
  id: string,
  dict: ImportedDictionary | undefined,
  fs: DictionaryFileOpener | undefined,
  settings: DictionarySettings,
): DictionaryProvider | undefined => {
  const cached = instanceCache.get(id);
  if (cached) return cached;
  const builtin = builtinFor(id);
  if (builtin) {
    instanceCache.set(id, builtin);
    return builtin;
  }
  if (id.startsWith('web:')) {
    const tpl = findWebTemplate(id, settings);
    if (!tpl) return undefined;
    const provider = createWebSearchProvider({ template: tpl });
    instanceCache.set(id, provider);
    return provider;
  }
  if (!dict) return undefined;
  if (!fs) return undefined;
  if (dict.kind === 'stardict') {
    const provider = createStarDictProvider({ dict, fs });
    instanceCache.set(id, provider);
    return provider;
  }
  if (dict.kind === 'mdict') {
    const provider = createMdictProvider({ dict, fs });
    instanceCache.set(id, provider);
    return provider;
  }
  if (dict.kind === 'dict') {
    const provider = createDictProvider({ dict, fs });
    instanceCache.set(id, provider);
    return provider;
  }
  if (dict.kind === 'slob') {
    const provider = createSlobProvider({ dict, fs });
    instanceCache.set(id, provider);
    return provider;
  }
  return undefined;
};

/**
 * Returns the ordered list of enabled providers ready for the popup.
 * - Filters out disabled ids.
 * - Filters out imported entries that are soft-deleted, unavailable on this
 *   device, or flagged unsupported.
 * - Filters out the system-dictionary sentinel (no in-popup UI; handled
 *   directly by the annotator before the popup opens).
 * - Preserves the order in `settings.providerOrder`.
 */
export const getEnabledProviders = ({
  settings,
  dictionaries,
  fs,
}: RegistryArgs): DictionaryProvider[] => {
  const dictById = new Map(dictionaries.map((d) => [d.id, d]));
  const out: DictionaryProvider[] = [];
  for (const id of settings.providerOrder) {
    if (settings.providerEnabled[id] === false) continue;
    // System-dictionary sentinel is intentionally skipped here — see
    // `isSystemDictionaryEnabled` for the dispatch path used by the
    // annotator's "Dictionary" button.
    if (id === BUILTIN_PROVIDER_IDS.systemDictionary) continue;
    if (id.startsWith('builtin:')) {
      const provider = getOrCreate(id, undefined, undefined, settings);
      if (provider) out.push(provider);
      continue;
    }
    if (id.startsWith('web:')) {
      const provider = getOrCreate(id, undefined, undefined, settings);
      if (provider) out.push(provider);
      continue;
    }
    const dict = dictById.get(id);
    if (!dict) continue;
    if (dict.deletedAt || dict.unavailable || dict.unsupported) continue;
    const provider = getOrCreate(id, dict, fs, settings);
    if (provider) out.push(provider);
  }
  return out;
};

/**
 * True when the user has enabled the system-dictionary entry in
 * settings. The annotator checks this before opening the in-app popup
 * — when set, the selection is handed to the OS instead. Independent
 * of `getEnabledProviders` (which filters the sentinel out so no empty
 * tab appears) so callers don't need to look at order/list shape to
 * make the dispatch decision.
 *
 * Gated by platform support: `providerEnabled` is whole-field synced
 * across devices, so a flag set on (say) macOS would otherwise reach a
 * Windows device that has no handoff implementation, leaving the
 * annotator with no way to look up a word. Short-circuiting here keeps
 * the setting durable on the device where it makes sense while making
 * it a no-op everywhere else.
 */
export const isSystemDictionaryEnabled = (settings: DictionarySettings): boolean => {
  if (!isSystemDictionarySupported()) return false;
  return settings.providerEnabled[BUILTIN_PROVIDER_IDS.systemDictionary] === true;
};

/** Drop a single provider from the cache (e.g. after dictionary deletion). */
export const evictProvider = (id: string): void => {
  const cached = instanceCache.get(id);
  cached?.dispose?.();
  instanceCache.delete(id);
};

/** Drop everything. Test helper. */
export const __resetRegistryForTests = (): void => {
  for (const [, provider] of instanceCache) provider.dispose?.();
  instanceCache.clear();
};
