// RDM autodiscover for the DMX Fixtures tab (Alpine conversion KL-132).
// Walks the rig via the Phase-1 /api/dmx/discover endpoint, lets the
// user confirm and pick a profile per discovered fixture, then bulk-
// appends to config.yaml via /api/dmx/discover/add.
//
// Disabled while the engine is running (RDM exchanges briefly pause
// fixture output). UI parity with the Govee tab's "Discover Lights" —
// same one-click pattern, same review-then-confirm flow.

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

const ROLES = ['ambient', 'bass_mover', 'mid_wash', 'high_accent'];

let cachedTemplates = null;

async function loadTemplates() {
    if (cachedTemplates) return cachedTemplates;
    try {
        const r = await fetch('/api/fixtures/catalog');
        const data = await r.json();
        cachedTemplates = data.templates || [];
    } catch (e) {
        cachedTemplates = [];
    }
    return cachedTemplates;
}

// Best-effort match: prefer an explicit rdm_model_ids alias hit, then
// fall back to (manufacturer + name) substring match against the RDM-
// reported strings. Returns the matched template id or '' if nothing
// fits — UI then defaults to "Unknown — pick a profile."
function autoMatchTemplate(templates, rdmManufacturer, rdmModel) {
    const mfr = (rdmManufacturer || '').toLowerCase();
    const mdl = (rdmModel || '').toLowerCase();
    if (!mfr && !mdl) return '';

    // Phase 2: explicit alias match wins outright (catalog author has
    // told us "this exact RDM model string maps here").
    for (const t of templates) {
        const aliases = t.rdm_model_ids || [];
        for (const alias of aliases) {
            const a = String(alias).toLowerCase();
            if (a && mdl && (mdl === a || mdl.includes(a) || a.includes(mdl))) {
                return t.id;
            }
        }
    }

    // Fallback: substring match on the catalog name + manufacturer.
    let best = null;
    for (const t of templates) {
        const tMfr = (t.manufacturer || '').toLowerCase();
        const tName = (t.name || '').toLowerCase();
        const mfrHit = mfr && tMfr && (mfr.includes(tMfr) || tMfr.includes(mfr));
        const nameHit = mdl && tName && (mdl.includes(tName) || tName.includes(mdl));
        if (mfrHit && nameHit) return t.id;
        if (mfrHit && !best) best = t.id;
        else if (nameHit && !best) best = t.id;
    }
    return best || '';
}

function defaultRoleFor(template) {
    if (!template) return 'ambient';
    const cat = (template.category || '').toLowerCase();
    if (cat === 'moving_head') return 'bass_mover';
    if (cat === 'par' || cat === 'wash') return 'mid_wash';
    return 'ambient';
}

let _onAdded = null;

