// UI/Options contexts use this controller to talk to background playback.

const CTRL_TYPE = 'xyz.playback.control';
const STATE_TYPE = 'xyz.playback.state';

export async function getPlaybackState() {
  return invokeControl({ action: 'getState' });
}

export async function playNow(episode) {
  console.log('[核桃FM][ui] playNow', episode?.id);
  return invokeControl({ action: 'playNow', episode: normalizeEpisode(episode) });
}

export async function enqueueNext(episode) {
  console.log('[核桃FM][ui] enqueueNext', episode?.id);
  return invokeControl({ action: 'enqueueNext', episode: normalizeEpisode(episode) });
}

export async function removeFromQueue(episodeId) {
  console.log('[核桃FM][ui] removeFromQueue', episodeId);
  return invokeControl({ action: 'removeFromQueue', episodeId: ensureString(episodeId) });
}

export async function complete(episodeId) {
  console.log('[核桃FM][ui] complete', episodeId);
  return invokeControl({ action: 'complete', episodeId: ensureString(episodeId) });
}

export async function toggle() {
  console.log('[核桃FM][ui] toggle');
  return invokeControl({ action: 'toggle' });
}

export async function pause() {
  console.log('[核桃FM][ui] pause');
  try {
    await invokeOffscreen({ type: 'pause' });
    return true;
  } catch (err) {
    return invokeControl({ action: 'pause' });
  }
}

export async function resume() {
  console.log('[核桃FM][ui] resume');
  try {
    await invokeOffscreen({ type: 'play' });
    return true;
  } catch (err) {
    return invokeControl({ action: 'resume' });
  }
}

export async function skipNext() {
  console.log('[核桃FM][ui] skipNext');
  return invokeControl({ action: 'skipNext' });
}

export async function seekBy(seconds) {
  console.log('[核桃FM][ui] seekBy', seconds);
  const s = Number(seconds) || 0;
  try {
    await invokeOffscreen({ type: 'seekBy', seconds: s });
    return true;
  } catch (err) {
    return invokeControl({ action: 'seekBy', seconds: s });
  }
}

export async function seekTo(positionSec) {
  const pos = Number(positionSec);
  const target = Number.isFinite(pos) ? Math.max(0, pos) : 0;
  console.log('[核桃FM][ui] seekTo', target);
  try {
    await invokeOffscreen({ type: 'seekTo', positionSec: target });
    return true;
  } catch (err) {
    return invokeControl({ action: 'seekTo', positionSec: target });
  }
}

export async function setRate(rate) {
  const r = Number(rate);
  console.log('[核桃FM][ui] setRate', r);
  const value = Number.isFinite(r) && r > 0 ? r : 1;
  try {
    await invokeOffscreen({ type: 'setRate', rate: value });
    return true;
  } catch (err) {
    return invokeControl({ action: 'setRate', rate: value });
  }
}

export async function reorderQueue(ids) {
  const arr = Array.isArray(ids) ? ids.filter(Boolean).map(String) : [];
  console.log('[核桃FM][ui] reorderQueue', arr);
  return invokeControl({ action: 'reorderQueue', ids: arr });
}

export function subscribe(callback) {
  function handler(message) {
    if (message && message.type === STATE_TYPE) {
      callback(message.state);
    }
  }
  chrome.runtime.onMessage.addListener(handler);
  return () => chrome.runtime.onMessage.removeListener(handler);
}

async function invokeControl(payload) {
  const message = { type: CTRL_TYPE, payload };
  return new Promise((resolve, reject) => {
    const maxRetries = 1; // only retry once for transient port issues
    const baseDelayMs = 80;
    const isTransient = (message) => {
      const msg = String(message || '').toLowerCase();
      return (
        msg.includes('message port closed') ||
        msg.includes('receiving end does not exist') ||
        msg.includes('no receiving end') ||
        msg.includes('extension context invalidated')
      );
    };
    const attempt = (tryIndex) => {
      try {
        chrome.runtime.sendMessage(message, response => {
          const err = chrome.runtime.lastError;
          if (err) {
            // Retry only on specific transient runtime errors
            if (tryIndex < maxRetries && isTransient(err.message || err)) {
              const jitter = Math.floor(Math.random() * 40);
              const delay = baseDelayMs * (1 + tryIndex) + jitter;
              setTimeout(() => attempt(tryIndex + 1), delay);
              return;
            }
            console.warn('[核桃FM][ui] control failed', payload?.action, err);
            try { chrome.runtime.sendMessage({ type: 'ui.playback.error', action: payload?.action || '', error: err?.message || String(err) }); } catch (_) {}
            reject(new Error(err.message || 'runtime error'));
            return;
          }
          if (!response || response.ok !== true) {
            console.warn('[核桃FM][ui] control rejected', payload?.action, response);
            try { chrome.runtime.sendMessage({ type: 'ui.playback.error', action: payload?.action || '', error: response?.error || 'playback control failed' }); } catch (_) {}
            reject(new Error(response?.error || 'playback control failed'));
            return;
          }
          resolve(response.state || response.result || null);
        });
      } catch (err) {
        if (tryIndex < maxRetries && isTransient(err?.message || err)) {
          const jitter = Math.floor(Math.random() * 40);
          const delay = baseDelayMs * (1 + tryIndex) + jitter;
          setTimeout(() => attempt(tryIndex + 1), delay);
          return;
        }
        console.warn('[核桃FM][ui] control threw', payload?.action, err);
        try { chrome.runtime.sendMessage({ type: 'ui.playback.error', action: payload?.action || '', error: err?.message || String(err) }); } catch (_) {}
        reject(new Error(err?.message || 'runtime error'));
      }
    };
    attempt(0);
  });
}

