import { SETTINGS_DEFAULTS, getSettings, subscribeToSettings } from '../shared/settings-store.js';

const DEFAULT_TTL_MS = Number.POSITIVE_INFINITY; // 默认永久保留

const ENTITY_TTL_MS = {
  podcast: Number.POSITIVE_INFINITY,
  episode: Number.POSITIVE_INFINITY,
};

const DEFAULT_ENTITY_MAX_ITEMS = {
  podcast: SETTINGS_DEFAULTS.podcastCacheLimit,
  episode: SETTINGS_DEFAULTS.episodeCacheLimit,
};

let storageQuotaBytes = SETTINGS_DEFAULTS.autoClearCacheThresholdMb * 1024 * 1024;
let isEnforcingQuota = false;

const ENTITY_STORAGE_KEYS = {
  podcast: 'podcastCache',
  episode: 'episodeCache',
};

const memoryCache = new Map();
const loadedEntities = new Set();
let entityMaxItems = { ...DEFAULT_ENTITY_MAX_ITEMS };

initializeEntityLimits();

/**
 * 从内存或持久化存储中读取缓存，如果存在且未过期则返回。
 */
export async function getCacheEntry({ entity, source, sourceRef }) {
  const normalized = normalizeKeyParts(entity, source, sourceRef);
  if (!normalized) {
    return null;
  }

  const { entityKey, cacheKey } = normalized;
  const entityCache = ensureEntityCache(entityKey);

  const memoryEntry = entityCache.get(cacheKey);
  if (memoryEntry) {
    if (!isExpired(memoryEntry)) {
      return memoryEntry;
    }
    entityCache.delete(cacheKey);
  }

  await ensureEntityLoadedFromStorage(entityKey);
  const persistedEntry = entityCache.get(cacheKey);
  if (!persistedEntry) {
    return null;
  }

  if (isExpired(persistedEntry)) {
    entityCache.delete(cacheKey);
    await persistEntityCache(entityKey);
    return null;
  }

  return persistedEntry;
}

/**
 * 写入缓存并同步到 chrome.storage.local。
 */
export async function setCacheEntry({ entity, source, sourceRef }, payload, options = {}) {
  const normalized = normalizeKeyParts(entity, source, sourceRef);
  if (!normalized) {
    return null;
  }

  const { entityKey, cacheKey } = normalized;
  const ttlMs = resolveTtl(entityKey, options.ttlMs);
  const now = Date.now();
  const expiresAt = Number.isFinite(ttlMs) ? now + ttlMs : null;

  const entry = {
    data: minimizePayload(entityKey, payload?.data),
    meta: payload?.meta || {},
    cachedAt: payload?.cachedAt || new Date(now).toISOString(),
    expiresAt,
  };

  const entityCache = ensureEntityCache(entityKey);
  const sanitizedEntry = sanitizeCacheEntry(entry);
  entityCache.set(cacheKey, sanitizedEntry);
  await persistEntityCache(entityKey);
  return sanitizedEntry;
}

export async function setCacheEntriesBatch(entity, entries, { ttlMs, source } = {}) {
  const entityKey = normalizeEntity(entity);
  if (!entityKey || !Array.isArray(entries) || entries.length === 0) {
    return;
  }

  await ensureEntityLoadedFromStorage(entityKey);
  const entityCache = ensureEntityCache(entityKey);
  const resolvedTtl = resolveTtl(entityKey, ttlMs);
  const now = Date.now();

  entries.forEach(entryPayload => {
    if (!entryPayload || typeof entryPayload !== 'object') {
      return;
    }
    const { source, sourceRef, data, meta, cachedAt } = entryPayload;
    const normalized = normalizeKeyParts(entity, source, sourceRef);
    if (!normalized) {
      return;
    }
    const expiresAt = Number.isFinite(resolvedTtl) ? now + resolvedTtl : null;
    const entry = {
      data: minimizePayload(entityKey, data),
      meta: meta || {},
      cachedAt: cachedAt || new Date(now).toISOString(),
      expiresAt,
    };
    entityCache.set(normalized.cacheKey, sanitizeCacheEntry(entry));
  });

  await persistEntityCache(entityKey);

  // 更新 fetchStatus：成功写入缓存时清除失败记录
  if (entityKey === 'podcast') {
    try {
      const stored = await chrome.storage.local.get({ fetchStatus: {} });
      const fetchStatus = stored.fetchStatus || {};
      let updated = false;
      entries.forEach(entryPayload => {
        const { source: entrySource, sourceRef } = entryPayload;
        const normalized = normalizeKeyParts(entity, entrySource, sourceRef);
        if (normalized) {
          fetchStatus[normalized.cacheKey] = {
            lastSuccessAt: now,
            lastErrorAt: null,
            lastError: null,
          };
          updated = true;
        }
      });
      if (updated) {
        await chrome.storage.local.set({ fetchStatus });
      }
    } catch (e) {
      console.warn('[cache-store] failed to update fetchStatus', e);
    }
  }

  try {
    console.log('[cache-store] setCacheEntriesBatch entity=', entityKey, 'size=', entries.length, 'memorySize=', ensureEntityCache(entityKey).size);
    try {
      chrome.runtime.sendMessage({
        type: 'xyz.cache.entityUpdated',
        entity: entityKey,
        size: ensureEntityCache(entityKey).size,
        updated: Date.now(),
        source: source || 'background',
      });
    } catch (_) { /* ignore */ }
  } catch (e) {}
}

