const audio = document.getElementById('player');
const coverEl = document.getElementById('episode-cover');
const episodeTitleEl = document.getElementById('episode-title');
const podcastTitleEl = document.getElementById('podcast-title');
audio.controls = false;
console.log('[核桃FM][offscreen] init');
let lastStateSentAt = 0;
let currentEpisode = null;
let mediaSessionHandlersRegistered = false;
resetEpisodeMetadata();

// Serialize operations to avoid races (e.g., play interrupted by load)
let opChain = Promise.resolve();
function enqueueOp(fn) {
  opChain = opChain.then(() => fn()).catch(err => { throw err; });
  return opChain;
}

// (We keep util if needed later, but no fixed wait to reduce latency)
function waitForReady(target, timeoutMs = 0) {
  return Promise.resolve();
}

// Diagnostics for measuring load/play timings
let lastTimingSentAt = 0;
let loadTrace = null;
function startLoadTrace(src) {
  loadTrace = { src: ensureString(src), t0: Date.now(), steps: {} };
  sendTiming({ event: 'load_cmd' });
}
function markStep(name) {
  try { if (loadTrace) loadTrace.steps[name] = Date.now(); } catch (_) {}
  sendTiming({ event: name });
}
function currentBufferedEnd() {
  try { const b = audio.buffered; const n = b ? b.length : 0; return n ? (b.end(n - 1) || 0) : 0; } catch (_) { return 0; }
}
function sendTiming(extra = {}) {
  try {
    const now = Date.now();
    const throttle = extra && extra.event === 'progress' ? 400 : 0;
    if (throttle && now - lastTimingSentAt < throttle) return;
    lastTimingSentAt = now;
    const t0 = loadTrace?.t0 || now;
    const payload = {
      type: 'offscreen.playback.timing',
      timing: {
        event: String(extra?.event || ''),
        at: now,
        sinceLoadMs: Math.max(0, now - t0),
        src: ensureString(loadTrace?.src || audio.currentSrc || audio.src || ''),
        networkState: Number(audio.networkState) || 0,
        readyState: Number(audio.readyState) || 0,
        positionSec: Number(audio.currentTime) || 0,
        durationSec: Number(audio.duration) || 0,
        bufferedEndSec: currentBufferedEnd(),
        steps: loadTrace?.steps || {},
        error: extra?.error || null,
      },
    };
    const maybe = chrome.runtime.sendMessage(payload);
    if (maybe && typeof maybe.then === 'function') maybe.catch(() => {});
  } catch (_) {}
}

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

function mimeFromUrl(url) {
  const lower = ensureString(url).toLowerCase();
  if (!lower) return '';
  if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
  if (lower.endsWith('.png')) return 'image/png';
  if (lower.endsWith('.webp')) return 'image/webp';
  if (lower.endsWith('.gif')) return 'image/gif';
  return '';
}

function updateMediaSessionMetadata({ title, podcastTitle, coverUrl }) {
  if (!('mediaSession' in navigator) || typeof window.MediaMetadata !== 'function') return;
  try {
    const artwork = coverUrl
      ? [{ src: coverUrl, sizes: '512x512', type: mimeFromUrl(coverUrl) || undefined }]
      : [];
    navigator.mediaSession.metadata = new MediaMetadata({
      title: title || podcastTitle || '核桃FM',
      artist: podcastTitle || '',
      album: podcastTitle || '',
      artwork,
    });
  } catch (err) {
    console.warn('[核桃FM][offscreen] mediaSession metadata failed', err);
  }
}

function clearMediaSessionMetadata() {
  if (!('mediaSession' in navigator)) return;
  try {
    navigator.mediaSession.metadata = null;
  } catch (err) {
    // ignore
  }
}