function invokeOffscreen(message) {
  const envelope = { type: 'offscreen.playback.control', payload: message };
  const isTransient = (msg) => {
    const s = String(msg || '').toLowerCase();
    return s.includes('receiving end does not exist') || s.includes('no receiving end') || s.includes('message port closed');
  };
  return new Promise((resolve, reject) => {
    const attempt = (triesLeft) => {
      try {
        chrome.runtime.sendMessage(envelope, response => {
          const err = chrome.runtime.lastError;
          if (err) {
            if (triesLeft > 0 && isTransient(err.message || err)) {
              setTimeout(() => attempt(triesLeft - 1), 60);
              return;
            }
            try { chrome.runtime.sendMessage({ type: 'ui.playback.error', action: String(message?.type || ''), error: err?.message || String(err) }); } catch (_) {}
            reject(new Error(err.message || 'runtime error'));
            return;
          }
          if (!response || response.ok !== true) {
            try { chrome.runtime.sendMessage({ type: 'ui.playback.error', action: String(message?.type || ''), error: response?.error || 'offscreen control failed' }); } catch (_) {}
            reject(new Error(response?.error || 'offscreen control failed'));
            return;
          }
          resolve(response.result || true);
        });
      } catch (err) {
        if (triesLeft > 0 && isTransient(err?.message || err)) {
          setTimeout(() => attempt(triesLeft - 1), 60);
          return;
        }
        try { chrome.runtime.sendMessage({ type: 'ui.playback.error', action: String(message?.type || ''), error: err?.message || String(err) }); } catch (_) {}
        reject(err);
      }
    };
    attempt(1);
  });
}

function normalizeEpisode(raw) {
  if (!raw || typeof raw !== 'object') return null;
  const id = ensureString(raw.id);
  let audioUrl = ensureString(raw.audioUrl || raw.audio?.url);
  const backupAudioUrls = collectBackupAudioUrls(raw);
  if (!audioUrl && backupAudioUrls.length) {
    audioUrl = backupAudioUrls[0];
  }
  if (!id || !audioUrl) return null;
  return {
    id,
    episodeTitle: ensureString(raw.episodeTitle || raw.title || id),
    audioUrl,
    backupAudioUrls,
    cover: ensureString(raw.cover || raw.image || ''),
    publishedAt: raw.publishedAt || raw.publishAt || raw.updatedAt || raw.createdAt || null,
    duration: toNumberOrUndefined(
      raw.duration || raw.durationSec || raw.durationSeconds || raw.audio?.duration
    ),
    episodeLink: ensureString(raw.episodeLink || raw.link || ''),
    podcastTitle: ensureString(raw.podcastTitle || raw.podcast?.title || ''),
    podcastSourceRef: ensureString(raw.podcastSourceRef || raw.podcast?.sourceRef || ''),
    podcastLink: ensureString(raw.podcastLink || raw.podcast?.link || ''),
  };
}

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

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

function collectBackupAudioUrls(raw) {
  const urls = [];
  const seen = new Set();
  const push = value => {
    const normalized = normalizeHttpUrl(value);
    if (!normalized || seen.has(normalized)) return;
    seen.add(normalized);
    urls.push(normalized);
  };

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

  const extras = [
    raw?.backupAudioUrl,
    raw?.alternativeAudioUrl,
    raw?.fallbackAudioUrl,
  ];
  extras.forEach(push);

  if (Array.isArray(raw?.backupAudioUrls)) {
    raw.backupAudioUrls.forEach(push);
  }
  if (Array.isArray(raw?.audioAlternatives)) {
    raw.audioAlternatives.forEach(push);
  }
  if (Array.isArray(raw?.alternateAudioUrls)) {
    raw.alternateAudioUrls.forEach(push);
  }

  return urls;
}

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