/**
 * 删除缓存项。
 */
export async function deleteCacheEntry({ entity, source, sourceRef }) {
  const normalized = normalizeKeyParts(entity, source, sourceRef);
  if (!normalized) {
    return;
  }
  const { entityKey, cacheKey } = normalized;
  const entityCache = ensureEntityCache(entityKey);
  if (entityCache.has(cacheKey)) {
    entityCache.delete(cacheKey);
    await persistEntityCache(entityKey);
  }
}

/**
 * 清空指定实体下的所有缓存。
 */
export async function clearEntityCache(entity) {
  const entityKey = normalizeEntity(entity);
  if (!entityKey) {
    return;
  }
  memoryCache.delete(entityKey);
  loadedEntities.delete(entityKey);
  const storageKey = ENTITY_STORAGE_KEYS[entityKey];
  if (!storageKey) {
    return;
  }
  await chrome.storage.local.remove(storageKey);
}

function resolveTtl(entity, overrideTtl) {
  if (typeof overrideTtl === 'number' && overrideTtl > 0) {
    return overrideTtl;
  }
  return ENTITY_TTL_MS[entity] ?? DEFAULT_TTL_MS;
}

export function resolveCacheTtlMs(entity, overrideTtl) {
  const entityKey = normalizeEntity(entity);
  return resolveTtl(entityKey, overrideTtl);
}

async function ensureEntityLoadedFromStorage(entity) {
  if (loadedEntities.has(entity)) {
    return;
  }
  const storageKey = ENTITY_STORAGE_KEYS[entity];
  if (!storageKey) {
    loadedEntities.add(entity);
    return;
  }

  const entityCache = ensureEntityCache(entity);
  try {
    const stored = await chrome.storage.local.get(storageKey);
    const payload = stored?.[storageKey];
    if (payload && typeof payload === 'object') {
      Object.entries(payload).forEach(([cacheKey, entry]) => {
        if (entry && typeof entry === 'object') {
          entityCache.set(cacheKey, sanitizeCacheEntry(entry));
        }
      });
      await persistEntityCache(entity);
    }
  } catch (error) {
    console.error('[核桃FM] Failed to load cache from storage:', error);
  } finally {
    loadedEntities.add(entity);
  }
}

async function persistEntityCache(entity) {
  const storageKey = ENTITY_STORAGE_KEYS[entity];
  if (!storageKey) {
    return;
  }
  const entityCache = ensureEntityCache(entity);
  pruneEntityCache(entity, entityCache);
  const serialized = {};
  entityCache.forEach((entry, key) => {
    if (!isExpired(entry)) {
      serialized[key] = deepMinimizeEntry(entity, entry);
    }
  });
  try {
    await chrome.storage.local.set({ [storageKey]: serialized });
  } catch (error) {
    if (isQuotaError(error)) {
      console.warn('[核桃FM] Cache quota exceeded, pruning and retrying…');
      pruneEntityCache(entity, entityCache, { aggressive: true });
      const retried = {};
      entityCache.forEach((entry, key) => {
        if (!isExpired(entry)) {
          retried[key] = deepMinimizeEntry(entity, entry);
        }
      });
      try {
        await chrome.storage.local.set({ [storageKey]: retried });
        await maybeEnforceStorageQuota();
        return;
      } catch (retryErr) {
        console.error('[核桃FM] Failed to persist cache after pruning:', retryErr);
      }
    }
    console.error('[核桃FM] Failed to persist cache:', error);
    await maybeEnforceStorageQuota();
    return;
  }
  await maybeEnforceStorageQuota();
}