function updateEpisodeMetadata(episode) {
  currentEpisode = episode || null;
  const title = ensureString(
    episode?.episodeTitle || episode?.title || episode?.name || episode?.id
  );
  const podcastTitle = ensureString(
    episode?.podcastTitle || episode?.podcast?.title || episode?.showTitle
  );
  const coverUrl = ensureString(episode?.cover || episode?.image || episode?.artwork);

  if (coverEl) {
    if (coverUrl) {
      coverEl.src = coverUrl;
      coverEl.style.display = 'block';
      coverEl.removeAttribute('aria-hidden');
    } else {
      coverEl.removeAttribute('src');
      coverEl.style.display = 'none';
      coverEl.setAttribute('aria-hidden', 'true');
    }
  }

  if (episodeTitleEl) {
    episodeTitleEl.textContent = title || 'Loading episode...';
  }

  if (podcastTitleEl) {
    podcastTitleEl.textContent = podcastTitle;
    podcastTitleEl.style.display = podcastTitle ? 'block' : 'none';
  }

  const docTitleParts = [];
  if (title) docTitleParts.push(title);
  if (podcastTitle) docTitleParts.push(podcastTitle);
  docTitleParts.push('Offscreen Player');
  document.title = docTitleParts.join(' - ');

  updateMediaSessionMetadata({ title, podcastTitle, coverUrl });
}

function resetEpisodeMetadata() {
  currentEpisode = null;
  if (coverEl) {
    coverEl.removeAttribute('src');
    coverEl.style.display = 'none';
    coverEl.setAttribute('aria-hidden', 'true');
  }
  if (episodeTitleEl) {
    episodeTitleEl.textContent = '';
  }
  if (podcastTitleEl) {
    podcastTitleEl.textContent = '';
    podcastTitleEl.style.display = 'none';
  }
  document.title = 'Offscreen Player';
  clearMediaSessionMetadata();
}

function syncMediaSessionPlaybackState() {
  if (!('mediaSession' in navigator)) return;
  try {
    navigator.mediaSession.playbackState = audio.paused ? 'paused' : 'playing';
  } catch (err) {
    // ignore
  }
}

function requestPlaybackControl(action, extraPayload = {}) {
  return new Promise((resolve, reject) => {
    if (!chrome?.runtime?.sendMessage) {
      reject(new Error('runtime unavailable'));
      return;
    }
    const payload = { action, ...extraPayload };
    try {
      chrome.runtime.sendMessage(
        { type: 'xyz.playback.control', payload },
        response => {
          const err = chrome.runtime.lastError;
          if (err) {
            reject(new Error(err.message || 'runtime error'));
            return;
          }
          if (!response || response.ok !== true) {
            reject(new Error(response?.error || 'playback control failed'));
            return;
          }
          resolve(response.result || true);
        }
      );
    } catch (err) {
      reject(err);
    }
  });
}

function setupMediaSessionHandlers() {
  if (mediaSessionHandlersRegistered) return;
  if (!('mediaSession' in navigator) || typeof navigator.mediaSession.setActionHandler !== 'function') {
    return;
  }
  const setHandler = (action, handler) => {
    try {
      navigator.mediaSession.setActionHandler(action, handler);
    } catch (err) {
      // Some platforms may not support specific handlers; ignore.
    }
  };
  setHandler('play', () => {
    audio.play().catch(() => {});
  });
  setHandler('pause', () => {
    audio.pause().catch(() => {});
  });
  setHandler('stop', () => {
    audio.pause().catch(() => {});
    audio.currentTime = 0;
    requestPlaybackControl('pause').catch(() => {});
  });
  setHandler('seekforward', details => {
    const offset = Number(details?.seekOffset) || 30;
    audio.currentTime = Math.min((audio.duration || Infinity), (audio.currentTime || 0) + offset);
    sendUpdate(0);
  });
  setHandler('seekbackward', details => {
    const offset = Number(details?.seekOffset) || 15;
    audio.currentTime = Math.max(0, (audio.currentTime || 0) - offset);
    sendUpdate(0);
  });
  setHandler('seekto', details => {
    const pos = Number(details?.seekTime);
    if (!Number.isFinite(pos)) return;
    const duration = Number(audio.duration) || 0;
    audio.currentTime = duration > 0 ? Math.max(0, Math.min(duration, pos)) : Math.max(0, pos);
    sendUpdate(0);
  });
  setHandler('nexttrack', () => {
    requestPlaybackControl('skipNext').catch(err => {
      console.warn('[核桃FM][offscreen] skipNext via mediaSession failed', err);
    });
  });
  setHandler('previoustrack', null);
  mediaSessionHandlersRegistered = true;
}

