/**
 * Audio Sources — CRUD for capture and processed audio sources.
 *
 * Audio sources are managed entities that encapsulate audio device
 * configuration. Capture sources represent physical audio devices;
 * processed sources apply audio processing filters to another source.
 * CSS audio type references an audio source by ID.
 *
 * Card rendering is handled by streams.js (Audio tab).
 * This module manages the editor modal and API operations.
 */

import { _cachedAudioSources, _cachedAudioTemplates, _cachedAudioProcessingTemplates, audioProcessingTemplatesCache, apiKey, audioSourcesCache } from '../core/state.ts';
import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.ts';
import { t } from '../core/i18n.ts';
import { showToast, showConfirm, lockBody, unlockBody } from '../core/ui.ts';
import { Modal } from '../core/modal.ts';
import { ICON_MUSIC, getAudioSourceIcon, ICON_AUDIO_TEMPLATE, ICON_AUDIO_INPUT, ICON_AUDIO_LOOPBACK } from '../core/icons.ts';
import { EntitySelect } from '../core/entity-palette.ts';
import { IconSelect } from '../core/icon-select.ts';
import { TagInput } from '../core/tag-input.ts';
import * as P from '../core/icon-paths.ts';
import { loadPictureSources } from './streams.ts';

let _audioSourceTagsInput: TagInput | null = null;

class AudioSourceModal extends Modal {
    constructor() { super('audio-source-modal'); }

    onForceClose() {
        if (_audioSourceTagsInput) { _audioSourceTagsInput.destroy(); _audioSourceTagsInput = null; }
    }

    snapshotValues() {
        return {
            name: (document.getElementById('audio-source-name') as HTMLInputElement).value,
            description: (document.getElementById('audio-source-description') as HTMLInputElement).value,
            type: (document.getElementById('audio-source-type') as HTMLSelectElement).value,
            device: (document.getElementById('audio-source-device') as HTMLSelectElement).value,
            audioTemplate: (document.getElementById('audio-source-audio-template') as HTMLSelectElement).value,
            parentSource: (document.getElementById('audio-source-parent') as HTMLSelectElement).value,
            processingTemplate: (document.getElementById('audio-source-processing-template') as HTMLSelectElement).value,
            tags: JSON.stringify(_audioSourceTagsInput ? _audioSourceTagsInput.getValue() : []),
        };
    }
}

const audioSourceModal = new AudioSourceModal();

// ── EntitySelect instances for audio source editor ──
let _asTemplateEntitySelect: EntitySelect | null = null;
let _asDeviceEntitySelect: EntitySelect | null = null;
let _asParentEntitySelect: EntitySelect | null = null;
let _asProcessingTemplateEntitySelect: EntitySelect | null = null;

const _svg = (d: string): string => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;

// ── Auto-name generation ──────────────────────────────────────

let _asNameManuallyEdited = false;

function _autoGenerateAudioSourceName() {
    if (_asNameManuallyEdited) return;
    if ((document.getElementById('audio-source-id') as HTMLInputElement).value) return;
    const type = (document.getElementById('audio-source-type') as HTMLSelectElement).value;
    let name = '';
    if (type === 'capture') {
        const devSel = document.getElementById('audio-source-device') as HTMLSelectElement | null;
        const devName = devSel?.selectedOptions[0]?.textContent?.trim();
        name = devName || t('audio_source.type.capture');
    } else if (type === 'processed') {
        const parentSel = document.getElementById('audio-source-parent') as HTMLSelectElement | null;
        const parentName = parentSel?.selectedOptions[0]?.textContent?.trim() || '';
        name = parentName ? `${parentName} (processed)` : t('audio_source.type.processed');
    }
    (document.getElementById('audio-source-name') as HTMLInputElement).value = name;
}

// ── Modal ─────────────────────────────────────────────────────

const _titleKeys: Record<string, Record<string, string>> = {
    capture: { add: 'audio_source.add.capture', edit: 'audio_source.edit.capture' },
    processed: { add: 'audio_source.add.processed', edit: 'audio_source.edit.processed' },
};

