import { fetchPodcastById, SOURCE_XIAOYUZHOU } from './fetch-client.js';
import { fetchRssByUrl } from './rss-fetcher.js';
import { setCacheEntriesBatch, getCacheEntry } from '../background/cache-store.js';

const DEFAULT_PER_PODCAST = 5;
const DEFAULT_MAX_ITEMS = Number.POSITIVE_INFINITY;
const DEFAULT_FORCE_BATCH_SIZE = 5;      // 强刷时每批抓取的播客数量
const DEFAULT_FORCE_BATCH_DELAY_MS = 1200; // 批次间隔，避免触发限频
const MIN_RSS_REFRESH_INTERVAL_MS = 30 * 60 * 1000; // RSS 最低刷新间隔 30 分钟
const MIN_XYZ_REFRESH_INTERVAL_MS = 60 * 60 * 1000; // 小宇宙最低刷新间隔 1 小时

export async function aggregateEpisodesForSubscriptions(subscriptions, options = {}) {
  const list = Array.isArray(subscriptions) ? subscriptions.slice() : [];
  const xyRefs = list
    .filter(s => (s && typeof s === 'object' ? (ensureString(s.source) || 'xiaoyuzhou') !== 'rss' : true))
    .map(s => (s && typeof s === 'object' ? ensureString(s.sourceRef) : ensureString(s)))
    .filter(Boolean);
  const rssRefs = list
    .filter(s => (s && typeof s === 'object' ? (ensureString(s.source) || 'xiaoyuzhou') === 'rss' : false))
    .map(s => ensureString(s.sourceRef))
    .filter(Boolean);

  console.log('[aggregator]', new Date().toISOString(), 'aggregateEpisodesForSubscriptions xyRefs=', xyRefs.length, 'rssRefs=', rssRefs.length, 'deferStore=', options.deferStore, 'source=', options.source);

  const [xyItems, rssItems] = await Promise.all([
    aggregateEpisodesForSourceRefs(xyRefs, options),
    aggregateEpisodesForRssUrls(rssRefs, options),
  ]);
  const merged = [...xyItems, ...rssItems];
  merged.sort((a, b) => {
    const aTime = a.publishedAt ? Date.parse(a.publishedAt) : 0;
    const bTime = b.publishedAt ? Date.parse(b.publishedAt) : 0;
    return bTime - aTime;
  });
  if (Number.isFinite(options.maxItems)) {
    return merged.slice(0, Math.max(1, Number(options.maxItems)));
  }
  return merged;
}

async function aggregateEpisodesForRssUrls(urls, options = {}) {
  const refs = Array.isArray(urls) ? urls.map(ensureString).filter(Boolean) : [];
  if (!refs.length) return [];
  const concurrency = normalizeConcurrency(options);
  const perPodcast = Number.isFinite(options.limitPerPodcast) ? Math.max(1, Number(options.limitPerPodcast)) : DEFAULT_PER_PODCAST;
  const force = Boolean(options.force);
  const deferStore = Boolean(options.deferStore);
  console.log('[aggregator] RSS START refs=', refs.length, 'force=', force, 'deferStore=', deferStore, 'source=', options.source);

  const results = [];
  if (force) {
    const batchSize = Number.isFinite(options.forceBatchSize) ? Math.max(1, Number(options.forceBatchSize)) : DEFAULT_FORCE_BATCH_SIZE;
    const delayMs = Number.isFinite(options.forceBatchDelayMs) ? Math.max(0, Number(options.forceBatchDelayMs)) : DEFAULT_FORCE_BATCH_DELAY_MS;
    for (let i = 0; i < refs.length; i += batchSize) {
      const batch = refs.slice(i, i + batchSize);
      const tasks = batch.map(ref => () => fetchOneRss(ref, { force: true, perPodcast, deferStore }));
      const batchResults = await runWithConcurrency(tasks, concurrency);
      results.push(...batchResults);
      if (i + batchSize < refs.length && delayMs > 0) await sleep(delayMs);
    }
  } else {
    const tasks = refs.map(ref => () => fetchOneRss(ref, { force: false, perPodcast, deferStore }));
    const batchResults = await runWithConcurrency(tasks, concurrency);
    results.push(...batchResults);
  }

  const episodes = [];
  const seen = new Set();
  const batchEntries = [];
  results.forEach(r => {
    if (r.status !== 'fulfilled') return;
    const { podcast, episodes: eps, rawData, meta, fromCache } = r.value || {};
    (eps || []).forEach((episode, index) => {
      const id = ensureString(episode.id || `${podcast.sourceRef}:${index}`);
      if (!id || seen.has(id)) return;
      seen.add(id);
      episodes.push(episode);
    });
    // 只写入新抓取的数据，跳过缓存数据
    if (deferStore && podcast?.sourceRef && rawData && !fromCache) {
      batchEntries.push({ source: 'rss', sourceRef: podcast.sourceRef, data: rawData, meta: meta || {}, cachedAt: Date.now() });
    }
  });

  if (deferStore && batchEntries.length) {
    try {
      console.log('[aggregator] RSS batch write count=', batchEntries.length, 'source=', options.source || 'background');
      await setCacheEntriesBatch('podcast', batchEntries, { ttlMs: options.ttlMs, source: options.source || 'background' });
    } catch (e) {
      console.warn('[episodes-aggregator] batch cache update for rss failed', e);
    }
  }

  episodes.sort((a, b) => {
    const aTime = a.publishedAt ? Date.parse(a.publishedAt) : 0;
    const bTime = b.publishedAt ? Date.parse(b.publishedAt) : 0;
    return bTime - aTime;
  });
  if (Number.isFinite(options.maxItems)) {
    return episodes.slice(0, Math.max(1, Number(options.maxItems)));
  }
  return episodes;
}

