// Drives a single active transcription: decode -> run the pipeline on-device ->
// save to the local library. Holds live progress + the partial transcript so
// the UI can "fill in" as chunks complete, plus an object URL for playback.
import { Platform } from 'react-native';
import { create } from 'zustand';

import { getDecoder, type AudioFileInput } from '@/lib/audio';
import { getRepo } from '@/lib/db';
import { DEFAULT_MODEL } from '@/lib/models/catalog';
import { getEngine } from '@/lib/transcription';
import { transcribe } from '@/lib/transcription/pipeline';
import type { ModelId, Segment } from '@/lib/types';
import { useEmbedding } from './embeddingStore';

type Status = 'idle' | 'loading' | 'transcribing' | 'done' | 'error';

interface StartOptions {
  title?: string;
  language?: string;
  translate?: boolean;
  courseId?: string | null;
  lectureDate?: number;
}

interface TranscribeState {
  status: Status;
  stage?: 'loading' | 'transcribing';
  progress: number;
  partial: Segment[];
  error?: string;
  modelId: ModelId;
  lastTranscriptId?: string;
  /** Playable source for the just-finished audio (object URL on web, uri on native). */
  audioUrl?: string;
  _abort?: AbortController;

  setModel: (m: ModelId) => void;
  start: (input: AudioFileInput, opts?: StartOptions) => Promise<string | undefined>;
  cancel: () => void;
  reset: () => void;
}

function makeAudioUrl(input: AudioFileInput): string | undefined {
  if (Platform.OS === 'web' && input.data) {
    return URL.createObjectURL(new Blob([input.data]));
  }
  return input.uri;
}

export const useTranscribe = create<TranscribeState>((set, get) => ({
  status: 'idle',
  progress: 0,
  partial: [],
  modelId: DEFAULT_MODEL,

  setModel: (m) => set({ modelId: m }),

  start: async (input, opts) => {
    // Clean up any previous object URL.
    const prev = get().audioUrl;
    if (prev && Platform.OS === 'web' && prev.startsWith('blob:')) URL.revokeObjectURL(prev);

    const abort = new AbortController();
    set({
      status: 'loading',
      stage: 'loading',
      progress: 0,
      partial: [],
      error: undefined,
      lastTranscriptId: undefined,
      audioUrl: makeAudioUrl(input),
      _abort: abort,
    });

    const { modelId } = get();
    try {
      const pcm = await getDecoder().decode(input);
      const segments = await transcribe({
        audio: pcm,
        options: { modelId, language: opts?.language, translate: opts?.translate },
        engine: getEngine(),
        signal: abort.signal,
        onProgress: (p) =>
          set({ stage: p.stage, progress: p.progress, partial: p.partial, status: p.stage }),
      });

      const durationSec = pcm.samples.length / pcm.sampleRate;
      const saved = await getRepo().create({
        title: opts?.title?.trim() || defaultTitle(),
        durationSec,
        modelId,
        language: opts?.language,
        segments,
        courseId: opts?.courseId ?? null,
        ...(opts?.lectureDate !== undefined ? { lectureDate: opts.lectureDate } : {}),
      });

      // Persist the source audio so it can be replayed after a reload. Best-effort:
      // a quota/unsupported failure must not lose the transcript itself.
      try {
        await getRepo().putMedia(saved.id, {
          data: input.data,
          uri: input.uri,
          mime: guessMime(input.name),
        });
      } catch (e) {
        console.warn('[wisp] could not persist source audio:', e);
      }

      // Index the new transcript for semantic search in the background. Lazy
      // model load happens inside embedOne; this must never block or fail the
      // transcription itself, so it's fire-and-forget with a swallowed error.
      void useEmbedding.getState().embedOne(saved.id).catch(() => {});

      set({ status: 'done', progress: 1, partial: segments, lastTranscriptId: saved.id });
      return saved.id;
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') {
        set({ status: 'idle', progress: 0 });
        return undefined;
      }
      set({ status: 'error', error: err instanceof Error ? err.message : String(err) });
      return undefined;
    }
  },

  cancel: () => {
    get()._abort?.abort();
  },

  reset: () => {
    const prev = get().audioUrl;
    if (prev && Platform.OS === 'web' && prev.startsWith('blob:')) URL.revokeObjectURL(prev);
    set({ status: 'idle', stage: undefined, progress: 0, partial: [], error: undefined, audioUrl: undefined });
  },
}));

function defaultTitle(): string {
  const d = new Date();
  const pad = (n: number) => String(n).padStart(2, '0');
  return `Transcript ${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

/** Best-effort MIME from a file name extension (for persisted media). */
function guessMime(name?: string): string {
  const ext = (name?.split('.').pop() || '').toLowerCase();
  const map: Record<string, string> = {
    mp3: 'audio/mpeg', m4a: 'audio/mp4', aac: 'audio/aac', wav: 'audio/wav',
    ogg: 'audio/ogg', oga: 'audio/ogg', flac: 'audio/flac', webm: 'audio/webm',
    mp4: 'video/mp4', mov: 'video/quicktime', mkv: 'video/x-matroska',
  };
  return map[ext] || 'application/octet-stream';
}