export async function showAudioSourceModal(sourceType: any, editData?: any) {
    const isEdit = !!editData;
    const st = isEdit ? editData.source_type : sourceType;
    const titleKey = _titleKeys[st]?.[isEdit ? 'edit' : 'add'] || _titleKeys.capture.add;

    document.getElementById('audio-source-modal-title')!.innerHTML = `${ICON_MUSIC} ${t(titleKey)}`;
    (document.getElementById('audio-source-id') as HTMLInputElement).value = isEdit ? editData.id : '';
    (document.getElementById('audio-source-error') as HTMLElement).style.display = 'none';

    const typeSelect = document.getElementById('audio-source-type') as HTMLSelectElement;
    typeSelect.value = st;
    typeSelect.disabled = isEdit; // can't change type after creation

    onAudioSourceTypeChange();

    if (isEdit) {
        (document.getElementById('audio-source-name') as HTMLInputElement).value = editData.name || '';
        (document.getElementById('audio-source-description') as HTMLInputElement).value = editData.description || '';

        if (editData.source_type === 'capture') {
            _loadAudioTemplates(editData.audio_template_id);
            (document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = () => { _filterDevicesBySelectedTemplate(); _autoGenerateAudioSourceName(); };
            await _loadAudioDevices();
            _selectAudioDevice(editData.device_index, editData.is_loopback);
        } else if (editData.source_type === 'processed') {
            _loadParentSources(editData.audio_source_id);
            _loadProcessingTemplates(editData.audio_processing_template_id);
        }
    } else {
        (document.getElementById('audio-source-name') as HTMLInputElement).value = '';
        (document.getElementById('audio-source-description') as HTMLInputElement).value = '';

        if (sourceType === 'capture') {
            _loadAudioTemplates();
            (document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = () => { _filterDevicesBySelectedTemplate(); _autoGenerateAudioSourceName(); };
            await _loadAudioDevices();
        } else if (sourceType === 'processed') {
            _loadParentSources();
            _loadProcessingTemplates();
        }
    }

    // Tags
    if (_audioSourceTagsInput) { _audioSourceTagsInput.destroy(); _audioSourceTagsInput = null; }
    _audioSourceTagsInput = new TagInput(document.getElementById('audio-source-tags-container'), { placeholder: t('tags.placeholder') });
    _audioSourceTagsInput.setValue(isEdit ? (editData.tags || []) : []);

    // Auto-name wiring
    _asNameManuallyEdited = isEdit;
    (document.getElementById('audio-source-name') as HTMLElement).oninput = () => { _asNameManuallyEdited = true; };
    if (!isEdit) _autoGenerateAudioSourceName();

    audioSourceModal.open();
    audioSourceModal.snapshot();
}

export async function closeAudioSourceModal() {
    await audioSourceModal.close();
}

export function onAudioSourceTypeChange() {
    const type = (document.getElementById('audio-source-type') as HTMLSelectElement).value;
    const captureSection = document.getElementById('audio-source-capture-section');
    const processedSection = document.getElementById('audio-source-processed-section');
    if (captureSection) captureSection.style.display = type === 'capture' ? '' : 'none';
    if (processedSection) processedSection.style.display = type === 'processed' ? '' : 'none';
}

// ── Save ──────────────────────────────────────────────────────

export async function saveAudioSource() {
    const id = (document.getElementById('audio-source-id') as HTMLInputElement).value;
    const name = (document.getElementById('audio-source-name') as HTMLInputElement).value.trim();
    const sourceType = (document.getElementById('audio-source-type') as HTMLSelectElement).value;
    const description = (document.getElementById('audio-source-description') as HTMLInputElement).value.trim() || null;
    const errorEl = document.getElementById('audio-source-error') as HTMLElement;

    if (!name) {
        errorEl.textContent = t('audio_source.error.name_required');
        errorEl.style.display = '';
        return;
    }

    const payload: any = { name, source_type: sourceType, description, tags: _audioSourceTagsInput ? _audioSourceTagsInput.getValue() : [] };

    if (sourceType === 'capture') {
        const deviceVal = (document.getElementById('audio-source-device') as HTMLSelectElement).value || '-1:1';
        const [devIdx, devLoop] = deviceVal.split(':');
        payload.device_index = parseInt(devIdx) || -1;
        payload.is_loopback = devLoop !== '0';
        payload.audio_template_id = (document.getElementById('audio-source-audio-template') as HTMLSelectElement).value || null;
    } else if (sourceType === 'processed') {
        payload.audio_source_id = (document.getElementById('audio-source-parent') as HTMLSelectElement).value;
        payload.audio_processing_template_id = (document.getElementById('audio-source-processing-template') as HTMLSelectElement).value;
    }

    try {
        const method = id ? 'PUT' : 'POST';
        const url = id ? `/audio-sources/${id}` : '/audio-sources';
        const resp = await fetchWithAuth(url, {
            method,
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload),
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        showToast(t(id ? 'audio_source.updated' : 'audio_source.created'), 'success');
        audioSourceModal.forceClose();
        audioSourcesCache.invalidate();
        await loadPictureSources();
    } catch (e: any) {
        errorEl.textContent = e.message;
        errorEl.style.display = '';
    }
}

// ── Edit ──────────────────────────────────────────────────────

export async function editAudioSource(sourceId: any) {
    try {
        const resp = await fetchWithAuth(`/audio-sources/${sourceId}`);
        if (!resp.ok) throw new Error(t('audio_source.error.load'));
        const data = await resp.json();
        await showAudioSourceModal(data.source_type, data);
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(e.message, 'error');
    }
}

// ── Clone ─────────────────────────────────────────────────────

export async function cloneAudioSource(sourceId: any) {
    try {
        const resp = await fetchWithAuth(`/audio-sources/${sourceId}`);
        if (!resp.ok) throw new Error(t('audio_source.error.load'));
        const data = await resp.json();
        delete data.id;
        data.name = data.name + ' (copy)';
        await showAudioSourceModal(data.source_type, data);
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(e.message, 'error');
    }
}

// ── Delete ────────────────────────────────────────────────────

export async function deleteAudioSource(sourceId: any) {
    const confirmed = await showConfirm(t('audio_source.delete.confirm'));
    if (!confirmed) return;

    try {
        const resp = await fetchWithAuth(`/audio-sources/${sourceId}`, { method: 'DELETE' });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        showToast(t('audio_source.deleted'), 'success');
        audioSourcesCache.invalidate();
        await loadPictureSources();
    } catch (e: any) {
        showToast(e.message, 'error');
    }
}

// ── Refresh devices ───────────────────────────────────────────

export async function refreshAudioDevices() {
    const btn = document.getElementById('audio-source-refresh-devices') as HTMLButtonElement | null;
    if (btn) btn.disabled = true;
    try {
        await _loadAudioDevices();
    } finally {
        if (btn) btn.disabled = false;
    }
}

// ── Helpers ───────────────────────────────────────────────────

let _cachedDevicesByEngine = {};

async function _loadAudioDevices() {
    try {
        const resp = await fetchWithAuth('/audio-devices');
        if (!resp.ok) throw new Error('fetch failed');
        const data = await resp.json();
        _cachedDevicesByEngine = data.by_engine || {};
    } catch {
        _cachedDevicesByEngine = {};
    }
    _filterDevicesBySelectedTemplate();
}

function _filterDevicesBySelectedTemplate() {
    const select = document.getElementById('audio-source-device') as HTMLSelectElement | null;
    if (!select) return;

    const prevOption = select.options[select.selectedIndex];
    const prevName = prevOption ? prevOption.textContent : '';

    const templateId = ((document.getElementById('audio-source-audio-template') as HTMLSelectElement | null) || { value: '' } as any).value;
    const templates = _cachedAudioTemplates || [];
    const template = templates.find(t => t.id === templateId);
    const engineType = template ? template.engine_type : null;

    let devices: any[] = [];
    if (engineType && _cachedDevicesByEngine[engineType]) {
        devices = _cachedDevicesByEngine[engineType];
    } else {
        for (const devList of Object.values(_cachedDevicesByEngine)) {
            devices = devices.concat(devList as any[]);
        }
    }

    select.innerHTML = devices.map((d: any) => {
        const val = `${d.index}:${d.is_loopback ? '1' : '0'}`;
        return `<option value="${val}">${escapeHtml(d.name)}</option>`;
    }).join('');

    if (devices.length === 0) {
        select.innerHTML = '<option value="-1:1">Default</option>';
    }

    if (prevName) {
        const match = Array.from(select.options).find((o: HTMLOptionElement) => o.textContent === prevName);
        if (match) select.value = match.value;
    }

    if (_asDeviceEntitySelect) _asDeviceEntitySelect.destroy();
    if (devices.length > 0) {
        _asDeviceEntitySelect = new EntitySelect({
            target: select,
            getItems: () => devices.map((d: any) => ({
                value: `${d.index}:${d.is_loopback ? '1' : '0'}`,
                label: d.name,
                icon: d.is_loopback ? ICON_AUDIO_LOOPBACK : ICON_AUDIO_INPUT,
                desc: d.is_loopback ? 'Loopback' : 'Input',
            })),
            placeholder: t('palette.search'),
        } as any);
    }
}

function _selectAudioDevice(deviceIndex: any, isLoopback: any) {
    const select = document.getElementById('audio-source-device') as HTMLSelectElement | null;
    if (!select) return;
    const val = `${deviceIndex ?? -1}:${isLoopback !== false ? '1' : '0'}`;
    const opt = Array.from(select.options).find((o: HTMLOptionElement) => o.value === val);
    if (opt) select.value = val;
}

function _loadParentSources(selectedId?: any) {
    const select = document.getElementById('audio-source-parent') as HTMLSelectElement | null;
    if (!select) return;
    // Processed sources can reference any audio source type
    const sources = _cachedAudioSources;
    select.innerHTML = sources.map(s =>
        `<option value="${s.id}"${s.id === selectedId ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
    ).join('');

    if (_asParentEntitySelect) _asParentEntitySelect.destroy();
    if (sources.length > 0) {
        _asParentEntitySelect = new EntitySelect({
            target: select,
            getItems: () => sources.map((s: any) => ({
                value: s.id,
                label: s.name,
                icon: getAudioSourceIcon(s.source_type),
                desc: s.source_type,
            })),
            placeholder: t('palette.search'),
        } as any);
    }
}

async function _loadProcessingTemplates(selectedId?: any) {
    const select = document.getElementById('audio-source-processing-template') as HTMLSelectElement | null;
    if (!select) return;
    // Use the DataCache for audio processing templates
    const templates = await audioProcessingTemplatesCache.fetch();
    select.innerHTML = templates.map((tmpl: any) =>
        `<option value="${tmpl.id}"${tmpl.id === selectedId ? ' selected' : ''}>${escapeHtml(tmpl.name)}</option>`
    ).join('');

    if (_asProcessingTemplateEntitySelect) _asProcessingTemplateEntitySelect.destroy();
    if (templates.length > 0) {
        _asProcessingTemplateEntitySelect = new EntitySelect({
            target: select,
            getItems: () => _cachedAudioProcessingTemplates.map((tmpl: any) => ({
                value: tmpl.id,
                label: tmpl.name,
                icon: ICON_AUDIO_TEMPLATE,
            })),
            placeholder: t('palette.search'),
        } as any);
    }
}

function _loadAudioTemplates(selectedId?: any) {
    const select = document.getElementById('audio-source-audio-template') as HTMLSelectElement | null;
    if (!select) return;
    const templates = _cachedAudioTemplates || [];
    select.innerHTML = templates.map(t =>
        `<option value="${t.id}"${t.id === selectedId ? ' selected' : ''}>${escapeHtml(t.name)} (${t.engine_type.toUpperCase()})</option>`
    ).join('');

    if (_asTemplateEntitySelect) _asTemplateEntitySelect.destroy();
    if (templates.length > 0) {
        _asTemplateEntitySelect = new EntitySelect({
            target: select,
            getItems: () => templates.map((tmpl: any) => ({
                value: tmpl.id,
                label: tmpl.name,
                icon: ICON_AUDIO_TEMPLATE,
                desc: tmpl.engine_type.toUpperCase(),
            })),
            placeholder: t('palette.search'),
        } as any);
    }
}

// ── Audio Source Test (real-time spectrum) ────────────────────

const NUM_BANDS = 64;
const PEAK_DECAY = 0.02;      // peak drop per frame
const BEAT_FLASH_DECAY = 0.06; // beat flash fade per frame

let _testAudioWs: WebSocket | null = null;
let _testAudioAnimFrame: number | null = null;
let _testAudioLatest: any = null;
let _testAudioPeaks = new Float32Array(NUM_BANDS);
let _testBeatFlash = 0;

const testAudioModal = new Modal('test-audio-source-modal', { backdrop: true, lock: true });

export function testAudioSource(sourceId: any) {
    const statusEl = document.getElementById('audio-test-status');
    if (statusEl) {
        statusEl.textContent = t('audio_source.test.connecting');
        statusEl.style.display = '';
    }

    // Reset state
    _testAudioLatest = null;
    _testAudioPeaks.fill(0);
    _testBeatFlash = 0;

    document.getElementById('audio-test-rms')!.textContent = '---';
    document.getElementById('audio-test-peak')!.textContent = '---';
    document.getElementById('audio-test-beat-dot')!.classList.remove('active');

    testAudioModal.open();

    // Size canvas to container
    const canvas = document.getElementById('audio-test-canvas') as HTMLCanvasElement;
    _sizeCanvas(canvas);

    // Connect WebSocket
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    const wsUrl = `${protocol}//${window.location.host}${API_BASE}/audio-sources/${sourceId}/test/ws?token=${encodeURIComponent(apiKey ?? '')}`;

    try {
        _testAudioWs = new WebSocket(wsUrl);

        _testAudioWs.onopen = () => {
            if (statusEl) statusEl.style.display = 'none';
        };

        _testAudioWs.onmessage = (event) => {
            try {
                _testAudioLatest = JSON.parse(event.data);
            } catch {}
        };

        _testAudioWs.onclose = () => {
            _testAudioWs = null;
        };

        _testAudioWs.onerror = () => {
            showToast(t('audio_source.test.error'), 'error');
            _cleanupTest();
        };
    } catch {
        showToast(t('audio_source.test.error'), 'error');
        _cleanupTest();
        return;
    }

    // Start render loop
    _testAudioAnimFrame = requestAnimationFrame(_renderLoop);
}

export function closeTestAudioSourceModal() {
    _cleanupTest();
    testAudioModal.forceClose();
}

function _cleanupTest() {
    if (_testAudioAnimFrame) {
        cancelAnimationFrame(_testAudioAnimFrame);
        _testAudioAnimFrame = null;
    }
    if (_testAudioWs) {
        _testAudioWs.onclose = null;
        _testAudioWs.close();
        _testAudioWs = null;
    }
    _testAudioLatest = null;
}

function _sizeCanvas(canvas: HTMLCanvasElement) {
    const rect = canvas.parentElement!.getBoundingClientRect();
    const dpr = window.devicePixelRatio || 1;
    canvas.width = rect.width * dpr;
    canvas.height = 200 * dpr;
    canvas.style.height = '200px';
    canvas.getContext('2d')!.scale(dpr, dpr);
}

function _renderLoop() {
    _renderAudioSpectrum();
    if (testAudioModal.isOpen) {
        _testAudioAnimFrame = requestAnimationFrame(_renderLoop);
    }
}

// ── Event delegation for audio source card actions ──

const _audioSourceActions: Record<string, (id: string) => void> = {
    'test-audio': testAudioSource,
    'clone-audio': cloneAudioSource,
    'edit-audio': editAudioSource,
};

export function initAudioSourceDelegation(container: HTMLElement): void {
    container.addEventListener('click', (e: MouseEvent) => {
        const btn = (e.target as HTMLElement).closest<HTMLElement>('[data-action]');
        if (!btn) return;

        const action = btn.dataset.action;
        if (!action) return;

        // Only handle audio-source actions (prefixed with audio-)
        const handler = _audioSourceActions[action];
        if (handler) {
            // Verify we're inside an audio source section
            const section = btn.closest<HTMLElement>('[data-card-section="audio-capture"], [data-card-section="audio-processed"]');
            if (!section) return;
            const card = btn.closest<HTMLElement>('[data-id]');
            const id = card?.getAttribute('data-id');
            if (!id) return;
            e.stopPropagation();
            handler(id);
        }
    });
}

function _renderAudioSpectrum() {
    const canvas = document.getElementById('audio-test-canvas') as HTMLCanvasElement | null;
    if (!canvas) return;

    const ctx = canvas.getContext('2d')!;
    const dpr = window.devicePixelRatio || 1;
    const w = canvas.width / dpr;
    const h = canvas.height / dpr;

    // Reset transform for clearing
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, w, h);

    const data = _testAudioLatest;
    if (!data || !data.spectrum) return;

    const spectrum = data.spectrum;
    const gap = 1;
    const barWidth = (w - gap * (NUM_BANDS - 1)) / NUM_BANDS;

    // Beat flash background
    if (data.beat) _testBeatFlash = Math.min(1.0, data.beat_intensity + 0.3);
    if (_testBeatFlash > 0) {
        ctx.fillStyle = `rgba(255, 255, 255, ${_testBeatFlash * 0.08})`;
        ctx.fillRect(0, 0, w, h);
        _testBeatFlash = Math.max(0, _testBeatFlash - BEAT_FLASH_DECAY);
    }

    for (let i = 0; i < NUM_BANDS; i++) {
        const val = Math.min(1, spectrum[i]);
        const barHeight = val * h;
        const x = i * (barWidth + gap);
        const y = h - barHeight;

        // Bar color: green → yellow → red based on value
        const hue = (1 - val) * 120;
        ctx.fillStyle = `hsl(${hue}, 85%, 50%)`;
        ctx.fillRect(x, y, barWidth, barHeight);

        // Falling peak indicator
        if (val > _testAudioPeaks[i]) {
            _testAudioPeaks[i] = val;
        } else {
            _testAudioPeaks[i] = Math.max(0, _testAudioPeaks[i] - PEAK_DECAY);
        }
        const peakY = h - _testAudioPeaks[i] * h;
        const peakHue = (1 - _testAudioPeaks[i]) * 120;
        ctx.fillStyle = `hsl(${peakHue}, 90%, 70%)`;
        ctx.fillRect(x, peakY, barWidth, 2);
    }

    // Update stats
    document.getElementById('audio-test-rms')!.textContent = (data.rms * 100).toFixed(1) + '%';
    document.getElementById('audio-test-peak')!.textContent = (data.peak * 100).toFixed(1) + '%';
    const beatDot = document.getElementById('audio-test-beat-dot');
    if (data.beat) {
        beatDot!.classList.add('active');
    } else {
        beatDot!.classList.remove('active');
    }
}

// ── Removed types ─────────────────────────────────────────────
// MonoAudioSource and BandExtractAudioSource have been removed.
// Channel selection is now handled by the channel_extract audio filter.
// Band filtering is now handled by the band_extract audio filter.
// These are applied via ProcessedAudioSource referencing an AudioProcessingTemplate.
// Exported stubs for backward compatibility (no-op):
export function onBandPresetChange() { /* removed */ }