setupMediaSessionHandlers();

function getState() {
  return {
    playing: !audio.paused,
    positionSec: audio.currentTime || 0,
    durationSec: audio.duration || 0,
    playbackRate: audio.playbackRate || 1,
    src: audio.src || '',
  };
}

function sendUpdate(throttleMs = 250) {
  const now = Date.now();
  if (now - lastStateSentAt < throttleMs) return;
  lastStateSentAt = now;
  try {
    const maybe = chrome.runtime.sendMessage({ type: 'offscreen.playback.update', state: getState() });
    if (maybe && typeof maybe.then === 'function') {
      maybe.catch(() => {});
    }
  } catch (e) {}
}

function ok(result) {
  return { ok: true, result };
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (!message || message.type !== 'offscreen.playback.control') return false;
  const payload = message.payload || {};
  const type = payload.type;
  enqueueOp(async () => {
    switch (type) {
      case 'preconnect': {
        const one = ensureString(payload.url);
        if (one) ensurePreconnectForUrl(one);
        const many = Array.isArray(payload.urls) ? payload.urls : [];
        many.forEach(u => ensurePreconnectForUrl(u));
        return true;
      }
      case 'toggle': {
        const hasSrc = ensureString(audio.currentSrc || audio.src);
        if (!hasSrc) {
          throw new Error('NO_SRC');
        }
        if (audio.paused) {
          try { const p = audio.play(); if (p && typeof p.catch === 'function') p.catch(() => {}); } catch (_) {}
        } else {
          try { audio.pause(); } catch (_) {}
        }
        return true;
      }
      case 'load': {
        const episode = payload.episode || {};
        const src = episode.audioUrl || episode.src;
        if (!src) throw new Error('Missing audio url');
        console.log('[核桃FM][offscreen] load', { src });
        startLoadTrace(src);
        ensurePreconnectForUrl(src);
        updateEpisodeMetadata(episode);
        audio.src = src;
        audio.playbackRate = 1;
        audio.currentTime = 0;
        try { audio.pause(); } catch (_) {}
        try { audio.load?.(); } catch (_) {}
        markStep('load_call');
        return true;
      }
      case 'play':
        console.log('[核桃FM][offscreen] play');
        try { const p = audio.play(); if (p && typeof p.catch === 'function') p.catch(() => {}); markStep('play_cmd'); } catch (_) {}
        return true;
      case 'pause':
        console.log('[核桃FM][offscreen] pause');
        try { audio.pause(); markStep('pause_cmd'); } catch (_) {}
        return true;
      case 'seekBy': {
        const seconds = Number(payload.seconds) || 0;
        console.log('[核桃FM][offscreen] seekBy', seconds);
        audio.currentTime = Math.max(0, (audio.currentTime || 0) + seconds);
        return true;
      }
      case 'seekTo': {
        const raw = payload.positionSec ?? payload.seconds ?? payload.position;
        const position = Number(raw);
        const duration = Number(audio.duration) || 0;
        const target = Number.isFinite(position) ? Math.max(0, position) : 0;
        const clamped = duration > 0 ? Math.min(duration, target) : target;
        console.log('[核桃FM][offscreen] seekTo', clamped);
        audio.currentTime = clamped;
        return true;
      }
      case 'setRate': {
        const rate = Number(payload.rate) || 1;
        console.log('[核桃FM][offscreen] setRate', rate);
        audio.playbackRate = rate;
        return true;
      }
      case 'stop':
        console.log('[核桃FM][offscreen] stop');
        try { audio.pause(); } catch (_) {}
        try { audio.removeAttribute('src'); } catch (_) {}
        try { audio.load(); } catch (_) {}
        resetEpisodeMetadata();
        syncMediaSessionPlaybackState();
        return true;
      default:
        throw new Error(`Unsupported offscreen action: ${type}`);
    }
  })
    .then(result => { try { sendResponse(ok(result)); sendUpdate(0); } catch (_) {} })
    .catch(err => { try { sendResponse({ ok: false, error: err?.message || String(err) }); sendUpdate(0); } catch (_) {} });
  return true;
});