function ensureEntityCache(entity) {
  const existing = memoryCache.get(entity);
  if (existing) {
    return existing;
  }
  const map = new Map();
  memoryCache.set(entity, map);
  return map;
}

function normalizeKeyParts(entity, source, sourceRef) {
  const entityKey = normalizeEntity(entity);
  const normalizedSource = ensureString(source) || 'xiaoyuzhou';
  const normalizedSourceRef = ensureString(sourceRef);
  if (!entityKey || !normalizedSourceRef) {
    return null;
  }
  return {
    entityKey,
    cacheKey: `${normalizedSource}:${normalizedSourceRef}`,
  };
}

function normalizeEntity(entity) {
  const value = ensureString(entity).toLowerCase();
  if (!value) {
    return '';
  }
  return value;
}

function ensureString(value) {
  if (typeof value === 'string') {
    return value.trim();
  }
  if (typeof value === 'number') {
    return String(value);
  }
  return '';
}

function isExpired(entry) {
  if (!entry || typeof entry !== 'object') {
    return true;
  }
  const expiresAt = entry.expiresAt;
  if (expiresAt == null) {
    return false;
  }
  const timestamp = Number(expiresAt);
  if (!Number.isFinite(timestamp)) {
    return false;
  }
  return timestamp <= Date.now();
}

function pruneEntityCache(entity, entityCache, { aggressive = false } = {}) {
  if (!entityCache || typeof entityCache.size !== 'number') return;
  // Remove expired entries first
  for (const [key, entry] of entityCache.entries()) {
    if (isExpired(entry)) {
      entityCache.delete(key);
    }
  }

  const baseLimit = resolveEntityLimit(entity);
  if (!baseLimit || baseLimit <= 0) return;
  const limit = aggressive ? Math.max(10, Math.floor(baseLimit * 0.6)) : baseLimit;
  if (entityCache.size <= limit) return;

  const entries = Array.from(entityCache.entries()).sort((a, b) => {
    const aTime = Number(new Date(a[1]?.cachedAt || 0));
    const bTime = Number(new Date(b[1]?.cachedAt || 0));
    if (!Number.isFinite(aTime) && !Number.isFinite(bTime)) return 0;
    if (!Number.isFinite(aTime)) return -1;
    if (!Number.isFinite(bTime)) return 1;
    return aTime - bTime;
  });
  const overflow = Math.max(0, entries.length - limit);
  for (let i = 0; i < overflow; i += 1) {
    entityCache.delete(entries[i][0]);
  }
}

function isQuotaError(error) {
  if (!error) return false;
  const message = String(error?.message || error || '').toLowerCase();
  return message.includes('quota') || message.includes('kquotabytes');
}

function sanitizeCacheEntry(entry) {
  if (!entry || typeof entry !== 'object') return entry;
  const clone = { ...entry };
  if (clone.raw) {
    delete clone.raw;
  }
  if (clone.data && typeof clone.data === 'object') {
    if (clone.data.raw) {
      delete clone.data.raw;
    }
  }
  if (clone.meta && typeof clone.meta === 'object' && clone.meta.raw) {
    delete clone.meta.raw;
  }
  return clone;
}

// Reduce payload size for storage: limit episodes per podcast and drop heavy fields
function minimizePayload(entity, data) {
  try {
    if (!data || typeof data !== 'object') return data;
    const isPodcast = String(entity).toLowerCase() === 'podcast';
    if (!isPodcast) return data;
    const d = { ...(data || {}) };
    const episodes = Array.isArray(d.episodes) ? d.episodes.slice(0, 25) : [];
    d.episodes = episodes.map(ep => {
      const e = { ...(ep || {}) };
      // Keep lean fields only
      const lean = {
        id: e.id,
        title: e.title || e.episodeTitle,
        episodeTitle: e.episodeTitle || e.title,
        audioUrl: e.audioUrl,
        backupAudioUrls: e.backupAudioUrls,
        publishedAt: e.publishedAt || e.publishAt || null,
        duration: e.duration,
        cover: e.cover || e.image || '',
        link: e.link || e.episodeLink || '',
        episodeLink: e.episodeLink || e.link || '',
      };
      return lean;
    });
    // Trim podcast fields
    if (d.podcast && typeof d.podcast === 'object') {
      const p = d.podcast;
      d.podcast = {
        title: p.title,
        cover: p.cover || p.image || '',
        link: p.link || p.shareUrl || p.url || '',
      };
    }
    return d;
  } catch (_) {
    return data;
  }
}

