// ─────────────────────────────────────────────────────────────
// SK BRIDGE — adapter between window.SK_DATA and the workbench
// shell. The shell imports nothing from SK_DATA directly; it only
// calls window.SK.* methods exposed here.
//
// Three modes, set by window.__SK_MODE:
//   'mock'     — no backend; calls are no-ops, loadIdeas returns null
//                so the shell falls back to its built-in fixtures.
//   'readonly' — loadIdeas reads real data; mutations no-op.
//   'live'     — full reads + writes through SK_DATA.
//
// Every mutation returns a real Promise. On rejection the bridge
// surfaces err.message via window.flashToast (if registered) and
// re-throws so the caller can revert optimistic state.
// ─────────────────────────────────────────────────────────────

(function () {
  const MODE  = window.__SK_MODE || (window.SK_DATA ? 'live' : 'mock');
  const isLive     = MODE === 'live'     && !!window.SK_DATA;
  const isReadonly = MODE === 'readonly' && !!window.SK_DATA;
  const canRead    = isLive || isReadonly;
  const canWrite   = isLive;
  const D = window.SK_DATA || {};
  const API = window.__SK_API || null;

  // ─── error surfacer ─────────────────────────────────────────
  function reportError(verb, err) {
    const msg = (err && err.message) || ('Couldn\u2019t ' + verb + '.');
    try { if (typeof window.flashToast === 'function') window.flashToast(msg); }
    catch (e) { /* swallow toast errors */ }
    console.warn('[SK]', verb, 'failed:', err);
  }

  // safeRead: read-side; returns null in mock, real promise in read/live.
  function safeRead(name) {
    return (...args) => {
      if (!canRead || typeof D[name] !== 'function') return Promise.resolve(null);
      try {
        const r = D[name](...args);
        return Promise.resolve(r).catch(err => {
          reportError(name, err);
          throw err;
        });
      } catch (err) {
        reportError(name, err);
        return Promise.reject(err);
      }
    };
  }

  // safeMut: write-side; no-op (resolved null) when !canWrite,
  // real promise in live. Errors toast + re-throw.
  function safeMut(name) {
    return (...args) => {
      if (!canWrite || typeof D[name] !== 'function') {
        if (!isLive) console.debug('[SK mock]', name, args);
        return Promise.resolve(null);
      }
      try {
        const r = D[name](...args);
        return Promise.resolve(r).catch(err => {
          reportError(name, err);
          throw err;
        });
      } catch (err) {
        reportError(name, err);
        return Promise.reject(err);
      }
    };
  }

  // debounce factory — keyed per (fn, id) so two ideas can save
  // independently without one cancelling the other.
  function debounced(fn, ms) {
    const timers = new Map();
    return (key, ...args) => {
      const existing = timers.get(key);
      if (existing) clearTimeout(existing);
      const t = setTimeout(() => {
        timers.delete(key);
        fn(key, ...args);
      }, ms);
      timers.set(key, t);
    };
  }

  // ── URL helpers ─────────────────────────────────────────────
  function mediaUrl(path) {
    if (canRead && typeof D.mediaUrl === 'function') return D.mediaUrl(path);
    if (typeof window.__SK_mediaUrl === 'function') return window.__SK_mediaUrl(path);
    if (API) return `${API}${path}`;
    return null;
  }
  function stillUrl(id, sceneKey, engIdx, candIdx) {
    if (canRead && typeof D.stillUrl === 'function') return D.stillUrl(id, sceneKey, engIdx, candIdx);
    return null;
  }
  // prev-round-strip-2026-06-11 / history-rounds-2026-06-12: archived stills.
  function prevStillUrl(id, name, prefix) {
    if (canRead && typeof D.prevStillUrl === 'function') return D.prevStillUrl(id, name, prefix);
    return null;
  }
  // adopt-prior-still-v2-2026-06-12: use an archived frame in the final render.
  const adoptPriorStill = safeMut('adoptPriorStill');
  function videoUrl(id) {
    if (canRead && typeof D.videoUrl === 'function') return D.videoUrl(id);
    return null;
  }
  function previewVideoUrl(id) {
    if (canRead && typeof D.previewVideoUrl === 'function') return D.previewVideoUrl(id);
    return null;
  }
  function clipUrl(id, idx) {
    if (canRead && typeof D.clipUrl === 'function') return D.clipUrl(id, idx);
    return null;
  }
  function previewMp4Url(id) {
    if (canRead && typeof D.previewMp4Url === 'function') return D.previewMp4Url(id);
    return null;
  }

  // ── Initial / polling read ───────────────────────────────────
  const _loadIdeasRaw = safeRead('loadIdeas');
  function loadIdeas() {
    if (MODE === 'mock') return Promise.resolve(null);
    return _loadIdeasRaw();
  }

  // ── Idea lifecycle ─────────────────────────────────────────
  const approveIdea         = safeMut('approveIdea');
  const rejectIdea          = safeMut('rejectIdea');
  const unrejectIdea        = safeMut('unrejectIdea');
  const regenWithFeedback   = safeMut('regenWithFeedback');
  const sendBack            = safeMut('sendBack');
  const generateIdea        = safeMut('generateIdea');
  const generateFactsSlideshow = safeMut('generateFactsSlideshow');
  const generatePlatformCopy= safeMut('generatePlatformCopy');

  // ── Curate / audio / animation ─────────────────────────────
  const finalizeCuration       = safeMut('finalizeCuration');
  const regenSceneCandidates   = safeMut('regenSceneCandidates');
  const regenCandidate         = safeMut('regenCandidate');
  const saveSceneEngines       = safeMut('saveSceneEngines');
  const saveAnimationStyle     = safeMut('saveAnimationStyle');
  const saveSceneMotionPrompt  = safeMut('saveSceneMotionPrompt');
  const generateMotionPrompt   = safeMut('generateMotionPrompt');
  const saveKeyframePromptRaw  = safeMut('saveKeyframePrompt');
  const saveFrameNoteRaw       = safeMut('saveFrameNote');
  const saveTextAndRebuild     = safeMut('saveTextAndRebuild');

  // ── Final + publish ────────────────────────────────────────
  const promotePreview   = safeMut('promotePreview');
  const finalizeAnimation= safeMut('finalizeAnimation');
  const prepareForPublish= safeMut('prepareForPublish');
  const approveFinal     = safeMut('approveFinal');
  const unapprove        = safeMut('unapprove');
  const deleteVideo      = safeMut('deleteVideo');
  const markPosted       = safeMut('markPosted');

  // ── Preview / rebuild / failure-recovery ───────────────────
  // These map 1:1 onto worker endpoints (read by SK_DATA in sk-data.js):
  //   rejectPreview    → POST /videos/{id}/preview-reject   { notes }
  //   rebuildPreview   → POST /videos/{id}/preview-rebuild  (re-render preview)
  //   rebuildAudioMix  → POST /videos/{id}/rebuild-audio-mix (audio-only remix)
  //   retryRender      → POST /videos/{id}/retry-render     (re-queue a failed row)
  //   dismissFailed    → POST /ideas/{id}/dismiss-failed    (drop a failed idea)
  //   forceKick        → POST /manual-kick { id }           (wake the renderer)
  const rejectPreview    = safeMut('rejectPreview');
  const rebuildPreview   = safeMut('rebuildPreview');
  const rebuildAudioMix  = safeMut('rebuildAudioMix');
  const retryRender      = safeMut('retryRender');
  const dismissFailed    = safeMut('dismissFailed');
  const forceKick        = safeMut('forceKick');
  const regenClip        = safeMut('regenClip');
  const regenSceneWithFeedback = safeMut('regenSceneWithFeedback');

  // ── Library / log ──────────────────────────────────────────
  const loadAudioCatalog   = safeRead('loadAudioCatalog');
  const loadVisualCatalog  = safeRead('loadVisualCatalog');
  const addSfxFromLibrary  = safeMut('addSfxFromLibrary');
  const setBgFromLibrary   = safeMut('setBgFromLibrary');
  const loadAuditLog       = safeRead('loadAuditLog');

  // ── Debounced text/slider saves ────────────────────────────
  // saveEdits   → 800ms (text inputs)
  // saveMix     → 400ms (sliders / cue edits)
  // saveKeyframePrompt → 800ms
  // saveFrameNote      → 800ms
  const _saveEditsRaw = safeMut('saveEdits');
  const _saveMixRaw   = safeMut('saveMix');
  const _saveEditsDeb = debounced((id, edits) => _saveEditsRaw(id, edits), 800);
  const _saveMixDeb   = debounced((id, mix)   => _saveMixRaw(id, mix),     400);
  const _saveKfpDeb   = debounced((k, id, scene, prompt) => saveKeyframePromptRaw(id, scene, prompt), 800);
  const _saveFnDeb    = debounced((k, id, scene, note)   => saveFrameNoteRaw(id, scene, note),       800);
  function saveEdits(id, edits)              { _saveEditsDeb(id, edits); }
  function saveMix(id, mix)                  { _saveMixDeb(id, mix); }
  function saveKeyframePrompt(id, scene, p)  { _saveKfpDeb(`${id}:${scene}`, id, scene, p); }
  function saveFrameNote(id, scene, n)       { _saveFnDeb(`${id}:${scene}`, id, scene, n); }

  // ── Stage advance → right verb ──────────────────────────────
  // The shell calls SK.advanceStage(id, fromStage, toStage, payload).
  // Returns the underlying Promise so the caller can await + revert.
  function advanceStage(id, fromStage, toStage, payload) {
    // 2026-06-04: a BACKWARD move (the ↩ send-back button) must never hit the
    // forward handlers below — especially toStage='animation' (which pops the
    // $3 render confirm) or toStage='audio' (which re-runs finalizeCuration).
    // Detect a backward step by stage order and route straight to the cheap
    // sendBack verb. Fixes "↩ asks to spend $3 / re-finalises".
    // 'approved' included so a backward move from an in-flight row is still
    // detected as backward instead of falling through to the forward handlers.
    const _ORDER = ['pending', 'approved', 'curating', 'audio', 'animation', 'final', 'published'];
    // ISSUE 6 (2026-06-05): map the UI target stage to the TRUE previous-stage
    // worker transition. 'animation' has back_to_animation; 'pending' now has
    // back_to_ideas (2026-06-09 — previously mapped to back_to_curate, so ↩
    // from Curate left the row in Curate and never reached the Ideas tab).
    const _BACK = {
      pending: 'back_to_ideas', approved: 'back_to_ideas',
      curating: 'back_to_curate',
      audio: 'back_to_sound', animation: 'back_to_animation',
    };
    if (_ORDER.indexOf(toStage) >= 0 && _ORDER.indexOf(fromStage) > _ORDER.indexOf(toStage)) {
      return sendBack(id, _BACK[toStage] || 'back_to_sound');
    }
    if (toStage === 'approved')                       return approveIdea(id, payload);
    if (toStage === 'audio' || toStage === 'preview') {
      // 2026-06-10 flow fix: Continue→Audio is now ALWAYS the cheap preview
      // build (voice + slideshow + audio stems, ~$0.02) — the worker no longer
      // promotes here for dev2. The paid clip render happens at the explicit
      // Promote (audio→animation) below, AFTER the user picks the animation
      // engine. The old $3 confirm at this step is gone with it.
      return finalizeCuration(id, (payload && payload.picks) || {});
    }

    // audio → animation: THE paid step. promotePreview kicks the full
    // Runway/Luma render (~$3, irreversible) and lands the row in the
    // Animate tab (build_status=awaiting_animation). This is the only
    // place we spend render money, so it's the only place we confirm.
    if (toStage === 'animation') {
      // B1/B5 (2026-06-03): never fire the full render without an explicit
      // money confirm. 2026-06-10: price depends on the picked engine
      // ($1.63 Kling Std 5s … $3.50 Runway 10s; Ken Burns = free, no confirm).
      const _vEng = payload && payload.idea_vendor;
      const _vPaid = _vEng && _vEng !== 'ken_burns_local';
      if (_vPaid && typeof window !== 'undefined' && typeof window.confirm === 'function'
          && !window.confirm('Start the full video render with ' + _vEng + '?\n\nThis is the PAID step (~$1.6–3.5 depending on engine) and can’t be undone.')) {
        return Promise.resolve({ cancelled: true });
      }
      // C1: forward the chosen video engine + any per-scene map so the
      // worker persists the user's pick. promotePreview requires an
      // idea_vendor (or falls back to the row's previously-saved one);
      // dev1 sends { idea_vendor, scene_engines } here. We pass through
      // whatever the workbench provided (one image-engine per video; no
      // per-scene UI per Scott's decision), letting the worker fall back
      // to row.idea_vendor when the payload omits it.
      const promoteBody = {};
      if (payload && payload.idea_vendor)   promoteBody.idea_vendor  = payload.idea_vendor;
      if (payload && payload.scene_engines) promoteBody.scene_engines = payload.scene_engines;
      return promotePreview(id, promoteBody);
    }

    // animation → final: advance the reviewed clips out of the Animate
    // tab. finalizeAnimation re-composites from CACHED clips (no new
    // vendor calls → free), so NO money confirm here. C3-adjacent.
    if (toStage === 'final')                          return finalizeAnimation(id);

    // final → published: C3. Publishing first re-muxes the audio
    // (prepareForPublish), NOT approveFinal. prepareForPublish queues the
    // audio-only mux + the publish transition; the renderer calls
    // /droplet/{id}/publish-prepared on completion to land it in Queue.
    if (toStage === 'published')                      return prepareForPublish(id);

    // back-tracking fallback (e.g. fromStage unknown like 'failed'): use the
    // same verb map — the old 'back_to_' + toStage built verbs the worker
    // doesn't support (back_to_pending/back_to_curating → 400) (2026-06-09).
    if (_BACK[toStage]) return sendBack(id, _BACK[toStage]);
    return Promise.resolve(null);
  }

  // ── expose ──────────────────────────────────────────────────
  window.SK = {
    mode: MODE,
    isLive, isReadonly, canWrite, canRead,
    api: API,

    // urls
    mediaUrl, stillUrl, prevStillUrl, adoptPriorStill, videoUrl, previewVideoUrl, clipUrl, previewMp4Url,
    voiceAudioUrl: (id)    => mediaUrl(`/videos/${id}/audio/voice`),
    bgAudioUrl:    (id)    => mediaUrl(`/videos/${id}/audio/bg`),
    sfxAudioUrl:   (id, n) => mediaUrl(`/videos/${id}/audio/sfx_${n}`),

    // reads
    loadIdeas,
    loadAudioCatalog, loadVisualCatalog, loadAuditLog,

    // idea lifecycle
    approveIdea, rejectIdea, unrejectIdea, regenWithFeedback, sendBack,
    generateIdea, generateFactsSlideshow, generatePlatformCopy,
    advanceStage,

    // edits (debounced)
    saveEdits, saveMix, saveKeyframePrompt, saveFrameNote,

    // curate / animation
    finalizeCuration, regenSceneCandidates, regenCandidate,
    saveSceneEngines, saveAnimationStyle,
    saveSceneMotionPrompt, generateMotionPrompt,
    saveTextAndRebuild,

    // final / publish
    promotePreview, finalizeAnimation, prepareForPublish,
    approveFinal, unapprove, deleteVideo, markPosted,

    // preview / rebuild / failure-recovery
    rejectPreview, rebuildPreview, rebuildAudioMix,
    retryRender, dismissFailed, forceKick, regenClip, regenSceneWithFeedback,

    // library
    addSfxFromLibrary, setBgFromLibrary,
  };

  // ── visible mode badge (so it's obvious which mode is active) ──
  if (typeof document !== 'undefined') {
    const stamp = () => {
      if (document.getElementById('sk-mode-badge')) return;
      const el = document.createElement('div');
      el.id = 'sk-mode-badge';
      const colour = MODE === 'live' ? '#2f6b4a'  /* B20: align to palette brand green */
                    : MODE === 'readonly' ? '#a87320'
                    : '#1d3557';
      el.textContent = MODE === 'mock' ? 'mock' : `${MODE} \u00b7 ${shortHost(API)}`;
      el.style.cssText = `
        position: fixed; top: 8px; right: 8px; z-index: 9999;
        font-family: "Geist Mono", ui-monospace, monospace;
        font-size: 9px; font-weight: 700; letter-spacing: 0.14em;
        text-transform: uppercase; pointer-events: none;
        padding: 3px 6px; border-radius: 3px;
        background: ${colour}; color: white; opacity: 0.78;
      `;
      document.body.appendChild(el);
    };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', stamp);
    else stamp();
  }

  function shortHost(url) {
    if (!url) return 'sk_data';
    try { return new URL(url).host.replace(/^www\./, ''); }
    catch { return String(url).replace(/^https?:\/\//, '').replace(/\/$/, ''); }
  }
})();
