// Govee tab — one-click "Find my lights", manual add/edit/delete,
// BT-MAC autoformatting, and inline "?" help popovers (KL-134 Alpine
// conversion).
//
// The scan → auto-add flow is POST /api/govee/autopopulate: the server
// runs the multicast scan, adds every previously-unseen device (keyed
// on BT MAC), and optionally flips the enable toggle on. Anything
// manual still routes through /api/govee/devices.
//
// Status polling (KL-36) hits /api/govee/status every 10s while the
// tab is active. A MutationObserver on the panel's `hidden` attribute
// starts/stops the loop so the backend doesn't run scan_lan in the
// background when the user is on a different tab.

import {pushToast} from './toasts.js';
import {registerNotificationAction} from '../js/notifications.js';

const STATUS_POLL_MS = 10_000;

let _statusPollTimer = null;
let _statusInflight = false;
let _panelObserver = null;

// Strip non-hex, uppercase, re-insert colons every 2 chars, cap at 17
// characters (6 octets + 5 colons). Pure helper.
function formatBtMac(value) {
    const hex = String(value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().slice(0, 12);
    const pairs = [];
    for (let i = 0; i < hex.length; i += 2) {
        pairs.push(hex.slice(i, i + 2));
    }
    return pairs.join(':');
}

function formatAgo(s) {
    if (s < 60) return `${s}s ago`;
    const m = Math.floor(s / 60);
    if (m < 60) return `${m}m ago`;
    const h = Math.floor(m / 60);
    return `${h}h ago`;
}

function deviceKey(d) {
    return d.ble_id || d.sku || '';
}

function storeDefinition() {
    return {
        // --- Server snapshot ---
        enabled: false,
        enabledStatusText: 'Loading…',
        devices: [],

        // --- Status polling ---
        statusByKey: {},
        statusInflight: false,

        // --- Busy flags for user-initiated actions (KL-169) ---
        // findBusy gates the toolbar + hero "Find my lights" buttons
        // while /api/govee/autopopulate is in flight. recheckBusy gates
        // the toolbar Recheck button while a *manual* status poll runs;
        // the auto-poll timer doesn't touch this flag (statusInflight
        // does that), so the button spinner doesn't blink every tick.
        findBusy: false,
        recheckBusy: false,

        // --- Add/Edit modal ---
        modalShown: false,
        editingIndex: null,
        form: {sku: '', name: '', ble_id: ''},
        formError: '',
        get modalTitle() {
            return this.editingIndex === null ? 'Add Govee device' : 'Edit Govee device';
        },

        // --- Empty-hero status surface (Find my lights from hero) ---
        emptyHeroStatusText: '',
        emptyHeroStatusKind: '',

        // --- LAN-Control callout ---
        lanExpanded: false,
        get lanToggleLabel() {
            return this.lanExpanded ? 'Hide' : 'Show me how';
        },

        // --- Help bubbles (single-open) ---
        openHelpId: '',

        // --- Computed visibility ---
        get showDeviceList() { return this.devices.length > 0; },
        get showEmptyHero() { return this.devices.length === 0; },
        get showToolbar() { return this.devices.length > 0; },

        // Lookup status for a device. Returns {status, title, label}.
        // 'unknown' (gray pulsing) until the first poll lands; then
        // 'online' (green) or 'offline' (gray) per server response.
        statusFor(d) {
            const key = deviceKey(d);
            const s = this.statusByKey[key];
            if (!s) {
                return {
                    status: 'unknown',
                    title: 'Checking…',
                    label: 'checking',
                };
            }
            if (s.online) {
                const ipPart = s.ip ? ` — ${s.ip}` : '';
                return {
                    status: 'online',
                    title: `Online${ipPart}`,
                    label: `online${ipPart}`,
                };
            }
            if (s.last_seen_seconds_ago == null) {
                return {
                    status: 'offline',
                    title: 'Offline — never seen since launch',
                    label: 'offline, never seen since launch',
                };
            }
            const ago = formatAgo(s.last_seen_seconds_ago);
            return {
                status: 'offline',
                title: `Offline — last seen ${ago}`,
                label: `offline, last seen ${ago}`,
            };
        },

        applyState(data) {
            this.enabled = !!data.enabled;
            // Hot-reload landed in KL-32 — toggle takes effect live.
            this.enabledStatusText = data.enabled ? 'Active.' : 'Disabled.';
            this.devices = data.devices || [];
        },

        async refresh() {
            try {
                const r = await fetch('/api/govee');
                const data = await r.json();
                this.applyState(data);
                // Kick the status poller if the tab is currently active.
                const panel = document.querySelector('[data-tab-panel="govee"]');
                if (panel && !panel.hidden) startStatusPolling();
            } catch (e) {
                this.devices = [];
                this.enabledStatusText = 'Failed to load.';
            }
        },

        async setEnabled(enabled) {
            try {
                const r = await fetch('/api/govee/enabled', {
                    method: 'PUT',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled}),
                });
                const data = await r.json().catch(() => ({}));
                if (data.live ?? true) {
                    this.enabledStatusText = enabled ? 'Active.' : 'Disabled.';
                } else {
                    this.enabledStatusText = enabled
                        ? 'Will be active after restart.'
                        : 'Will be disabled after restart.';
                }
                // Refresh the device list — disabling tears the controller
                // down server-side, so the rows shouldn't pretend to be
                // online. Status dots already poll independently.
                await this.refresh();
            } catch (e) { /* noop */ }
        },

        openAdd() {
            this.editingIndex = null;
            this.form = {sku: '', name: '', ble_id: ''};
            this.formError = '';
            this.modalShown = true;
            setTimeout(() => document.getElementById('govee-device-sku')?.focus(), 0);
        },

        openEdit(index) {
            this.editingIndex = index;
            const d = this.devices[index] || {};
            this.form = {
                sku: d.sku || '',
                name: d.name || '',
                ble_id: d.ble_id || '',
            };
            this.formError = '';
            this.modalShown = true;
            setTimeout(() => document.getElementById('govee-device-sku')?.focus(), 0);
        },

        closeModal() {
            this.modalShown = false;
            this.editingIndex = null;
            this.openHelpId = '';
        },

        async saveDevice() {
            const sku = (this.form.sku || '').trim();
            const name = (this.form.name || '').trim() || sku;
            const ble_id = (this.form.ble_id || '').trim();
            if (!sku) {
                this.formError = 'SKU is required.';
                return;
            }
            const body = {sku, name};
            if (ble_id) body.ble_id = ble_id;
            const url = this.editingIndex === null
                ? '/api/govee/devices'
                : `/api/govee/devices/${this.editingIndex}`;
            const method = this.editingIndex === null ? 'POST' : 'PUT';
            try {
                const r = await fetch(url, {
                    method,
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(body),
                });
                const data = await r.json();
                if (!r.ok) {
                    this.formError = data.error || 'Save failed.';
                    return;
                }
                this.closeModal();
                await this.refresh();
            } catch (e) {
                this.formError = `Save failed: ${e}`;
            }
        },

        async deleteDevice(index) {
            const d = this.devices[index];
            if (!d) return;
            if (!confirm(`Delete ${d.name} (${d.sku})?`)) return;
            try {
                await fetch(`/api/govee/devices/${index}`, {method: 'DELETE'});
                await this.refresh();
            } catch (e) { /* noop */ }
        },

        // One-click setup. Two surfaces: 'hero' (empty-state CTA, with
        // an inline status line below) and 'toolbar' (compact button
        // up top, no inline status). Toast surfaces for both.
        async findMyLights(surface = 'toolbar') {
            if (this.findBusy) return;
            const setHero = (text, kind = '') => {
                if (surface !== 'hero') return;
                this.emptyHeroStatusText = text || '';
                this.emptyHeroStatusKind = kind || '';
            };
            this.findBusy = true;
            setHero('Scanning your network…', 'is-busy');
            try {
                const r = await fetch('/api/govee/autopopulate', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enable: true}),
                });
                const data = await r.json();
                if (!r.ok) {
                    const msg = data.error || 'Scan failed.';
                    setHero(msg, 'is-error');
                    pushToast({
                        key: 'govee-find-error',
                        level: 'error',
                        title: 'Govee scan failed',
                        detail: msg,
                    });
                    return;
                }
                this.applyState({enabled: data.enabled, devices: data.devices});
                const {added, skipped} = data;
                if (added === 0 && skipped === 0) {
                    setHero("No lights responded. Double-check LAN Control is on in the Govee Home app, then try again.", 'is-error');
                    pushToast({
                        key: 'govee-find-result',
                        level: 'warn',
                        title: 'No Govee lights found',
                        detail: 'Toggle LAN Control on in the Govee Home app, then try again.',
                    });
                    return;
                }
                setHero('');
                let msg;
                if (added) {
                    const base = `Added ${added} light${added === 1 ? '' : 's'}${skipped ? ` (${skipped} already known)` : ''}.`;
                    msg = data.live
                        ? `${base} They're ready to go.`
                        : `${base} Restart Conductør to take effect.`;
                } else {
                    msg = `All ${skipped} light${skipped === 1 ? '' : 's'} already configured.`;
                }
                pushToast({
                    key: 'govee-find-result',
                    level: 'info',
                    title: 'Govee setup complete',
                    detail: msg,
                });
            } catch (e) {
                setHero(`Scan failed: ${e}`, 'is-error');
            } finally {
                this.findBusy = false;
            }
        },

        // Manual recheck wrapper — drives the Recheck button's busy
        // state (label flip + spinner + disabled) while delegating the
        // actual work to pollStatusOnce. Auto-poll calls pollStatusOnce
        // directly so the button doesn't blink between ticks.
        async manualRecheck() {
            if (this.recheckBusy) return;
            this.recheckBusy = true;
            try {
                await this.pollStatusOnce();
            } finally {
                this.recheckBusy = false;
            }
        },

        async pollStatusOnce() {
            if (_statusInflight) return;
            if (!this.devices.length) return;
            _statusInflight = true;
            this.statusInflight = true;
            try {
                const r = await fetch('/api/govee/status');
                if (!r.ok) return;
                const data = await r.json();
                const map = {};
                for (const s of (data.devices || [])) {
                    const key = s.ble_id || s.sku || '';
                    if (key) map[key] = s;
                }
                this.statusByKey = map;
            } catch (_) { /* transient — keep previous map */ }
            finally {
                _statusInflight = false;
                this.statusInflight = false;
            }
        },

        toggleLan() {
            this.lanExpanded = !this.lanExpanded;
        },

        toggleHelp(id) {
            this.openHelpId = (this.openHelpId === id) ? '' : id;
        },

        // Bound on the BLE input's x-on:input. Formats and writes back
        // to form state AND mutates the input element directly so the
        // user sees the formatting as they type.
        formatBleField(el) {
            const formatted = formatBtMac(el.value);
            this.form.ble_id = formatted;
            if (el.value !== formatted) el.value = formatted;
        },
    };
}

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

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