audio.addEventListener('timeupdate', () => { sendUpdate(250); sendTiming({ event: 'timeupdate' }); });
audio.addEventListener('loadstart', () => sendTiming({ event: 'loadstart' }));
audio.addEventListener('loadedmetadata', () => sendTiming({ event: 'loadedmetadata' }));
audio.addEventListener('loadeddata', () => sendTiming({ event: 'loadeddata' }));
audio.addEventListener('progress', () => sendTiming({ event: 'progress' }));
audio.addEventListener('canplay', () => sendTiming({ event: 'canplay' }));
audio.addEventListener('canplaythrough', () => sendTiming({ event: 'canplaythrough' }));
audio.addEventListener('playing', () => {
  syncMediaSessionPlaybackState();
  sendUpdate(0);
  sendTiming({ event: 'playing' });
});
audio.addEventListener('pause', () => {
  syncMediaSessionPlaybackState();
  sendUpdate(0);
  sendTiming({ event: 'pause' });
});
audio.addEventListener('ratechange', () => sendUpdate(0));
audio.addEventListener('durationchange', () => sendUpdate(0));
audio.addEventListener('error', () => {
  sendUpdate(0);
  syncMediaSessionPlaybackState();
  try {
    const mediaError = audio.error || {};
    const detail = {
      code: mediaError.code || null,
      message: mediaError.message || '',
      src: audio.currentSrc || audio.src || '',
      positionSec: audio.currentTime || 0,
    };
    sendTiming({ event: 'error', error: detail });
    const maybe = chrome.runtime.sendMessage({
      type: 'offscreen.playback.error',
      error: detail,
      state: getState(),
    });
    if (maybe && typeof maybe.then === 'function') {
      maybe.catch(() => {});
    }
  } catch (e) {}
});
audio.addEventListener('ended', () => {
  sendUpdate(0);
  syncMediaSessionPlaybackState();
  try {
    const maybe = chrome.runtime.sendMessage({ type: 'offscreen.playback.ended' });
    if (maybe && typeof maybe.then === 'function') {
      maybe.catch(() => {});
    }
  } catch (e) {}
});

// Signal ready once script is evaluated
try {
  console.log('[核桃FM][offscreen] ready');
  const maybe = chrome.runtime.sendMessage({ type: 'offscreen.ready' });
  if (maybe && typeof maybe.then === 'function') {
    maybe.catch(() => {});
  }
} catch (e) {}
// Preconnect cache to warm up TLS/DNS for audio origins
const preconnectedOrigins = new Set();
function ensurePreconnectForUrl(url) {
  try {
    const u = new URL(ensureString(url), location.href);
    if (!/^https?:$/i.test(u.protocol)) return;
    const origin = u.origin;
    if (preconnectedOrigins.has(origin)) return;
    preconnectedOrigins.add(origin);
    const head = document.head || document.getElementsByTagName('head')[0];
    const mk = (rel) => { try { const link = document.createElement('link'); link.rel = rel; link.href = origin; head.appendChild(link); } catch (_) {} };
    mk('preconnect');
    mk('dns-prefetch');
  } catch (_) {}
}
