// Fixtures tab — list slice + entry point (KL-164 split).
//
// Owns: list state and rendering, filter, refresh, delete, store
// composition, Alpine wiring, deep-link hooks, notification action
// dispatch. The fixtures store is composed from three slices —
// list (here), editor, and universemap — into a single
// `$store.fixtures` namespace so all existing template `x-bind` paths
// and inter-slice `this.` calls keep working.
//
// CRUD on the running user's ~/.conductor/config.yaml via /api/fixtures.
// Changes are restart-to-apply by design: an LD might queue several
// edits (re-address, re-role, new fixture) before wanting any of them
// to take effect on stage.

import {registerNotificationAction} from '../js/notifications.js';
import {editorSlice} from './fixtures.editor.js';
import {universemapSlice} from './fixtures.universemap.js';

function listSlice() {
    return {
        // --- List state ---
        allFixtures: [],
        fixtureErrors: {},  // {idx: [error, ...]}
        restartPending: false,
        filter: '',

        // --- Computed: list ---
        get visibleFixtures() {
            const q = this.filter.trim().toLowerCase();
            const out = [];
            this.allFixtures.forEach((f, idx) => {
                if (q) {
                    const hay = `${f.name || ''} ${f.stamped_from || 'custom'} ${f.role || ''}`.toLowerCase();
                    if (!hay.includes(q)) return;
                }
                out.push({
                    fixture: f,
                    idx,
                    errors: this.fixtureErrors[idx] || [],
                    badgeText: f.stamped_from || 'custom',
                    isCustom: !f.stamped_from,
                    addrText: `u${f.universe ?? 0} · addr ${f.address ?? '?'} · ${f.channels ?? 0}ch`,
                    errorText: (this.fixtureErrors[idx] || []).map(e => e.message).join('; '),
                });
            });
            return out;
        },
        get showEmpty() { return this.allFixtures.length === 0; },
        get showFilteredEmpty() {
            return this.allFixtures.length > 0 && this.visibleFixtures.length === 0;
        },

        // --- Methods: list ---
        async refresh() {
            try {
                const r = await fetch('/api/fixtures');
                const data = await r.json();
                this.allFixtures = data.fixtures || [];
                this.restartPending = !!data.restart_pending;
            } catch (e) {
                this.allFixtures = [];
            }
            // Per-fixture validation errors (M4d) live on the engine
            // status endpoint. Merge them in by index so the render
            // pass can red-tint broken cards and surface the message.
            try {
                const sr = await fetch('/api/engine/status');
                const sd = await sr.json();
                const errs = {};
                (sd.errors || []).forEach(e => {
                    if (e.stage !== 'fixture' || e.index == null) return;
                    (errs[e.index] = errs[e.index] || []).push(e);
                });
                this.fixtureErrors = errs;
            } catch (_) {
                this.fixtureErrors = {};
            }
        },

        async deleteFixture(idx, name) {
            if (!confirm(`Delete "${name}"? This removes it from config.yaml.`)) return;
            try {
                const r = await fetch(`/api/fixtures/${idx}`, {method: 'DELETE'});
                if (!r.ok) {
                    const body = await r.json().catch(() => ({}));
                    alert(body.error || `Delete failed (${r.status})`);
                    return;
                }
                await this.refresh();
            } catch (e) {
                alert(String(e));
            }
        },
    };
}

// Compose three slices into one object while preserving getter
// semantics. Plain object spread (`{...slice}`) eagerly evaluates
// getters and replaces them with snapshot values — that breaks
// reactivity for the ~10 `get foo()` accessors in the editor and
// universemap slices. Object.defineProperties + getOwnPropertyDescriptors
// carries the descriptors through intact.
function compose(...slices) {
    const out = {};
    for (const slice of slices) {
        Object.defineProperties(out, Object.getOwnPropertyDescriptors(slice));
    }
    return out;
}

function storeDefinition() {
    return compose(listSlice(), editorSlice(), universemapSlice());
}

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

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

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

    // Refresh hook used by the RDM autodiscover flow after appending
    // new fixtures, so the list updates without a full page reload.
    window.conductorRefreshFixtures = () => ensureStore()?.refresh();

    // Deep-link hook for the "Fix in Fixtures" notification action
    // (M4d). Refreshes the list so the modal sees current data, then
    // opens the edit modal scrolled to the errored fixture.
    window.conductorOpenFixture = async (idx) => {
        try {
            if (typeof window.conductorActivateTab === 'function') {
                window.conductorActivateTab('fixtures');
            }
        } catch (_) {}
        const s = ensureStore();
        if (!s) return;
        await s.refresh();
        const row = document.querySelector(`.fixture-row[data-idx="${idx}"]`);
        if (row) row.scrollIntoView({behavior: 'smooth', block: 'center'});
        await s.openEdit(idx);
    };
}

// Server-side error notifications can ship an action that deep-links
// the user back into the fixture editor. Resolve the action's
// fixture_index from the live notification list (server attaches it
// to the action payload) and route through conductorOpenFixture.
registerNotificationAction('open_fixture_editor', ({actionId}) => {
    fetch('/api/notifications').then(r => r.json()).then(d => {
        const notif = (d.notifications || []).find(n =>
            (n.actions || []).some(a => a.id === actionId));
        if (!notif) return;
        const act = (notif.actions || []).find(a => a.id === actionId);
        const idx = act && typeof act.fixture_index === 'number' ? act.fixture_index : 0;
        if (typeof window.conductorOpenFixture === 'function') {
            window.conductorOpenFixture(idx);
        }
    }).catch(() => {});
});
