/**
 * Favicon helpers.
 *
 * Order: Chrome → Google faviconV2 → site /favicon.ico → DuckDuckGo → icon.horse → error icon.
 * chrome:// / chrome-extension:// URLs use the Chrome browser icon.
 * Known private hosts (.lan / RFC1918) skip CDNs: Chrome → site icons → error icon.
 *
 * Important: Chrome / Google often respond HTTP 200 with a generic default globe when
 * they have no real icon. Those defaults are treated as failures (same as onerror)
 * so the cascade continues.
 *
 * Successful (non-default) icons are cached as data URLs in chrome.storage.local.
 */

/** Shown when no favicon can be resolved for a bookmark URL. */
export const FAVICON_PLACEHOLDER = '../images/favicon-error.svg';

/** Chrome browser icon for chrome:// (and related) internal URLs. */
export const CHROME_BROWSER_ICON = '../images/chrome-logo.svg';

export const FAVICON_CACHE_STORAGE_KEY = 'faviconDataCacheV5';

const FAVICON_LOAD_TIMEOUT_MS = 2500;
const FAVICON_CACHE_MAX_ENTRIES = 800;
const DEFAULT_ICON_PROBE = 'https://tabmark-favicon-probe.invalid/';
const LOCAL_HOST_SUFFIXES = [
  '.lan',
  '.local',
  '.internal',
  '.intranet',
  '.corp',
  '.home',
  '.localdomain',
  '.localhost'
];

/** @type {Map<string, string>} */
const memoryCache = new Map();
/** @type {Map<string, { dataUrl: string, updatedAt: number }>} */
const pendingCacheWrites = new Map();
let cacheWriteTimer = null;
let cacheHydratePromise = null;

/** @type {Map<string, Uint8Array | null>} */
const defaultIconBytesByKey = new Map();
/** @type {Map<string, Promise<Uint8Array | null>>} */
const defaultIconPromises = new Map();

export function chromeFaviconURL(pageUrl, size = 32) {
  const url = new URL(chrome.runtime.getURL('/_favicon/'));
  url.searchParams.set('pageUrl', pageUrl);
  url.searchParams.set('size', String(size));
  url.searchParams.set('cache', '1');
  return url.toString();
}

/** Google faviconV2 — works for sites like 1688 where s2/favicons often fails. */
export function googleFaviconV2URL(pageUrl, size = 32) {
  let targetUrl = pageUrl;
  try {
    targetUrl = new URL(pageUrl).origin;
  } catch {
    // keep original
  }
  const url = new URL('https://t0.gstatic.com/faviconV2');
  url.searchParams.set('client', 'SOCIAL');
  url.searchParams.set('type', 'FAVICON');
  url.searchParams.set('fallback_opts', 'TYPE,SIZE,URL');
  url.searchParams.set('url', targetUrl);
  url.searchParams.set('size', String(size));
  return url.toString();
}

function getHostname(pageUrl) {
  try {
    return new URL(pageUrl).hostname || '';
  } catch {
    return '';
  }
}

/** chrome:// / chrome-extension:// pages — use the Chrome browser icon. */
export function isChromeInternalUrl(pageUrl) {
  if (typeof pageUrl !== 'string' || !pageUrl) return false;
  try {
    const protocol = new URL(pageUrl).protocol;
    return protocol === 'chrome:' || protocol === 'chrome-extension:';
  } catch {
    return pageUrl.startsWith('chrome://') || pageUrl.startsWith('chrome-extension://');
  }
}

function getOrigin(pageUrl) {
  try {
    return new URL(pageUrl).origin;
  } catch {
    return '';
  }
}