function deepMinimizeEntry(entity, entry) {
  const e = { ...(entry || {}) };
  e.data = minimizePayload(entity, e.data);
  return sanitizeCacheEntry(e);
}

function initializeEntityLimits() {
  getSettings()
    .then(applySettingsToEntityLimits)
    .catch(error => {
      console.warn('[核桃FM] Failed to load settings for cache limits:', error);
      entityMaxItems = { ...DEFAULT_ENTITY_MAX_ITEMS };
      storageQuotaBytes = SETTINGS_DEFAULTS.autoClearCacheThresholdMb * 1024 * 1024;
    });
  subscribeToSettings(applySettingsToEntityLimits);
}

function applySettingsToEntityLimits(settings) {
  if (!settings || typeof settings !== 'object') {
    entityMaxItems = { ...DEFAULT_ENTITY_MAX_ITEMS };
    storageQuotaBytes = SETTINGS_DEFAULTS.autoClearCacheThresholdMb * 1024 * 1024;
    return;
  }
  const next = {
    podcast: normalizeLimit(settings.podcastCacheLimit, DEFAULT_ENTITY_MAX_ITEMS.podcast),
    episode: normalizeLimit(settings.episodeCacheLimit, DEFAULT_ENTITY_MAX_ITEMS.episode),
  };
  entityMaxItems = next;
  const thresholdMb = Number(settings.autoClearCacheThresholdMb);
  storageQuotaBytes =
    Number.isFinite(thresholdMb) && thresholdMb > 0 ? Math.round(thresholdMb) * 1024 * 1024 : 0;
  if (!isEnforcingQuota) {
    maybeEnforceStorageQuota().catch(error => {
      console.warn('[核桃FM] Failed to enforce quota after settings update:', error);
    });
  }
}

function normalizeLimit(value, fallback) {
  const num = Number(value);
  if (!Number.isFinite(num) || num <= 0) {
    return fallback;
  }
  return Math.round(num);
}

function resolveEntityLimit(entity) {
  return entityMaxItems[entity] ?? DEFAULT_ENTITY_MAX_ITEMS[entity] ?? 0;
}

async function maybeEnforceStorageQuota() {
  if (isEnforcingQuota) {
    return;
  }
  if (!storageQuotaBytes || storageQuotaBytes <= 0) {
    return;
  }
  isEnforcingQuota = true;
  try {
    let usage = await getBytesInUse();
    if (!Number.isFinite(usage) || usage <= storageQuotaBytes) {
      return;
    }
    await shrinkCachesForQuota();
    usage = await getBytesInUse();
    if (Number.isFinite(usage) && usage > storageQuotaBytes) {
      console.warn('[核桃FM] Storage usage remains above threshold after cleanup:', usage, 'bytes');
    }
  } catch (error) {
    console.warn('[核桃FM] Failed to enforce cache quota:', error);
  } finally {
    isEnforcingQuota = false;
  }
}

async function shrinkCachesForQuota() {
  await ensureEntityLoadedFromStorage('podcast');
  await ensureEntityLoadedFromStorage('episode');

  const entries = collectCacheEntries();
  if (!entries.length) {
    return;
  }

  entries.sort((a, b) => a.cachedAt - b.cachedAt);

  for (const entry of entries) {
    const entityCache = ensureEntityCache(entry.entity);
    if (!entityCache.delete(entry.cacheKey)) {
      continue;
    }
    await persistEntityCache(entry.entity);
    const usage = await getBytesInUse();
    if (Number.isFinite(usage) && usage <= storageQuotaBytes) {
      return;
    }
  }
}

function collectCacheEntries() {
  const entries = [];
  for (const [entity, cache] of memoryCache.entries()) {
    if (entity !== 'podcast' && entity !== 'episode') {
      continue;
    }
    for (const [cacheKey, entry] of cache.entries()) {
      const cachedAt = Number(new Date(entry?.cachedAt || entry?.meta?.cachedAt || 0));
      entries.push({ entity, cacheKey, cachedAt: Number.isFinite(cachedAt) ? cachedAt : 0 });
    }
  }
  return entries;
}

function getBytesInUse() {
  return new Promise((resolve, reject) => {
    chrome.storage.local.getBytesInUse(null, bytes => {
      const lastError = chrome.runtime.lastError;
      if (lastError) {
        reject(new Error(lastError.message || 'Failed to read storage usage'));
        return;
      }
      resolve(bytes);
    });
  });
}