function storeDefinition() {
    return {
        // Modal visibility
        shown: false,

        // Pre-scan state
        // KL-147 removed engineRunning gate — the server now auto-pauses
        // output for the RDM exchange and resumes after.
        templates: [],
        roles: ROLES,

        // Scan state
        results: [],
        statusText: '',
        statusBusy: false,
        errorText: '',
        runBtnLabel: 'Start scan',
        showRunBtn: true,
        runBtnDisabled: false,

        // Computed
        get showStatus() { return !!this.statusText; },
        get showError() { return !!this.errorText; },
        get showResults() { return this.results.length > 0; },
        get showEmpty() {
            // Empty state surfaces only after a scan completed with
            // zero results — gated on showRunBtn being false (run
            // hidden after a scan).
            return !this.showRunBtn && this.results.length === 0 && !this.statusText;
        },
        get showAddBtn() { return this.results.length > 0; },
        get keepCount() { return this.results.filter(f => f._keep).length; },
        get addBtnLabel() {
            const n = this.keepCount;
            return n === 1 ? 'Add 1 fixture' : `Add ${n} fixtures`;
        },
        get addBtnDisabled() { return this.keepCount === 0; },

        // KL-162: any kept-and-not-keep-existing row that has at least
        // one overlap candidate. Drives the modal-level destructive
        // warning banner — only appears when an overwrite would happen.
        get hasOverwriteOverlaps() {
            return this.results.some(f =>
                f._keep
                && !f._keep_existing
                && Array.isArray(f.overlap_candidates)
                && f.overlap_candidates.length > 0
            );
        },

        // KL-162: total number of existing fixtures that'll be wiped on
        // submit. For the banner copy ("...will replace 3 existing
        // fixtures.") and the post-add toast.
        get overwriteCount() {
            const indices = new Set();
            for (const f of this.results) {
                if (!f._keep || f._keep_existing) continue;
                for (const c of (f.overlap_candidates || [])) {
                    indices.add(c.index);
                }
            }
            return indices.size;
        },

        reset() {
            this.results = [];
            this.statusText = '';
            this.statusBusy = false;
            this.errorText = '';
            this.runBtnLabel = 'Start scan';
            this.showRunBtn = true;
            this.runBtnDisabled = false;
        },

        async open() {
            this.reset();
            this.templates = await loadTemplates();
            // KL-147: no engine pre-check — the discover endpoint
            // auto-pauses output for the RDM exchange and resumes after.
            this.shown = true;
        },

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

        setStatus(text, busy = false) {
            this.statusText = text || '';
            this.statusBusy = !!busy;
        },

        clearError() {
            this.errorText = '';
        },

        showErrorMsg(msg) {
            this.errorText = msg;
        },

        async runScan() {
            this.clearError();
            this.runBtnDisabled = true;
            this.setStatus('Scanning your rig… this can take 5–15 seconds.', true);
            this.results = [];
            try {
                const r = await fetch('/api/dmx/discover', {method: 'POST'});
                const data = await r.json();
                if (!r.ok) {
                    this.showErrorMsg(data.error || `Scan failed (${r.status})`);
                    this.runBtnDisabled = false;
                    this.setStatus('');
                    return;
                }
                const fixtures = data.fixtures || [];
                this.setStatus('');

                // Annotate each fixture with the auto-match + transient
                // _keep/_template_id/_role fields. Spread into a fresh
                // array so Alpine sees the assignment as a list change.
                this.results = fixtures.map(f => {
                    const matchId = autoMatchTemplate(this.templates, f.manufacturer, f.model);
                    const matchedTpl = matchId ? this.templates.find(t => t.id === matchId) : null;
                    return {
                        ...f,
                        _keep: true,
                        _template_id: matchId,
                        _role: defaultRoleFor(matchedTpl),
                        // KL-162: per-row "Keep existing instead" toggle.
                        // Defaults to false (i.e., overwrite). Server's
                        // overlap_candidates array drives the warning UI.
                        _keep_existing: false,
                    };
                });

                const broadcastNote = data.broadcast_used
                    ? ' (broadcast mode — any RDM-capable node on the LAN)'
                    : ` (queried ${(data.configured_nodes || []).length} configured node${(data.configured_nodes || []).length === 1 ? '' : 's'})`;
                if (this.results.length) {
                    pushToast({
                        key: 'dmx-discover-result',
                        level: 'info',
                        title: 'RDM scan complete',
                        detail: `Found ${this.results.length} fixture${this.results.length === 1 ? '' : 's'}${broadcastNote}.`,
                    });
                }

                this.showRunBtn = false;
            } catch (e) {
                this.showErrorMsg(`Scan failed: ${e}`);
                this.runBtnDisabled = false;
                this.setStatus('');
            }
        },

        async addSelected() {
            this.clearError();
            const keepers = this.results.filter(f => f._keep);
            if (!keepers.length) return;

            // KL-162: collect existing-fixture indices to delete BEFORE
            // appending the new ones. Skip rows where the user toggled
            // "Keep existing" — those discovered fixtures are skipped
            // entirely and their candidates stay untouched.
            const replaceIndexSet = new Set();
            const finalKeepers = [];
            for (const f of keepers) {
                if (f._keep_existing) continue;
                finalKeepers.push(f);
                for (const c of (f.overlap_candidates || [])) {
                    replaceIndexSet.add(c.index);
                }
            }

            const payload = {
                fixtures: finalKeepers.map(f => ({
                    template_id: f._template_id || null,
                    role: f._role || 'ambient',
                    universe: f.universe || 0,
                    address: f.dmx_start_address || 1,
                    uid: f.uid,
                    source_node_ip: f.source_node_ip,
                    name: f.model || f.manufacturer || `RDM ${f.uid}`,
                })),
                replace_indices: [...replaceIndexSet],
            };
            try {
                const r = await fetch('/api/dmx/discover/add', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(payload),
                });
                const data = await r.json();
                if (!r.ok) {
                    this.showErrorMsg(data.error || `Add failed (${r.status})`);
                    return;
                }
                // KL-162: surface the replaced count when overwrites
                // happened, so the user has a clear post-action receipt.
                const addedCount = data.added || 0;
                const replacedCount = data.replaced || 0;
                let detail = `Added ${addedCount} fixture${addedCount === 1 ? '' : 's'}`;
                if (replacedCount > 0) {
                    detail += `, replaced ${replacedCount} existing`;
                }
                detail += ' in config.yaml.';
                if (data.restart_required) {
                    detail += ' Restart Conductør to take effect.';
                }
                pushToast({
                    key: 'dmx-discover-added',
                    level: 'info',
                    title: 'Fixtures added',
                    detail,
                });
                this.close();
                if (typeof _onAdded === 'function') _onAdded();
            } catch (e) {
                this.showErrorMsg(`Add failed: ${e}`);
            }
        },

        // Helpers for per-row template/role labels (called from x-for).
        templateOptionLabel(t) {
            return `${t.name} (${t.manufacturer || '—'}, ${t.channels}ch)`;
        },

        // Helpers for per-row info display.
        fixturePersonalityLabel(f) {
            if (f.personality_count > 1 && f.personality_descriptions?.[f.current_personality - 1]) {
                return ` · ${f.personality_descriptions[f.current_personality - 1]}`;
            }
            return '';
        },
        fixtureSourceLabel(f) {
            return f.source_node_ip ? ` · via ${f.source_node_ip}` : '';
        },
        fixturePatchText(f) {
            return `u${f.universe} · addr ${f.dmx_start_address || '?'} · ${f.dmx_footprint || 0}ch`;
        },
    };
}

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

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

// First-run toast action: server already auto-populated config, so
// we just switch to the Fixtures tab so the user can see what was
// saved. Registered at module load (not inside initDmxDiscover) so
// it's available as soon as toasts are.
registerNotificationAction('open_fixtures_tab', () => {
    try {
        if (typeof window.conductorActivateTab === 'function') {
            window.conductorActivateTab('fixtures');
        }
    } catch (_) {}
});

export function initDmxDiscover({onAdded} = {}) {
    _onAdded = onAdded || null;

    const modal = document.getElementById('fixture-discover-modal');
    if (!modal) return;

    let store = ensureStore();
    if (!store) {
        // Wait for Alpine before doing anything else.
        document.addEventListener('alpine:init', () => { ensureStore(); }, {once: true});
    }
}