export async function aggregateEpisodesForSourceRefs(sourceRefs, options = {}) {
  const refs = Array.isArray(sourceRefs) ? sourceRefs.map(ensureString).filter(Boolean) : [];
  if (!refs.length) return [];

  const concurrency = normalizeConcurrency(options);
  const perPodcast = Number.isFinite(options.limitPerPodcast) ? Math.max(1, Number(options.limitPerPodcast)) : DEFAULT_PER_PODCAST;
  const maxItems = Number.isFinite(options.maxItems) ? Math.max(1, Number(options.maxItems)) : DEFAULT_MAX_ITEMS;
  const force = Boolean(options.force);
  const deferStore = Boolean(options.deferStore);
  console.log('[aggregator] START refs=', refs.length, 'force=', force, 'deferStore=', deferStore, 'source=', options.source);

  // 分批强刷：当 force=true 时启用批处理，底层按批次 + 并发抓取，批次之间延时
  const results = [];
  if (force) {
    const batchSize = Number.isFinite(options.forceBatchSize)
      ? Math.max(1, Number(options.forceBatchSize))
      : DEFAULT_FORCE_BATCH_SIZE;
    const delayMs = Number.isFinite(options.forceBatchDelayMs)
      ? Math.max(0, Number(options.forceBatchDelayMs))
      : DEFAULT_FORCE_BATCH_DELAY_MS;

    for (let i = 0; i < refs.length; i += batchSize) {
      const batch = refs.slice(i, i + batchSize);
      const tasks = batch.map(ref => () => fetchOnePodcast(ref, { force: true, perPodcast, deferStore }));
      const batchResults = await runWithConcurrency(tasks, concurrency);
      results.push(...batchResults);
      if (i + batchSize < refs.length && delayMs > 0) {
        await sleep(delayMs);
      }
    }
  } else {
    const tasks = refs.map(ref => () => fetchOnePodcast(ref, { force: false, perPodcast, deferStore }));
    const batchResults = await runWithConcurrency(tasks, concurrency);
    results.push(...batchResults);
  }
  console.log('[aggregator] fetched podcasts=', results.length, 'deferStore=', deferStore);

  const items = [];
  const seen = new Set();
  const batchEntries = [];
  results.forEach(r => {
    if (r.status !== 'fulfilled') return;
    const { podcast, episodes, rawData, meta, fromCache } = r.value || {};
    (episodes || []).forEach((episode, index) => {
      const id = ensureString(episode.id || `${podcast.sourceRef}:${index}`);
      if (!id || seen.has(id)) return;
      seen.add(id);
      items.push(episode);
    });

    // 只写入新抓取的数据，跳过缓存数据
    if (deferStore && podcast?.sourceRef && rawData && !fromCache) {
      batchEntries.push({
        source: SOURCE_XIAOYUZHOU,
        sourceRef: podcast.sourceRef,
        data: rawData,
        meta: meta || {},
        cachedAt: Date.now(),
      });
    }
  });

  items.sort((a, b) => {
    const aTime = a.publishedAt ? Date.parse(a.publishedAt) : 0;
    const bTime = b.publishedAt ? Date.parse(b.publishedAt) : 0;
    return bTime - aTime;
  });

  const aggregated = items.slice(0, maxItems);

  if (deferStore && batchEntries.length) {
    try {
      console.log('[aggregator] batch write count=', batchEntries.length, 'source=', options.source || 'background', 'caller=', new Error().stack.split('\n')[2]);
      await setCacheEntriesBatch('podcast', batchEntries, {
        ttlMs: options.ttlMs,
        source: options.source || 'background',
      });
    } catch (error) {
      console.warn('[episodes-aggregator] batch cache update failed', error);
    }
  }

  return aggregated;
}

