// KL-72: First-run audio source wizard (Alpine conversion KL-???).
//
// The biggest first-run failure mode for Conductør is the user hits play
// and sees no signal because they didn't realize their system audio
// isn't routed into the input device the engine listens on. The Settings
// → Audio panel already exposes the picker, but a brand-new user doesn't
// know to look there. This wizard hoists the same picker into a modal on
// first dashboard load so the answer is unmissable.
//
// Coexistence with #mac-audio-modal: if Mac status is `no_loopback` we
// stand down — the BlackHole-onboarding modal handles that flow and
// stacking two first-run modals would be obnoxious. Once the user has
// BlackHole installed (or they're on Win/Linux), this wizard runs.
//
// Modal show/hide + status line + helper line are Alpine-driven via
// Alpine.store('audioWizard'). The 10Hz signal meter and the device
// <select> stay imperative — meter writes are too frequent to flow
// through reactive bindings, and the select is populated by an
// external function (settings.js#loadAudioDevicesInto).

import {socket} from '../js/socket.js';
import {loadAudioDevicesInto} from './audio_input.js';

// KL-104: dismissal flag persists in backend user_prefs.json (was
// browser localStorage; WKWebView on macOS doesn't survive restarts).
// localStorage key kept only as a one-time migration source — if it's
// set on first check, we mirror it to the backend then ignore it forever.
const LEGACY_LS_FLAG_KEY = 'conductor.audio_source_chosen';
// Same threshold initMacAudio uses to call audio "live" — keeps the
// "no signal in 30s" gate consistent with the rest of the app's notion
// of what counts as healthy input.
const SIGNAL_THRESHOLD = 0.05;
const NO_SIGNAL_RESHOW_MS = 30_000;
// KL-170: matches the Settings-tab debounce. Same rationale.
const SAVE_DEBOUNCE_MS = 250;

let selectEl = null;
let meterFill = null;
let meterLabel = null;
let lastSignalSeenAt = 0;
let reshowTimer = null;
let saveTimer = null;

function storeDefinition() {
    return {
        shown: false,
        statusLevel: '',     // '' | 'info' | 'ok' | 'warn' | 'error'
        statusText: '',
        helperContent: '',   // HTML or plain text — rendered via x-html
        helperShown: false,

        setStatus(level, text) {
            this.statusLevel = level || '';
            this.statusText = text || '';
        },

        // Mirror renderAudioHelperLine in settings.js — same copy logic
        // so the user sees consistent guidance whether they hit the
        // picker via the modal or the Settings tab.
        renderHelper(data) {
            if (!data) {
                this.helperShown = false;
                this.helperContent = '';
                return;
            }
            const devices = data.devices || [];
            if (data.os === 'mac') {
                const hasBlackHole = devices.some(d => d.kind === 'blackhole');
                if (hasBlackHole) {
                    this.helperShown = false;
                    this.helperContent = '';
                    return;
                }
                this.helperContent = (
                    'No BlackHole detected. Want Spotify or DJ apps to feed in? '
                    + '<a href="/docs/mac-audio-setup" target="_blank" rel="noopener">Setup guide →</a>'
                );
                this.helperShown = true;
                return;
            }
            if (data.os === 'linux' && data.pipewire_monitor_available) {
                this.helperContent = 'PipeWire is feeding system audio in automatically — you can leave this on Auto-detect.';
                this.helperShown = true;
                return;
            }
            this.helperShown = false;
            this.helperContent = '';
        },

        open(data) {
            this.shown = true;
            this.renderHelper(data);
            this.setStatus('', '');
        },

        close() {
            this.shown = false;
        },

        // KL-170: bound to the <select>'s change event. Optimistic
        // status flip gives instant feedback; debounced POST keeps a
        // quick scroll-through from triggering multiple engine swaps.
        scheduleSave() {
            this.setStatus('info', 'Switching…');
            if (saveTimer) clearTimeout(saveTimer);
            saveTimer = setTimeout(() => this.save(), SAVE_DEBOUNCE_MS);
        },

        async save() {
            if (!selectEl) return;
            const backend = selectEl.value;
            this.setStatus('info', 'Switching…');
            try {
                const resp = await fetch('/api/audio/backend', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({backend}),
                });
                const data = await resp.json();
                if (resp.ok && data.status === 'ok') {
                    this.setStatus('ok', data.device_name
                        ? `✓ Listening on ${data.device_name}. Play something to verify.`
                        : '✓ Saved. Play something to verify.');
                } else {
                    this.setStatus('error', `✗ ${data.error || 'unknown error'}`);
                }
            } catch (e) {
                this.setStatus('error', '✗ Network error.');
            }
        },

        markDoneAndClose() {
            setFlag();
            this.close();
        },

        // "Skip", close-X, and backdrop click all behave the same way:
        // set the flag, close, and schedule a no-signal reshow as a
        // safety net in case the user genuinely needs the picker but
        // dismissed too quickly.
        markSkipAndClose() {
            setFlag();
            this.close();
            scheduleNoSignalReshow();
        },
    };
}

function ensureStore() {
    if (!window.Alpine) return null;
    if (!window.Alpine.store('audioWizard')) {
        window.Alpine.store('audioWizard', storeDefinition());
    }
    return window.Alpine.store('audioWizard');
}

if (window.Alpine) {
    ensureStore();
} else {
    document.addEventListener('alpine:init', ensureStore);
}

function setFlag() {
    fetch('/api/audio/wizard-dismissed', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({dismissed: true}),
    }).catch(() => { /* offline / transient — next dismissal retries */ });
    // Belt-and-suspenders: also write to localStorage so a within-session
    // re-render of the wizard short-circuits without waiting for the GET.
    try { localStorage.setItem(LEGACY_LS_FLAG_KEY, '1'); } catch (e) {}
}

