import { CueStateMachine } from "./cues.js";
import { createSessionId } from "./worker-protocol.js";
import { ResourceGovernor } from "./resource-governor.js";
import { ProviderError } from "./providers.js";
export class LocalCaptionSession {
    translator;
    hooks;
    now;
    cues = new CueStateMachine();
    controllers = new Map();
    submitted = new Set();
    retryable = new Map();
    retryableSince = new Map();
    failedTranslations = new Set();
    automaticOutputRetries = new Set();
    governor;
    running = false;
    startupComplete = false;
    translatorReady = false;
    translatorPreparing;
    settings;
    cueSequence = 0;
    transcriber;
    resolvedSourceLanguage;
    sessionId = createSessionId();
    constructor(translator, hooks, now = Date.now) {
        this.translator = translator;
        this.hooks = hooks;
        this.now = now;
        this.governor = new ResourceGovernor({
            now,
            onReset: () => this.transcriber?.reset?.("overload"),
            onOverloaded: () => this.handleAudioOverloaded(),
        });
    }
    async start(settings) {
        this.sessionId = createSessionId();
        this.governor.reset();
        this.running = true;
        this.startupComplete = false;
        this.settings = settings;
        this.hooks.status("starting");
        if (!settings.autoTranslate) {
            this.hooks.status("ready");
            this.startupComplete = true;
            return;
        }
        // Caption collection must become usable before (and independently from)
        // translation.  Checking a large translation bundle here made the overlay
        // show a stale "download model" warning before the first subtitle existed.
        // The actual cue path checks and prepares the translator lazily instead.
        this.hooks.status("ready");
        this.startupComplete = true;
    }
    updateSettings(settings) {
        const wasAutoTranslate = this.settings?.autoTranslate === true;
        this.settings = settings;
        if (wasAutoTranslate && !settings.autoTranslate)
            for (const controller of this.controllers.values())
                controller.abort();
    }
    submitNativeCue(text, startMs, endMs, settings) {
        if (!this.running || !text.trim())
            return;
        const currentSettings = this.settings ?? settings;
        const sourceText = text.trim();
        const signature = `${startMs}:${endMs}:${sourceText}`;
        if (this.submitted.has(signature))
            return;
        this.submitted.add(signature);
        const cue = { cueId: `native-${++this.cueSequence}`, revision: 1, state: "STABLE_SOURCE", sourceText, startMs, endMs };
        const accepted = this.cues.apply(cue);
        if (!accepted)
            return;
        this.hooks.cue(accepted);
        if (currentSettings.autoTranslate) {
            this.rememberRetryable(accepted);
            void this.translate(accepted, currentSettings, "automatic");
        }
    }
    async startAudio(settings, transcriber) {
        this.sessionId = createSessionId();
        this.governor.reset();
        this.running = true;
        this.startupComplete = false;
        this.settings = settings;
        this.transcriber = transcriber;
        this.resolvedSourceLanguage = settings.sourceLanguage === "auto" ? undefined : settings.sourceLanguage;
        this.hooks.status("starting");
        const availability = await transcriber.availability({ sourceLanguage: settings.sourceLanguage, profile: settings.localAsrProfile });
        if (!this.running)
            return;
        if (availability === "corrupt") {
            this.hooks.status("asr_cache_corrupt");
            return;
        }
        if (availability !== "available") {
            this.hooks.status(availability === "unconfigured" ? "audio_unconfigured" : "audio_unavailable");
            return;
        }
        // The execution path is decided inside the transcriber from the model store
        // (GPU machines pick webgpu automatically), so none is passed here.
        await transcriber.start?.({ sourceLanguage: settings.sourceLanguage, profile: settings.localAsrProfile, onLanguage: (language) => { this.resolvedSourceLanguage = language; }, onPartial: (text, startMs, endMs) => this.submitPartial(text, startMs, endMs), onStable: (text, startMs, endMs) => this.submitAudioCue(text, startMs, endMs), onError: (error) => this.hooks.status(error.code), onMetrics: (metrics) => this.governor.updateMetrics(metrics) });
        if (!this.running) {
            transcriber.stop();
            this.transcriber = undefined;
            return;
        }
        if (!this.settings?.autoTranslate) {
            this.hooks.status("ready");
            this.startupComplete = true;
            return;
        }
        // Keep audio startup independent from the much larger translation model.
        // Translation is checked and prepared lazily when the first stable cue arrives.
        this.hooks.status("ready");
        this.startupComplete = true;
    }
    pushAudio(frame) { if (this.running)
        this.transcriber?.push(frame); }
    stop(reportStatus = true) {
        this.running = false;
        this.startupComplete = false;
        for (const controller of this.controllers.values())
            controller.abort();
        this.controllers.clear();
        this.governor.reset();
        this.transcriber?.stop();
        this.transcriber = undefined;
        if (reportStatus)
            this.hooks.status("stopped");
    }
    retryTranslation(cueId) {
        if (!this.running || !this.settings?.autoTranslate || this.controllers.has(cueId))
            return false;
        const cue = this.retryable.get(cueId) ?? this.cues.pendingCue(cueId);
        if (!cue)
            return false;
        void this.translate(cue, this.settings, "retry");
        return true;
    }
    translationUnavailable() {
        this.translatorReady = false;
        for (const controller of this.controllers.values())
            controller.abort();
        this.controllers.clear();
        for (const cueId of this.retryable.keys())
            this.hooks.translation?.(cueId, "model_unconfigured");
        if (this.running)
            this.hooks.status("ready");
    }
    submitPartial(text, startMs, endMs) {
        if (!this.running || !text.trim())
            return;
        this.hooks.cue({ cueId: `audio-partial-${startMs}`, revision: 1, state: "ACTIVE_PARTIAL", sourceText: text.trim(), startMs, endMs });
    }
    submitAudioCue(text, startMs, endMs) {
        const settings = this.settings;
        if (!this.running || !settings || !text.trim())
            return;
        const cue = { cueId: `audio-${++this.cueSequence}`, revision: 1, state: "STABLE_SOURCE", sourceText: text.trim(), startMs, endMs };
        const accepted = this.cues.apply(cue);
        if (!accepted)
            return;
        this.hooks.cue(accepted);
        if (settings.autoTranslate) {
            this.rememberRetryable(accepted);
            const resolved = this.resolvedSourceLanguage;
            if (resolved)
                void this.translate(accepted, { ...settings, sourceLanguage: resolved }, "automatic");
            else
                this.hooks.status("language_unsupported");
        }
    }
    async translate(source, settings, priority) {
        const controller = new AbortController();
        this.controllers.set(source.cueId, controller);
        this.hooks.translation?.(source.cueId, "pending");
        const sessionId = this.sessionId;
        try {
            const translationText = await this.governor.enqueue({
                cueId: source.cueId,
                sourceRevision: source.revision,
                sessionId,
                priority,
                signal: controller.signal,
                run: async (signal) => {
                    if (!(await this.ensureTranslator(settings)))
                        throw new ProviderError("translator_unconfigured");
                    return this.translator.translate(source.sourceText, settings.sourceLanguage, settings.targetLanguage, signal, priority, { cueId: source.cueId, sourceRevision: source.revision, sessionId });
                },
            });
            if (!this.running || this.sessionId !== sessionId || controller.signal.aborted || !translationText || this.cues.pendingCue(source.cueId)?.revision !== source.revision)
                return;
            const bilingual = this.cues.apply({ ...source, revision: source.revision + 1, state: "STABLE_BILINGUAL", translationText });
            if (bilingual) {
                this.retryable.delete(source.cueId);
                this.retryableSince.delete(source.cueId);
                const recovered = this.failedTranslations.delete(source.cueId);
                this.hooks.cue(bilingual);
                if (recovered)
                    this.reportTranslationHealth();
            }
        }
        catch (error) {
            if (!this.running || this.sessionId !== sessionId || controller.signal.aborted)
                return;
            if (error instanceof ProviderError) {
                if (error.code === "cancelled")
                    return;
                if (this.shouldRetryOutput(source, error, priority)) {
                    this.automaticOutputRetries.add(source.cueId);
                    queueMicrotask(() => {
                        if (this.running && !this.controllers.has(source.cueId))
                            void this.translate(source, settings, "automatic");
                    });
                    return;
                }
                this.hooks.translation?.(source.cueId, translationCueStatus(error.code));
                if (isSessionTranslationFailure(error.code) || error.code === "translator_failed")
                    this.hooks.status(error.code);
                else {
                    this.failedTranslations.add(source.cueId);
                    this.reportTranslationHealth();
                }
                return;
            }
            this.hooks.translation?.(source.cueId, "failed");
            this.failedTranslations.add(source.cueId);
            this.reportTranslationHealth();
        }
        finally {
            if (this.controllers.get(source.cueId) === controller)
                this.controllers.delete(source.cueId);
        }
    }
    handleAudioOverloaded() {
        if (!this.running)
            return;
        this.hooks.status("audio_overloaded");
        // The background treats this as terminal for the current tab and removes
        // the overlay.  Stop locally as well so capture and translation workers do
        // not keep consuming resources after the user-visible failure.
        this.stop(false);
    }
    async ensureTranslator(settings) {
        if (this.translatorReady)
            return true;
        if (this.translatorPreparing)
            return this.translatorPreparing;
        this.translatorPreparing = (async () => {
            const availability = await this.translator.availability(settings.sourceLanguage, settings.targetLanguage);
            if (availability === "corrupt")
                throw new ProviderError("translator_cache_corrupt");
            if (availability !== "available")
                return false;
            try {
                await this.translator.prepare?.(settings.sourceLanguage, settings.targetLanguage);
                this.translatorReady = true;
                return true;
            }
            catch (error) {
                throw error instanceof ProviderError ? error : new ProviderError("translator_failed");
            }
        })();
        try {
            return await this.translatorPreparing;
        }
        finally {
            this.translatorPreparing = undefined;
        }
    }
    rememberRetryable(cue) {
        this.retryable.set(cue.cueId, cue);
        this.retryableSince.set(cue.cueId, this.now());
        while (this.retryable.size > 100) {
            const cueId = this.retryable.keys().next().value;
            this.retryable.delete(cueId);
            this.retryableSince.delete(cueId);
            this.failedTranslations.delete(cueId);
            this.automaticOutputRetries.delete(cueId);
        }
    }
    shouldRetryOutput(source, error, priority) {
        const receivedAt = this.retryableSince.get(source.cueId);
        return error.code === "translator_output_invalid" && priority === "automatic" && receivedAt !== undefined && !this.automaticOutputRetries.has(source.cueId) && this.now() - receivedAt <= 20_000;
    }
    reportTranslationHealth() {
        if (!this.running || !this.startupComplete)
            return;
        this.hooks.status(this.failedTranslations.size ? "translator_partial_failure" : "ready");
    }
}
function translationCueStatus(code) {
    if (code === "translator_unconfigured" || code === "translator_unavailable" || code === "language_unsupported")
        return "model_unconfigured";
    if (code === "translator_overloaded")
        return "overloaded";
    return "failed";
}
function isSessionTranslationFailure(code) {
    return code === "translator_unavailable" || code === "translator_unconfigured" || code === "language_unsupported" || code === "translator_load_failed" || code === "translator_cache_corrupt";
}