function startStatusPolling() {
    if (_statusPollTimer) return;
    const tick = async () => {
        const store = ensureStore();
        if (store) await store.pollStatusOnce();
    };
    tick();  // immediate
    _statusPollTimer = setInterval(tick, STATUS_POLL_MS);
}

function stopStatusPolling() {
    if (_statusPollTimer) {
        clearInterval(_statusPollTimer);
        _statusPollTimer = null;
    }
}

// First-run toast action: server already auto-populated config and
// flipped govee.enabled, so we just switch to the Govee tab so the
// user can see what was saved. Registered at module load — available
// regardless of whether initGovee() has been called yet.
registerNotificationAction('open_govee_tab', () => {
    try {
        if (typeof window.conductorActivateTab === 'function') {
            window.conductorActivateTab('govee');
        }
    } catch (_) {}
});

export function initGovee() {
    const panel = document.querySelector('[data-tab-panel="govee"]');
    if (!panel) return;

    let store = ensureStore();
    if (!store) {
        document.addEventListener('alpine:init', () => {
            ensureStore()?.refresh();
        }, {once: true});
    } else {
        store.refresh();
    }

    // Visibility-aware polling. The tabs store toggles the panel's
    // hidden attribute via :hidden binding; observe that to start/stop
    // polling. (Same pattern as MIDI tab.)
    if (_panelObserver) _panelObserver.disconnect();
    _panelObserver = new MutationObserver(() => {
        if (panel.hidden) stopStatusPolling();
        else startStatusPolling();
    });
    _panelObserver.observe(panel, {attributes: true, attributeFilter: ['hidden']});
}