async function fetchOnePodcast(sourceRef, { force = false, perPodcast = DEFAULT_PER_PODCAST, deferStore = false } = {}) {
  // 检查缓存，如果未过最低刷新间隔则使用缓存
  const cached = await getCacheEntry({ entity: 'podcast', source: SOURCE_XIAOYUZHOU, sourceRef });
  const now = Date.now();
  const cacheAge = cached?.cachedAt ? now - cached.cachedAt : Infinity;

  if (!force && cached && cacheAge < MIN_XYZ_REFRESH_INTERVAL_MS) {
    console.log('[aggregator] fetchOnePodcast SKIP ref=', sourceRef, 'cacheAge=', Math.round(cacheAge / 60000), 'min');
    const podcast = cached.data?.podcast ?? {};
    const episodes = Array.isArray(cached.data?.episodes) ? cached.data.episodes.slice(0, perPodcast) : [];
    const result = buildXyzResult(sourceRef, podcast, episodes, cached.data, cached.meta);
    result.fromCache = true; // 标记为缓存数据
    return result;
  }

  console.log('[aggregator] fetchOnePodcast FETCH ref=', sourceRef, 'force=', force, 'deferStore=', deferStore, 'cacheAge=', cached ? Math.round(cacheAge / 60000) + 'min' : 'none');
  const cacheOptions = deferStore
    ? { cache: { bypass: false, deferStore: true } }
    : force
    ? { cache: { forceRefresh: true } }
    : {};
  const { data, meta } = await fetchPodcastById(sourceRef, cacheOptions);
  const podcast = data?.podcast ?? {};
  const episodes = Array.isArray(data?.episodes) ? data.episodes.slice(0, perPodcast) : [];
  return buildXyzResult(sourceRef, podcast, episodes, data, meta);
}

function buildXyzResult(sourceRef, podcast, episodes, rawData, meta) {
  const base = {
    sourceRef,
    title: ensureString(podcast.title),
    cover: ensureString(podcast.cover || podcast.image || ''),
    link: ensureString(podcast.link || podcast.shareUrl || podcast.url || ''),
  };

  const mapped = episodes.map((episode, index) => {
    const sources = resolveEpisodeAudioSources(episode);
    return {
      id: ensureString(episode.id || `${sourceRef}:${index}`),
      episodeTitle: ensureString(episode.title || episode.name || ''),
      episodeDescription: ensureString(episode.description || episode.brief || ''),
      audioUrl: sources.audioUrl,
      backupAudioUrls: sources.backupAudioUrls,
      publishedAt: episode.publishedAt || episode.publishAt || episode.updatedAt || episode.createdAt || null,
      duration: toNumberOrUndefined(
        episode.duration || episode.durationSec || episode.durationSeconds || episode.audio?.duration
      ),
      cover: base.cover,
      episodeLink: ensureString(
        episode.link ||
          episode.url ||
          episode.shareUrl ||
          (episode.sourceRef ? `https://www.xiaoyuzhoufm.com/episode/${episode.sourceRef}` : '')
      ),
      podcastTitle: ensureString(base.title || base.sourceRef),
      podcastSourceRef: base.sourceRef,
      podcastLink: ensureString(base.link) || `https://www.xiaoyuzhoufm.com/podcast/${base.sourceRef}`,
      source: 'xiaoyuzhou',
    };
  });

  return { podcast: base, episodes: mapped, rawData, meta };
}

async function fetchOneRss(sourceRef, { force = false, perPodcast = DEFAULT_PER_PODCAST, deferStore = false } = {}) {
  // 检查缓存，如果未过最低刷新间隔则使用缓存
  const cached = await getCacheEntry({ entity: 'podcast', source: 'rss', sourceRef });
  const now = Date.now();
  const cacheAge = cached?.cachedAt ? now - cached.cachedAt : Infinity;

  if (!force && cached && cacheAge < MIN_RSS_REFRESH_INTERVAL_MS) {
    console.log('[aggregator] fetchOneRss SKIP url=', sourceRef, 'cacheAge=', Math.round(cacheAge / 60000), 'min');
    const podcast = cached.data?.podcast ?? {};
    const episodes = Array.isArray(cached.data?.episodes) ? cached.data.episodes.slice(0, perPodcast) : [];
    const result = buildRssResult(sourceRef, podcast, episodes, cached.data, cached.meta);
    result.fromCache = true; // 标记为缓存数据
    return result;
  }

  console.log('[aggregator] fetchOneRss FETCH url=', sourceRef, 'force=', force, 'cacheAge=', cached ? Math.round(cacheAge / 60000) + 'min' : 'none');
  const { data, meta } = await fetchRssByUrl(sourceRef);
  const podcast = data?.podcast ?? {};
  const episodes = Array.isArray(data?.episodes) ? data.episodes.slice(0, perPodcast) : [];
  return buildRssResult(sourceRef, podcast, episodes, data, meta);
}

