/**
 * CreatorExportWizard — 4-step modal for exporting a Creator Studio video
 * with in-browser Piper TTS narration.
 *
 * Flow:
 *   1. Target    — MP4 (edit/share) vs MP4 for YouTube.
 *   2. Voiceover — Piper voice + rate + pitch + subtitle burn-in toggle,
 *                  with a Preview button.
 *   3. Generate  — synthesize narration for every scene that has text
 *                  (or re-synthesize all if the user checks "regenerate")
 *                  and upload each WAV to the backend.
 *   4. Render    — POST /studio/videos/{id}/export/mp4, poll, download.
 *
 * Race protection:
 *   Every async op captures a per-mount epoch; closing the modal or
 *   starting a new render bumps the epoch and stale responses are
 *   dropped. Same pattern as App.tsx's account-switch guard.
 */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AlertTriangle, CheckCircle2, Download, Film, Loader2, Mic, Play, Square, X, Youtube, } from 'lucide-react';
import { listVoices, synthesizeToBlob, speak as piperSpeak, stop as piperStop, isSupported as piperSupported, getSelectedVoiceId, setSelectedVoiceId, } from '../tts/piperTts';
import { DEFAULT_PIPER_VOICE_ID } from '../tts/piperVoices';
const POLL_INTERVAL_MS = 1000;
const POLL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
const PREVIEW_TEXT = 'Hello, this is a preview of your selected voice.';
function authHeaders() {
    const token = localStorage.getItem('homepilot_auth_token') || '';
    return token ? { Authorization: `Bearer ${token}` } : {};
}
function trimBackend(u) {
    return u.replace(/\/+$/, '');
}
export default function CreatorExportWizard({ open, onClose, backendUrl, videoId, scenes, videoTitle, disabledReason, }) {
    // ── Step ─────────────────────────────────────────────────────────────────
    const [step, setStep] = useState(1);
    // ── Step 1 state ─────────────────────────────────────────────────────────
    const [kind, setKind] = useState('mp4_youtube');
    // ── Step 2 state ─────────────────────────────────────────────────────────
    const voices = useMemo(() => listVoices(), []);
    const [voiceId, setVoiceId] = useState(() => piperSupported() ? getSelectedVoiceId() : DEFAULT_PIPER_VOICE_ID);
    const [rate, setRate] = useState(0.9);
    const [pitch, setPitch] = useState(1.0);
    const [subtitlesBurnIn, setSubtitlesBurnIn] = useState(true);
    // Default: synthesize every scene that has narration text, even if an
    // audioUrl already exists. The user's invariant is "audio from all
    // scenes" at export time, so freshness beats speed by default.
    // "Keep existing audio" below is the opt-in for re-exports of an
    // unchanged project, where regenerating every take is wasted work.
    const [keepExisting, setKeepExisting] = useState(false);
    const [previewing, setPreviewing] = useState(false);
    const [previewError, setPreviewError] = useState(null);
    // ── Step 3 state ─────────────────────────────────────────────────────────
    const [genIdx, setGenIdx] = useState(0);
    const [genTotal, setGenTotal] = useState(0);
    const [genError, setGenError] = useState(null);
    // ── Step 4 state ─────────────────────────────────────────────────────────
    const [job, setJob] = useState(null);
    const [submitError, setSubmitError] = useState(null);
    // Race protection.
    const epochRef = useRef(0);
    // Reset when modal opens or closes.
    useEffect(() => {
        if (open) {
            setStep(1);
            setJob(null);
            setSubmitError(null);
            setGenIdx(0);
            setGenTotal(0);
            setGenError(null);
            setPreviewing(false);
            setPreviewError(null);
        }
        else {
            epochRef.current += 1;
            try {
                piperStop();
            }
            catch { /* ignore */ }
        }
    }, [open]);
    // Persist voice selection so subsequent renders remember the pick.
    useEffect(() => {
        if (voiceId)
            setSelectedVoiceId(voiceId);
    }, [voiceId]);
    const downloadUrl = useMemo(() => {
        if (!job || job.status !== 'done')
            return '';
        const tok = localStorage.getItem('homepilot_auth_token') || '';
        const qp = tok ? `?token=${encodeURIComponent(tok)}` : '';
        return `${trimBackend(backendUrl)}/studio/videos/${videoId}/export/jobs/${job.id}/download${qp}`;
    }, [job, backendUrl, videoId]);
    // ── Step 2 — Preview ─────────────────────────────────────────────────────
    const handlePreview = useCallback(async () => {
        setPreviewError(null);
        if (previewing) {
            try {
                piperStop();
            }
            catch { /* ignore */ }
            setPreviewing(false);
            return;
        }
        if (!piperSupported()) {
            setPreviewError('Piper requires a secure context (https) with Web Audio + OPFS support.');
            return;
        }
        setPreviewing(true);
        try {
            await piperSpeak(PREVIEW_TEXT, {
                voiceId,
                rate,
                onEnd: () => setPreviewing(false),
                onError: (err) => {
                    setPreviewError(String(err?.message || err));
                    setPreviewing(false);
                },
            });
        }
        catch (err) {
            setPreviewError(String(err?.message || err));
            setPreviewing(false);
        }
    }, [previewing, voiceId, rate]);
    // ── Step 3 — Generate all scene narrations ───────────────────────────────
    /** Scenes that need synthesis under the current toggle state.
     *  Default ("keep existing" off): every scene with narration text.
     *  Opt-in ("keep existing" on):   only scenes that do NOT already
     *                                 have an uploaded audioUrl. */
    const scenesToSynthesize = useMemo(() => scenes.filter((s) => {
        const hasText = (s.narration || '').trim().length > 0;
        if (!hasText)
            return false;
        if (keepExisting && s.audioUrl)
            return false;
        return true;
    }), [scenes, keepExisting]);
    const handleGenerate = useCallback(async () => {
        if (!piperSupported()) {
            setGenError('Piper is not supported in this browser. Use a Chromium-based browser on https.');
            return;
        }
        epochRef.current += 1;
        const myEpoch = epochRef.current;
        setStep(3);
        setGenError(null);
        setGenIdx(0);
        setGenTotal(scenesToSynthesize.length);
        if (scenesToSynthesize.length === 0) {
            // Nothing to synthesize — jump straight to render.
            setStep(4);
            void startRender(myEpoch);
            return;
        }
        for (let i = 0; i < scenesToSynthesize.length; i++) {
            if (epochRef.current !== myEpoch)
                return;
            const scene = scenesToSynthesize[i];
            try {
                const blob = await synthesizeToBlob(scene.narration, { voiceId });
                if (epochRef.current !== myEpoch)
                    return;
                // Upload to the backend so render_mp4 can mix it in.
                const fd = new FormData();
                fd.append('file', blob, `${scene.id}.wav`);
                const url = `${trimBackend(backendUrl)}/studio/videos/${videoId}/scenes/${scene.id}/narration`;
                const r = await fetch(url, {
                    method: 'POST',
                    headers: { ...authHeaders() },
                    body: fd,
                });
                if (epochRef.current !== myEpoch)
                    return;
                if (!r.ok) {
                    const text = await r.text().catch(() => '');
                    throw new Error(text || `Upload failed (HTTP ${r.status})`);
                }
            }
            catch (err) {
                if (epochRef.current !== myEpoch)
                    return;
                setGenError(`Scene ${scene.idx + 1}: ${String(err?.message || err)}`);
                return;
            }
            setGenIdx(i + 1);
        }
        if (epochRef.current !== myEpoch)
            return;
        // All done — kick off the render.
        setStep(4);
        void startRender(myEpoch);
    }, [scenesToSynthesize, voiceId, backendUrl, videoId]);
    // ── Step 4 — Render + poll ───────────────────────────────────────────────
    const pollJob = useCallback(async (jobId, myEpoch) => {
        const url = `${trimBackend(backendUrl)}/studio/videos/${videoId}/export/jobs/${jobId}`;
        const start = Date.now();
        while (Date.now() - start < POLL_TIMEOUT_MS) {
            if (epochRef.current !== myEpoch)
                return;
            try {
                const r = await fetch(url, { headers: { ...authHeaders() } });
                if (epochRef.current !== myEpoch)
                    return;
                if (!r.ok) {
                    setJob((prev) => prev ? { ...prev, status: 'error', error: `HTTP ${r.status}` } : prev);
                    return;
                }
                const j = await r.json();
                if (epochRef.current !== myEpoch)
                    return;
                setJob({
                    id: j.id,
                    status: j.status,
                    progress: typeof j.progress === 'number' ? j.progress : 0,
                    error: j.error || null,
                    kind: j.kind,
                });
                if (j.status === 'done' || j.status === 'error')
                    return;
            }
            catch (err) {
                if (epochRef.current !== myEpoch)
                    return;
                setJob((prev) => prev
                    ? { ...prev, status: 'error', error: String(err?.message || err) }
                    : prev);
                return;
            }
            await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
        }
        if (epochRef.current === myEpoch) {
            setJob((prev) => prev ? { ...prev, status: 'error', error: 'Render timed out' } : prev);
        }
    }, [backendUrl, videoId]);
    const startRender = useCallback(async (myEpoch) => {
        setSubmitError(null);
        setJob({ id: '', status: 'queued', progress: 0, kind });
        try {
            const r = await fetch(`${trimBackend(backendUrl)}/studio/videos/${videoId}/export/mp4`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', ...authHeaders() },
                body: JSON.stringify({
                    kind,
                    audio_rate: rate,
                    audio_pitch: pitch,
                    subtitles: subtitlesBurnIn ? 'burn_in' : 'none',
                }),
            });
            if (epochRef.current !== myEpoch)
                return;
            if (!r.ok) {
                const text = await r.text().catch(() => '');
                const detail = text || `HTTP ${r.status}`;
                setSubmitError(detail);
                setJob({ id: '', status: 'error', progress: 0, error: detail, kind });
                return;
            }
            const j = await r.json();
            if (epochRef.current !== myEpoch)
                return;
            setJob({
                id: j.id,
                status: j.status,
                progress: typeof j.progress === 'number' ? j.progress : 0,
                kind: j.kind,
            });
            void pollJob(j.id, myEpoch);
        }
        catch (err) {
            if (epochRef.current !== myEpoch)
                return;
            const msg = String(err?.message || err);
            setSubmitError(msg);
            setJob({ id: '', status: 'error', progress: 0, error: msg, kind });
        }
    }, [backendUrl, videoId, kind, rate, pitch, subtitlesBurnIn, pollJob]);
    if (!open)
        return null;
    const submitDisabled = Boolean(disabledReason);
    // ── UI ───────────────────────────────────────────────────────────────────
    return (<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4" onClick={onClose} role="dialog" aria-modal="true" aria-label="Export video">
      <div className="w-full max-w-xl rounded-2xl border border-white/10 bg-[#0b0b0c] text-white shadow-2xl max-h-[90vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}>
        {/* Header */}
        <div className="flex items-center justify-between border-b border-white/10 px-5 py-3 flex-shrink-0">
          <div>
            <div className="text-sm font-semibold">Export video</div>
            {videoTitle ? (<div className="text-xs text-white/50 truncate max-w-[28rem]">{videoTitle}</div>) : null}
          </div>
          <div className="flex items-center gap-3 text-[11px] text-white/40">
            <span className={step >= 1 ? 'text-white/80' : ''}>1 · Target</span>
            <span>›</span>
            <span className={step >= 2 ? 'text-white/80' : ''}>2 · Voiceover</span>
            <span>›</span>
            <span className={step >= 3 ? 'text-white/80' : ''}>3 · Narration</span>
            <span>›</span>
            <span className={step >= 4 ? 'text-white/80' : ''}>4 · Render</span>
          </div>
          <button type="button" onClick={onClose} className="p-1.5 rounded-lg hover:bg-white/10 text-white/60 hover:text-white" aria-label="Close">
            <X size={16}/>
          </button>
        </div>

        {/* Body */}
        <div className="px-5 py-5 space-y-4 overflow-y-auto">
          {step === 1 && (<>
              <div className="text-xs text-white/60">
                Choose the encode profile. Both produce an .mp4 file you can
                download.
              </div>
              <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                <button type="button" onClick={() => setKind('mp4_plain')} className={[
                'text-left rounded-xl border p-4 transition-all',
                kind === 'mp4_plain'
                    ? 'border-cyan-400/60 bg-cyan-400/5'
                    : 'border-white/10 hover:border-white/30 bg-white/5',
            ].join(' ')} aria-pressed={kind === 'mp4_plain'}>
                  <div className="flex items-center gap-2 font-medium">
                    <Film size={16} className="text-cyan-300"/>
                    MP4 (edit / share)
                  </div>
                  <div className="mt-2 text-[11px] leading-relaxed text-white/60">
                    Balanced size and quality. Plays on any device. Good for
                    re-editing later in another tool.
                  </div>
                  <div className="mt-2 text-[10px] text-white/40">
                    H.264 · CRF 20 · AAC 128 kbps
                  </div>
                </button>

                <button type="button" onClick={() => setKind('mp4_youtube')} className={[
                'text-left rounded-xl border p-4 transition-all',
                kind === 'mp4_youtube'
                    ? 'border-red-400/60 bg-red-400/5'
                    : 'border-white/10 hover:border-white/30 bg-white/5',
            ].join(' ')} aria-pressed={kind === 'mp4_youtube'}>
                  <div className="flex items-center gap-2 font-medium">
                    <Youtube size={16} className="text-red-400"/>
                    MP4 for YouTube
                  </div>
                  <div className="mt-2 text-[11px] leading-relaxed text-white/60">
                    Matches YouTube's recommended upload spec. Faststart layout
                    means YouTube ingests without re-mux.
                  </div>
                  <div className="mt-2 text-[10px] text-white/40">
                    H.264 high · CRF 18 · AAC 192 kbps · +faststart
                  </div>
                </button>
              </div>

              {disabledReason ? (<div className="flex items-start gap-2 rounded-lg border border-amber-400/30 bg-amber-400/10 p-3 text-[12px] text-amber-200">
                  <AlertTriangle size={14} className="mt-0.5 flex-shrink-0"/>
                  <span>{disabledReason}</span>
                </div>) : null}

              <div className="flex justify-end gap-2 pt-2">
                <button type="button" onClick={onClose} className="px-3 py-1.5 rounded-lg text-sm text-white/70 hover:bg-white/10">
                  Cancel
                </button>
                <button type="button" onClick={() => setStep(2)} disabled={submitDisabled} className={[
                'px-3 py-1.5 rounded-lg text-sm font-medium',
                submitDisabled
                    ? 'bg-white/10 text-white/40 cursor-not-allowed'
                    : 'bg-white text-black hover:bg-white/90',
            ].join(' ')}>
                  Next: Voiceover
                </button>
              </div>
            </>)}

          {step === 2 && (<>
              <div className="flex items-center gap-2 text-xs text-white/60">
                <Mic size={14} className="text-cyan-300"/>
                Piper runs fully in-browser via WebAssembly. First use
                downloads a voice model (~20 MB, cached).
              </div>

              <div className="space-y-3">
                <label className="block text-[11px] uppercase tracking-wide text-white/50">
                  Piper voice
                </label>
                <select value={voiceId} onChange={(e) => setVoiceId(e.target.value)} className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-sm text-white/90 focus:outline-none focus:border-white/30">
                  {voices.map((v) => (<option key={v.id} value={v.id}>
                      {v.name} — {v.lang} · {v.gender} · {v.quality}
                    </option>))}
                </select>
              </div>

              <div className="grid grid-cols-2 gap-4">
                <div>
                  <div className="flex items-center justify-between text-[11px] uppercase tracking-wide text-white/50">
                    <span>Rate</span>
                    <span className="font-mono text-white/70">{rate.toFixed(2)}</span>
                  </div>
                  <input type="range" min={0.5} max={2.0} step={0.05} value={rate} onChange={(e) => setRate(Number(e.target.value))} className="w-full accent-cyan-400"/>
                </div>
                <div>
                  <div className="flex items-center justify-between text-[11px] uppercase tracking-wide text-white/50">
                    <span>Pitch</span>
                    <span className="font-mono text-white/70">{pitch.toFixed(2)}</span>
                  </div>
                  <input type="range" min={0.5} max={2.0} step={0.05} value={pitch} onChange={(e) => setPitch(Number(e.target.value))} className="w-full accent-cyan-400"/>
                </div>
              </div>

              <div className="flex items-center gap-2">
                <button type="button" onClick={handlePreview} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm bg-white/10 hover:bg-white/15 border border-white/10">
                  {previewing ? <Square size={14}/> : <Play size={14}/>}
                  {previewing ? 'Stop preview' : 'Preview voice'}
                </button>
                <span className="text-[11px] text-white/40 truncate">{PREVIEW_TEXT}</span>
              </div>

              {previewError ? (<div className="text-[11px] text-red-300/80">{previewError}</div>) : null}

              <label className="flex items-center gap-2 text-sm text-white/80 cursor-pointer">
                <input type="checkbox" checked={subtitlesBurnIn} onChange={(e) => setSubtitlesBurnIn(e.target.checked)} className="accent-cyan-400"/>
                Burn subtitles into the video (from scene narration text)
              </label>

              <label className="flex items-center gap-2 text-sm text-white/80 cursor-pointer">
                <input type="checkbox" checked={keepExisting} onChange={(e) => setKeepExisting(e.target.checked)} className="accent-cyan-400"/>
                Keep existing narration audio where present
                <span className="text-[11px] text-white/40">(skip scenes that already have a take)</span>
              </label>

              <div className="text-[11px] text-white/40">
                {scenesToSynthesize.length === 0
                ? 'No scenes have narration text — the export will have silent audio.'
                : `${scenesToSynthesize.length} of ${scenes.length} scene${scenes.length === 1 ? '' : 's'} will be synthesized with the selected voice.`}
              </div>

              <div className="flex justify-between gap-2 pt-2">
                <button type="button" onClick={() => setStep(1)} className="px-3 py-1.5 rounded-lg text-sm text-white/70 hover:bg-white/10">
                  Back
                </button>
                <button type="button" onClick={handleGenerate} className="px-3 py-1.5 rounded-lg text-sm font-medium bg-white text-black hover:bg-white/90">
                  {scenesToSynthesize.length === 0 ? 'Skip & render' : 'Generate narration'}
                </button>
              </div>
            </>)}

          {step === 3 && (<>
              <div className="flex items-center gap-2 text-white/80">
                <Loader2 size={16} className="animate-spin"/>
                <span className="text-sm">Synthesizing narration…</span>
              </div>
              <div className="h-2 rounded-full bg-white/10 overflow-hidden">
                <div className="h-full bg-cyan-400 transition-all" style={{
                width: `${genTotal === 0 ? 0 : Math.round((genIdx / genTotal) * 100)}%`,
            }}/>
              </div>
              <div className="text-[11px] text-white/40 text-center">
                {genIdx} of {genTotal} scenes
              </div>

              {genError ? (<div className="space-y-3">
                  <div className="flex items-start gap-2 rounded-lg border border-red-400/30 bg-red-400/10 p-3 text-[12px] text-red-200">
                    <AlertTriangle size={14} className="mt-0.5 flex-shrink-0"/>
                    <div>
                      <div className="font-medium mb-0.5">Narration synthesis failed</div>
                      <div className="text-red-300/80 break-words">{genError}</div>
                    </div>
                  </div>
                  <button type="button" onClick={() => setStep(2)} className="w-full px-3 py-1.5 rounded-lg text-sm bg-white/10 hover:bg-white/15">
                    Back to voiceover
                  </button>
                </div>) : null}
            </>)}

          {step === 4 && (<>
              {job?.status === 'done' ? (<div className="space-y-4">
                  <div className="flex items-center gap-2 text-emerald-300">
                    <CheckCircle2 size={18}/>
                    <span className="font-medium">Render complete</span>
                  </div>
                  <a href={downloadUrl} download className="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-xl bg-white text-black font-medium hover:bg-white/90">
                    <Download size={16}/>
                    Download .mp4
                  </a>
                  <div className="text-[11px] text-white/40 text-center">
                    {job.kind === 'mp4_youtube' ? 'YouTube-optimized' : 'Plain MP4'}
                    {subtitlesBurnIn ? ' · subtitles burned in' : ''}
                  </div>
                </div>) : job?.status === 'error' ? (<div className="space-y-3">
                  <div className="flex items-start gap-2 rounded-lg border border-red-400/30 bg-red-400/10 p-3 text-[12px] text-red-200">
                    <AlertTriangle size={14} className="mt-0.5 flex-shrink-0"/>
                    <div>
                      <div className="font-medium mb-0.5">Render failed</div>
                      <div className="text-red-300/80 break-words">
                        {job.error || submitError || 'Unknown error'}
                      </div>
                    </div>
                  </div>
                  <button type="button" onClick={() => setStep(2)} className="w-full px-3 py-1.5 rounded-lg text-sm bg-white/10 hover:bg-white/15">
                    Back to voiceover
                  </button>
                </div>) : (<div className="space-y-3">
                  <div className="flex items-center gap-2 text-white/80">
                    <Loader2 size={16} className="animate-spin"/>
                    <span className="text-sm">
                      {job?.status === 'queued' ? 'Queued…' : 'Rendering…'}
                    </span>
                  </div>
                  <div className="h-2 rounded-full bg-white/10 overflow-hidden">
                    <div className="h-full bg-cyan-400 transition-all" style={{
                    width: `${Math.max(2, Math.min(100, job?.progress || 0))}%`,
                }}/>
                  </div>
                  <div className="text-[11px] text-white/40 text-center">
                    {Math.round(job?.progress || 0)}% — this can take a minute or two
                  </div>
                </div>)}

              <div className="flex justify-end pt-2">
                <button type="button" onClick={onClose} className="px-3 py-1.5 rounded-lg text-sm text-white/70 hover:bg-white/10">
                  Close
                </button>
              </div>
            </>)}
        </div>
      </div>
    </div>);
}