async function hasFlag() {
    // One-time migration: if the legacy localStorage flag is set, mirror
    // it to the backend so users who dismissed pre-KL-104 don't see the
    // wizard again. Subsequent calls take the GET path.
    try {
        if (localStorage.getItem(LEGACY_LS_FLAG_KEY)) {
            setFlag();
            return true;
        }
    } catch (e) { /* private mode — fall through to backend */ }

    try {
        const r = await fetch('/api/audio/wizard-dismissed');
        if (!r.ok) return false;
        const data = await r.json();
        return !!data.dismissed;
    } catch (e) {
        // Network/offline — let the wizard render. Same as today.
        return false;
    }
}

async function refreshDevices() {
    if (!selectEl) return null;
    try {
        return await loadAudioDevicesInto(selectEl);
    } catch (e) {
        ensureStore()?.setStatus('error', 'Could not load device list.');
        return null;
    }
}

// Pre-set the flag for users who already have a working setup so the
// wizard doesn't pop in their face on update day. Two heuristics, OR'd:
// (a) they've explicitly picked a non-auto backend at some point, or
// (b) signal is currently live above threshold (the engine is already
// hearing audio, so they have nothing to set up).
function shouldPreSetFlagFor(data, signalLive) {
    if (!data) return false;
    if (data.backend && data.backend !== 'auto') return true;
    if (signalLive) return true;
    return false;
}

// Schedule a one-shot "if no signal in 30s, reopen" timer. Used after
// the user clicks "I'll set this up later" — gives them the benefit of
// the doubt but reappears once if they never get audio working.
function scheduleNoSignalReshow() {
    if (reshowTimer) return;
    reshowTimer = setTimeout(async () => {
        reshowTimer = null;
        if (Date.now() - lastSignalSeenAt < NO_SIGNAL_RESHOW_MS) return;
        // Re-fetch fresh device data so the OS helper reflects current
        // state (e.g. BlackHole installed in the interim).
        const data = await refreshDevices();
        const store = ensureStore();
        if (store) {
            store.open(data);
            store.setStatus('warn', 'Still no audio after 30 seconds — pick a source so we can hear something.');
        }
    }, NO_SIGNAL_RESHOW_MS);
}

// Other first-run modals that should take precedence — we wait for
// these to be dismissed before opening the wizard so we never stack
// two modals on the user. The welcome modal is now Alpine-driven but
// its `:hidden` binding still toggles the hidden attribute, so the
// el.hidden check works the same as before.
const HIGHER_PRIORITY_MODALS = [
    'welcome-modal',           // permissions blocker
    'mac-audio-modal',         // BlackHole onboarding
    'telemetry-opt-in-modal',  // telemetry consent
];

function aHigherPriorityModalIsOpen() {
    for (const id of HIGHER_PRIORITY_MODALS) {
        const el = document.getElementById(id);
        if (el && !el.hidden) return true;
    }
    return false;
}

// Decide whether to auto-open on first dashboard load. Called once
// after a small startup delay so we have a chance to receive a fresh
// state_update tick before deciding.
async function maybeAutoOpen() {
    if (await hasFlag()) return;

    // Defer to the Mac BlackHole onboarding flow when applicable —
    // settings.js initMacAudio() opens its own modal in the same case
    // and we don't want to stack first-run modals.
    let macHealth = null;
    try {
        macHealth = await fetch('/api/mac-audio/health').then(r => r.json());
    } catch (e) { /* non-mac or transient */ }
    if (macHealth && macHealth.is_macos && macHealth.status === 'no_loopback') {
        // Mac modal will handle this; don't compete. Don't set the flag
        // — once the user resolves BlackHole we still want to give them
        // the picker on the next launch.
        return;
    }

    const data = await refreshDevices();
    const signalLive = lastSignalSeenAt > 0;
    if (shouldPreSetFlagFor(data, signalLive)) {
        setFlag();
        return;
    }

    if (aHigherPriorityModalIsOpen()) {
        setTimeout(maybeAutoOpen, 2000);
        return;
    }

    const store = ensureStore();
    if (store) store.open(data);
}

export function initAudioWizard() {
    const wizardEl = document.getElementById('audio-source-wizard');
    if (!wizardEl) return;
    selectEl = document.getElementById('audio-wizard-select');
    meterFill = document.getElementById('audio-wizard-meter-fill');
    meterLabel = document.getElementById('audio-wizard-meter-label');

    // 10Hz meter — imperative. Mirrors the Settings-tab meter without
    // a second subscription channel. Alpine reactivity isn't worth it
    // here; CSS width/textContent writes at 10Hz are cheap as long as
    // they don't go through a Proxy + scheduler.
    socket.on('state_update', (data) => {
        const s = data && data.smoothed;
        if (!s) return;
        const peak = Math.max(s[0] || 0, s[1] || 0, s[2] || 0);
        if (peak > SIGNAL_THRESHOLD) lastSignalSeenAt = Date.now();
        if (meterFill) meterFill.style.width = `${Math.min(100, peak * 100)}%`;
        if (meterLabel) meterLabel.textContent = `Signal: ${(peak * 100).toFixed(0)}%`;
    });

    // Wait one second before deciding whether to open — gives the first
    // state_update tick a chance to land so the "signal already live"
    // heuristic in shouldPreSetFlagFor() can catch existing healthy
    // setups before the modal flashes.
    setTimeout(maybeAutoOpen, 1000);
}