function buildRssResult(sourceRef, podcast, episodes, rawData, meta) {
  const base = {
    sourceRef,
    title: ensureString(podcast.title),
    cover: ensureString(podcast.cover || ''),
    link: ensureString(podcast.link || ''),
  };

  const mapped = episodes.map((episode, index) => ({
    ...episode,
    id: ensureString(episode.id || `${sourceRef}:${index}`),
    podcastTitle: ensureString(base.title || base.sourceRef),
    podcastSourceRef: base.sourceRef,
    podcastLink: ensureString(base.link || ''),
    cover: ensureString(base.cover || ''),
    source: 'rss',
  }));

  return { podcast: base, episodes: mapped, rawData, meta };
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, Number(ms) || 0));
}

async function runWithConcurrency(taskFactories, limit = 3) {
  const results = new Array(taskFactories.length);
  const workers = [];
  let index = 0;

  const worker = async () => {
    while (index < taskFactories.length) {
      const current = index++;
      const fn = taskFactories[current];
      try {
        const value = await fn();
        results[current] = { status: 'fulfilled', value };
      } catch (reason) {
        results[current] = { status: 'rejected', reason };
      }
    }
  };

  const count = Math.min(Math.max(1, limit), taskFactories.length || 1);
  for (let i = 0; i < count; i++) {
    workers.push(worker());
  }
  await Promise.allSettled(workers);
  return results;
}

function normalizeConcurrency(options) {
  if (!options) return 3;
  if (options.sequential === true) return 1;
  const c = Number(options.concurrency);
  return Number.isFinite(c) && c > 0 ? c : 3;
}

export function resolveEpisodeAudioSources(rawEpisode) {
  const seen = new Set();
  let primary = '';
  const backups = [];

  const primaryCandidates = [
    rawEpisode?.audioUrl,
    rawEpisode?.audio?.url,
    rawEpisode?.audio?.src,
    rawEpisode?.enclosure?.url,
    rawEpisode?.enclosures?.[0]?.url,
    rawEpisode?.media?.source?.url,
    rawEpisode?.media?.url,
  ];

  primaryCandidates.some(candidate => {
    const url = normalizeHttpUrl(candidate);
    if (url && !primary) {
      primary = url;
      seen.add(url);
      return true;
    }
    return false;
  });

  const appendBackupCandidate = value => {
    const url = normalizeHttpUrl(value);
    if (!url || seen.has(url)) {
      return;
    }
    backups.push(url);
    seen.add(url);
  };

  const media = rawEpisode?.media || {};
  if (media.backupSource) {
    appendBackupCandidate(media.backupSource.url || media.backupSource.src || media.backupSource);
  }
  if (Array.isArray(media.backupSources)) {
    media.backupSources.forEach(item => {
      if (!item) return;
      if (typeof item === 'string') {
        appendBackupCandidate(item);
      } else {
        appendBackupCandidate(item.url || item.src);
      }
    });
  }
  if (Array.isArray(media.sources)) {
    media.sources.forEach(item => {
      if (!item) return;
      if (typeof item === 'string') {
        appendBackupCandidate(item);
      } else {
        appendBackupCandidate(item.url || item.src);
      }
    });
  }
  if (media.backupUrl) appendBackupCandidate(media.backupUrl);
  if (media.fallbackUrl) appendBackupCandidate(media.fallbackUrl);

  const additionalCandidates = [
    rawEpisode?.backupAudioUrl,
    rawEpisode?.alternativeAudioUrl,
    rawEpisode?.fallbackAudioUrl,
  ];
  additionalCandidates.forEach(appendBackupCandidate);

  if (Array.isArray(rawEpisode?.backupAudioUrls)) {
    rawEpisode.backupAudioUrls.forEach(appendBackupCandidate);
  }
  if (Array.isArray(rawEpisode?.audioAlternatives)) {
    rawEpisode.audioAlternatives.forEach(appendBackupCandidate);
  }
  if (Array.isArray(rawEpisode?.alternateAudioUrls)) {
    rawEpisode.alternateAudioUrls.forEach(appendBackupCandidate);
  }

  if (!primary && backups.length) {
    primary = backups.shift();
  }

  return {
    audioUrl: primary || '',
    backupAudioUrls: backups,
  };
}

function normalizeHttpUrl(value) {
  const str = ensureString(value);
  if (!str) return '';
  if (/^https?:\/\//i.test(str)) return str;
  if (str.startsWith('//')) {
    return `https:${str}`;
  }
  return '';
}

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

function toNumberOrUndefined(value) {
  if (value == null) return undefined;
  const num = Number(value);
  return Number.isFinite(num) ? num : undefined;
}