/** Local / LAN / private hosts — external favicon CDNs cannot reach them. */
export function isLocalOrPrivateHost(hostname) {
  if (!hostname) return false;
  const host = String(hostname).toLowerCase().replace(/\.$/, '');

  if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0') {
    return true;
  }

  if (LOCAL_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix))) {
    return true;
  }

  const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
  if (ipv4) {
    const parts = ipv4.slice(1).map(Number);
    if (parts.some((n) => n > 255)) return false;
    const [a, b] = parts;
    if (a === 10 || a === 127) return true;
    if (a === 192 && b === 168) return true;
    if (a === 172 && b >= 16 && b <= 31) return true;
    if (a === 169 && b === 254) return true;
    return false;
  }

  if (!host.includes('.') && !host.includes(':')) {
    return true;
  }

  return false;
}

function getDirectSiteFaviconUrls(pageUrl) {
  const origin = getOrigin(pageUrl);
  if (!origin) return [];
  return [
    `${origin}/favicon.ico`,
    `${origin}/favicon.png`,
    `${origin}/apple-touch-icon.png`,
    `${origin}/apple-touch-icon-precomposed.png`
  ];
}

function bytesEqual(a, b) {
  if (!a || !b || a.byteLength !== b.byteLength) return false;
  for (let i = 0; i < a.byteLength; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

function getDefaultProbeUrl(sourceUrl, size) {
  if (sourceUrl.includes('/_favicon/')) {
    return { key: `chrome:${size}`, probeUrl: chromeFaviconURL(DEFAULT_ICON_PROBE, size) };
  }
  if (sourceUrl.includes('gstatic.com/faviconV2') || sourceUrl.includes('google.com/s2/favicons')) {
    return { key: `google:${size}`, probeUrl: googleFaviconV2URL(DEFAULT_ICON_PROBE, size) };
  }
  return null;
}

/** Sources known to return HTTP 200 + generic placeholder when they have no real icon. */
function canReturnDefaultIcon(sourceUrl) {
  return Boolean(getDefaultProbeUrl(sourceUrl, 32));
}

function getDefaultIconBytes(sourceUrl, size = 32) {
  const probe = getDefaultProbeUrl(sourceUrl, size);
  if (!probe) return Promise.resolve(null);

  if (defaultIconBytesByKey.has(probe.key)) {
    return Promise.resolve(defaultIconBytesByKey.get(probe.key));
  }
  if (defaultIconPromises.has(probe.key)) {
    return defaultIconPromises.get(probe.key);
  }

  const promise = fetch(probe.probeUrl)
    .then(async (response) => {
      if (!response.ok) {
        defaultIconBytesByKey.set(probe.key, null);
        return null;
      }
      const bytes = new Uint8Array(await response.arrayBuffer());
      defaultIconBytesByKey.set(probe.key, bytes);
      return bytes;
    })
    .catch(() => {
      defaultIconBytesByKey.set(probe.key, null);
      return null;
    })
    .finally(() => {
      defaultIconPromises.delete(probe.key);
    });

  defaultIconPromises.set(probe.key, promise);
  return promise;
}

/**
 * True when the CDN/Chrome response is their generic default icon (treat as failure).
 */
async function isDefaultFaviconResponse(sourceUrl, size = 32) {
  if (!canReturnDefaultIcon(sourceUrl)) return false;
  try {
    const [defaultBytes, response] = await Promise.all([
      getDefaultIconBytes(sourceUrl, size),
      fetch(sourceUrl)
    ]);
    if (!defaultBytes || !response.ok) return false;
    const bytes = new Uint8Array(await response.arrayBuffer());
    return bytesEqual(bytes, defaultBytes);
  } catch {
    return false;
  }
}

function hydrateFaviconCache() {
  if (!cacheHydratePromise) {
    cacheHydratePromise = chrome.storage.local
      .get(FAVICON_CACHE_STORAGE_KEY)
      .then((result) => {
        const cache = result[FAVICON_CACHE_STORAGE_KEY] || {};
        Object.entries(cache).forEach(([hostname, entry]) => {
          if (entry?.dataUrl && !memoryCache.has(hostname)) {
            memoryCache.set(hostname, entry.dataUrl);
          }
        });
      })
      .catch(() => {
        // ignore storage errors
      });
  }
  return cacheHydratePromise;
}

async function getCachedFaviconDataUrl(pageUrl) {
  const hostname = getHostname(pageUrl);
  if (!hostname) return null;

  if (memoryCache.has(hostname)) {
    return memoryCache.get(hostname);
  }

  await hydrateFaviconCache();
  return memoryCache.get(hostname) || null;
}

async function flushFaviconCacheWrites() {
  cacheWriteTimer = null;
  if (pendingCacheWrites.size === 0) return;

  const batch = new Map(pendingCacheWrites);
  pendingCacheWrites.clear();

  try {
    const result = await chrome.storage.local.get(FAVICON_CACHE_STORAGE_KEY);
    const cache = { ...(result[FAVICON_CACHE_STORAGE_KEY] || {}) };

    batch.forEach((entry, hostname) => {
      cache[hostname] = entry;
    });

    const keys = Object.keys(cache);
    if (keys.length > FAVICON_CACHE_MAX_ENTRIES) {
      keys
        .sort((a, b) => (cache[a].updatedAt || 0) - (cache[b].updatedAt || 0))
        .slice(0, keys.length - FAVICON_CACHE_MAX_ENTRIES)
        .forEach((key) => {
          delete cache[key];
          memoryCache.delete(key);
        });
    }

    await chrome.storage.local.set({ [FAVICON_CACHE_STORAGE_KEY]: cache });
  } catch (error) {
    console.warn('Failed to persist favicon cache:', error);
  }
}

function scheduleFaviconCacheWrite(pageUrl, dataUrl) {
  const hostname = getHostname(pageUrl);
  if (!hostname || !dataUrl || !String(dataUrl).startsWith('data:')) return;

  memoryCache.set(hostname, dataUrl);
  pendingCacheWrites.set(hostname, { dataUrl, updatedAt: Date.now() });

  if (cacheWriteTimer !== null) return;
  cacheWriteTimer = setTimeout(() => {
    flushFaviconCacheWrites();
  }, 400);
}

async function fetchAsDataUrl(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Favicon fetch failed: ${response.status}`);
  }
  const blob = await response.blob();
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
}

function isPlaceholderSource(url) {
  return typeof url === 'string' && (
    url.includes('favicon-error') ||
    url.includes('placeholder-icon')
  );
}

export function getFaviconSourceList(pageUrl, size = 32) {
  if (isChromeInternalUrl(pageUrl)) {
    return [CHROME_BROWSER_ICON];
  }

  const hostname = getHostname(pageUrl);
  if (!hostname) {
    return [FAVICON_PLACEHOLDER];
  }

  const directIcons = getDirectSiteFaviconUrls(pageUrl);

  if (isLocalOrPrivateHost(hostname)) {
    return [
      chromeFaviconURL(pageUrl, size),
      ...directIcons,
      FAVICON_PLACEHOLDER
    ];
  }

  return [
    // Chrome → Google → 站点自身 → DDG → icon.horse → 错误图标
    // Google/Chrome 默认图会当作失败，继续往后试
    chromeFaviconURL(pageUrl, size),
    googleFaviconV2URL(pageUrl, size),
    ...directIcons,
    `https://icons.duckduckgo.com/ip3/${hostname}.ico`,
    `https://icon.horse/icon/${hostname}`,
    FAVICON_PLACEHOLDER
  ];
}

/**
 * Assign favicon sources to an <img>, with local cache + fallback cascade.
 * @returns {() => void} cleanup that clears handlers/timers
 */
export function applyFaviconWithFallback(img, pageUrl, size = 32) {
  const sources = getFaviconSourceList(pageUrl, size);
  let step = 0;
  let timeoutId = null;
  let cancelled = false;
  let fromCache = false;

  const clearTimer = () => {
    if (timeoutId !== null) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
  };

  const cacheSuccessfulIcon = (sourceUrl) => {
    if (!sourceUrl || isPlaceholderSource(sourceUrl) || isChromeInternalUrl(pageUrl)) return;
    fetchAsDataUrl(sourceUrl)
      .then((dataUrl) => {
        if (!cancelled && dataUrl) {
          scheduleFaviconCacheWrite(pageUrl, dataUrl);
        }
      })
      .catch(() => {
        // ignore cache write failures
      });
  };

  const show = (index) => {
    if (cancelled) return;
    fromCache = false;
    clearTimer();
    step = index;
    if (step >= sources.length) {
      img.onerror = null;
      img.onload = null;
      return;
    }

    img.src = sources[step];

    if (step < sources.length - 1) {
      timeoutId = setTimeout(() => {
        show(step + 1);
      }, FAVICON_LOAD_TIMEOUT_MS);
    }
  };

  /** Network error OR CDN default placeholder — same handling. */
  const failCurrentSource = () => {
    if (fromCache) {
      invalidateCachedHostname();
      startCascade();
      return;
    }
    show(step + 1);
  };

  const startCascade = () => {
    // Warm default-icon probes used for "default == error" checks
    getDefaultIconBytes(chromeFaviconURL(DEFAULT_ICON_PROBE, size), size);
    getDefaultIconBytes(googleFaviconV2URL(DEFAULT_ICON_PROBE, size), size);
    show(0);
  };

  const invalidateCachedHostname = () => {
    const hostname = getHostname(pageUrl);
    if (!hostname) return;
    memoryCache.delete(hostname);
    pendingCacheWrites.delete(hostname);
    chrome.storage.local.get(FAVICON_CACHE_STORAGE_KEY).then((result) => {
      const cache = { ...(result[FAVICON_CACHE_STORAGE_KEY] || {}) };
      if (cache[hostname]) {
        delete cache[hostname];
        return chrome.storage.local.set({ [FAVICON_CACHE_STORAGE_KEY]: cache });
      }
      return undefined;
    }).catch(() => {});
  };

  img.onerror = () => {
    failCurrentSource();
  };

  img.onload = () => {
    clearTimer();
    if (fromCache) return;

    const currentStep = step;
    const currentSrc = sources[currentStep];
    const loadedSrc = img.src;

    if (!canReturnDefaultIcon(currentSrc)) {
      cacheSuccessfulIcon(currentSrc);
      return;
    }

    // CDN/Chrome 默认地球图：视为错误，继续回退
    isDefaultFaviconResponse(currentSrc, size).then((isDefault) => {
      if (cancelled) return;
      if (isDefault && img.src === loadedSrc) {
        failCurrentSource();
        return;
      }
      if (img.src === loadedSrc && currentStep === step) {
        cacheSuccessfulIcon(currentSrc);
      }
    });
  };

  // Internal chrome:// URLs always use the browser icon (skip stale cache).
  if (isChromeInternalUrl(pageUrl)) {
    startCascade();
  } else {
    getCachedFaviconDataUrl(pageUrl).then((cached) => {
      if (cancelled) return;
      if (cached) {
        fromCache = true;
        img.src = cached;
        return;
      }
      startCascade();
    });
  }

  return () => {
    cancelled = true;
    clearTimer();
    img.onerror = null;
    img.onload = null;
  };
}

/** Clear in-memory and persisted favicon caches. */
export async function clearFaviconCache() {
  memoryCache.clear();
  pendingCacheWrites.clear();
  if (cacheWriteTimer !== null) {
    clearTimeout(cacheWriteTimer);
    cacheWriteTimer = null;
  }
  cacheHydratePromise = null;

  try {
    await chrome.storage.local.remove(FAVICON_CACHE_STORAGE_KEY);
  } catch (error) {
    console.warn('Failed to clear favicon cache:', error);
    throw error;
  }
}

if (typeof chrome !== 'undefined' && chrome.storage?.onChanged) {
  chrome.storage.onChanged.addListener((changes, areaName) => {
    if (areaName !== 'local' || !(FAVICON_CACHE_STORAGE_KEY in changes)) return;
    if (!changes[FAVICON_CACHE_STORAGE_KEY].newValue) {
      memoryCache.clear();
      pendingCacheWrites.clear();
      cacheHydratePromise = null;
    }
  });
}
