/**
 * Icon picker modal — choose a custom icon for an entity card.
 *
 * Generic over entity types (devices, LED targets, …). The picker is
 * opened by document-level click delegation matching:
 *
 *   data-icon-picker-trigger="<entityType>:<entityId>"
 *
 * Each registered entity type provides: cache lookup, PATCH endpoint,
 * reload callback, and an optional ``inheritedFrom`` resolver — used by
 * LED targets to fall back to the related device's icon when the target
 * doesn't have its own.
 */

import { Modal } from '../core/modal.ts';
import { t } from '../core/i18n.ts';
import { fetchWithAuth, escapeHtml } from '../core/api.ts';
import { showToast } from '../core/ui.ts';
import { devicesCache, outputTargetsCache } from '../core/state.ts';
import {
    DEVICE_ICONS,
    CATEGORIES,
    iconsByCategory,
    filterIcons,
    getDeviceIconDef,
    renderDeviceIconSvg,
    type IconCategory,
    type DeviceIconDef,
} from '../core/device-icons.ts';

const RECENT_KEY = 'ledgrab.icon-picker.recent';
const RECENT_MAX = 10;

// ────────────────────────────────────────────────────────────────
// Entity-type registry
// ────────────────────────────────────────────────────────────────
//
// Built-in types (device, target) are registered below. Other feature
// modules call ``registerIconEntityType()`` at import time to add their
// own — keeps icon-picker.ts decoupled from each feature's cache and
// reload pathway.

export type EntityType = string;

export interface EntityRecord {
    id: string;
    name: string;
    icon: string;
    icon_color: string;
}

export interface InheritedIcon {
    /** The icon id we'd render if this entity has no own icon. */
    iconId: string;
    /** Effective color for the inherited icon. */
    color: string;
    /** Display name of the source (e.g. parent device name). */
    fromName: string;
}

export interface EntityTypeAdapter {
    /** Look up the entity by id. Returns null when missing from the cache. */
    lookup(id: string): EntityRecord | null;
    /** Build the PUT endpoint URL (path only — fetchWithAuth prepends ``/api/v1``). */
    endpoint(id: string): string;
    /** Invalidate the cache and reload the relevant view. */
    reload(): Promise<void>;
    /** Optional fallback icon (e.g. LED target → parent device). */
    inheritedFrom(id: string): InheritedIcon | null;
    /** Display label like "Device" / "LED target" / "Picture source". */
    typeLabel(id: string): string;
    /** Optional CSS selector(s) to find the live card element so the
     *  picker preview can read its channel accent. Tried in order. */
    cardSelectors?: (id: string) => string[];
    /** Optional extra fields merged into the PUT body — used for
     *  discriminator-keyed routes (e.g. output-targets needs
     *  ``target_type`` for the ``OutputTargetUpdate`` discriminated
     *  union). Receives the entity id; adapter does its own lookup. */
    bodyExtras?: (id: string) => Record<string, unknown>;
}

const _adapters: Map<EntityType, EntityTypeAdapter> = new Map();

/** Public registration entry used by feature modules. Registering an
 *  unknown type is idempotent — re-registering replaces the adapter. */
export function registerIconEntityType(type: EntityType, adapter: EntityTypeAdapter): void {
    _adapters.set(type, adapter);
}

/** Helper for the common case: a single cache + a single PUT endpoint
 *  that accepts ``{icon, icon_color}`` directly. Reduces per-feature
 *  registration to ~6 lines. */
export function makeSimpleIconAdapter<T extends { id: string; name?: string; icon?: string; icon_color?: string }>(opts: {
    cache: { data: T[] | null; invalidate: () => void };
    /** PUT path prefix (no ``/api/v1``). Final URL = ``${endpointPrefix}/${id}``. */
    endpointPrefix: string;
    /** Async refresh hook. Cache is invalidated automatically. */
    reload: () => Promise<void> | void;
    typeLabelKey: string;
    typeLabelFallback: string;
    cardSelectors?: (id: string) => string[];
    bodyExtras?: (rec: T) => Record<string, unknown>;
}): EntityTypeAdapter {
    const _find = (id: string): T | undefined =>
        (opts.cache.data ?? []).find((x) => x.id === id);
    return {
        lookup: (id: string) => {
            const rec = _find(id);
            if (!rec) return null;
            return {
                id: rec.id,
                name: (rec.name as string | undefined) ?? rec.id,
                icon: (rec.icon as string | undefined) ?? '',
                icon_color: (rec.icon_color as string | undefined) ?? '',
            };
        },
        endpoint: (id: string) => `${opts.endpointPrefix}/${id}`,
        reload: async () => {
            opts.cache.invalidate();
            await opts.reload();
        },
        inheritedFrom: () => null,
        typeLabel: () => t(opts.typeLabelKey) || opts.typeLabelFallback,
        cardSelectors: opts.cardSelectors,
        bodyExtras: opts.bodyExtras
            ? (id: string) => {
                const rec = _find(id);
                return rec ? opts.bodyExtras!(rec) : {};
            }
            : undefined,
    };
}

// ── Built-in adapters: device + target ──────────────────────────

registerIconEntityType('device', {
    lookup: (id: string) => {
        const dev = (devicesCache.data ?? []).find((d: any) => d.id === id);
        if (!dev) return null;
        return {
            id: dev.id,
            name: dev.name ?? dev.id,
            icon: (dev.icon as string | undefined) ?? '',
            icon_color: (dev.icon_color as string | undefined) ?? '',
        };
    },
    endpoint: (id) => `/devices/${id}`,
    reload: async () => {
        devicesCache.invalidate();
        await window.loadDevices?.();
    },
    inheritedFrom: () => null,
    typeLabel: () => t('device.icon.entity.device') || 'Device',
    cardSelectors: (id) => [`[data-device-id="${CSS.escape(id)}"]`],
});

registerIconEntityType('target', {
    lookup: (id: string) => {
        const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
        if (!tgt) return null;
        return {
            id: tgt.id,
            name: tgt.name ?? tgt.id,
            icon: (tgt.icon as string | undefined) ?? '',
            icon_color: (tgt.icon_color as string | undefined) ?? '',
        };
    },
    endpoint: (id) => `/output-targets/${id}`,
    reload: async () => {
        outputTargetsCache.invalidate();
        await window.loadTargetsTab?.();
    },
    inheritedFrom: (id) => {
        const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
        if (!tgt) return null;
        // Only LED targets inherit from a device — HA-light targets don't.
        const deviceId = (tgt as any).device_id;
        if (!deviceId) return null;
        const dev = (devicesCache.data ?? []).find((d: any) => d.id === deviceId);
        const iconId = (dev?.icon as string | undefined) ?? '';
        if (!iconId) return null;
        return {
            iconId,
            color: (dev?.icon_color as string | undefined) || '',
            fromName: dev?.name ?? deviceId,
        };
    },
    typeLabel: (id: string) => {
        const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
        if ((tgt as any)?.target_type === 'ha_light') {
            return t('device.icon.entity.ha_light_target') || 'HA light target';
        }
        return t('device.icon.entity.target') || 'LED target';
    },
    cardSelectors: (id) => [
        `[data-target-id="${CSS.escape(id)}"]`,
        `[data-ha-target-id="${CSS.escape(id)}"]`,
    ],
    bodyExtras: (id: string) => {
        const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
        return { target_type: ((tgt as any)?.target_type as string | undefined) ?? 'led' };
    },
});

// ────────────────────────────────────────────────────────────────
// Picker state
// ────────────────────────────────────────────────────────────────

interface PickerContext {
    entityType: EntityType;
    entityId: string;
    entityName: string;
    initialIconId: string;
    initialColor: string;
    /** CSS color used for the live channel preview (e.g. '#4CAF50'). */
    channelColor: string;
    /** Optional inherited icon (LED target → device). */
    inherited: InheritedIcon | null;
}

let _ctx: PickerContext | null = null;
let _selectedIconId: string = '';
let _selectedColor: string = '';
let _activeCategory: IconCategory | 'all' = 'all';
let _query: string = '';
let _modalInstance: Modal | null = null;

// ────────────────────────────────────────────────────────────────
// Recent-icons persistence
// ────────────────────────────────────────────────────────────────

function _readRecent(): string[] {
    try {
        const raw = localStorage.getItem(RECENT_KEY);
        if (!raw) return [];
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed)) return [];
        return parsed.filter((x): x is string => typeof x === 'string').slice(0, RECENT_MAX);
    } catch {
        return [];
    }
}

function _pushRecent(iconId: string): void {
    if (!iconId) return;
    try {
        const list = _readRecent().filter((x) => x !== iconId);
        list.unshift(iconId);
        localStorage.setItem(RECENT_KEY, JSON.stringify(list.slice(0, RECENT_MAX)));
    } catch {
        /* ignore quota / disabled storage */
    }
}

// ────────────────────────────────────────────────────────────────
// Public entry points
// ────────────────────────────────────────────────────────────────

/** Open the picker for the given entity. Reads current icon from cache. */
export function openIconPicker(entityType: EntityType, entityId: string): void {
    if (!entityId) return;
    const adapter = _adapters.get(entityType);
    if (!adapter) return;

    const rec = adapter.lookup(entityId);
    const initialIconId = rec?.icon ?? '';
    const initialColor = rec?.icon_color ?? '';
    const inherited = adapter.inheritedFrom(entityId);

    // Resolve channel color from the live card so the preview matches.
    // Adapters provide the candidate selectors; we try them in order
    // and fall back to the global accent when no card is found.
    const selectors = adapter.cardSelectors?.(entityId) ?? [];
    let card: HTMLElement | null = null;
    for (const sel of selectors) {
        card = document.querySelector(sel) as HTMLElement | null;
        if (card) break;
    }
    const channelColor = card
        ? (getComputedStyle(card).getPropertyValue('--ch') || '').trim() || _fallbackChannel()
        : _fallbackChannel();

    _ctx = {
        entityType,
        entityId,
        entityName: rec?.name ?? entityId,
        initialIconId,
        initialColor,
        channelColor,
        inherited,
    };
    _selectedIconId = initialIconId;
    _selectedColor = initialColor;
    _activeCategory = 'all';
    _query = '';

    if (!_modalInstance) {
        _modalInstance = new Modal('icon-picker-modal');
    }
    _renderModal();
    _modalInstance.open();

    requestAnimationFrame(() => {
        const search = document.getElementById('icon-picker-search') as HTMLInputElement | null;
        search?.focus();
    });
}

/** Back-compat shim — early callers still import this name. */
export function openDeviceIconPicker(deviceId: string): void {
    openIconPicker('device', deviceId);
}

/** Close the picker without applying changes. */
export function closeIconPicker(): void {
    _modalInstance?.close();
    _ctx = null;
}

// ────────────────────────────────────────────────────────────────
// Rendering
// ────────────────────────────────────────────────────────────────

function _fallbackChannel(): string {
    const root = getComputedStyle(document.documentElement);
    return (root.getPropertyValue('--ch-signal') || '#4CAF50').trim();
}

/** What the preview displays right now (resolves inheritance). */
function _resolveDisplayIcon(): { iconId: string; color: string; isInherited: boolean } {
    if (!_ctx) return { iconId: '', color: '', isInherited: false };
    if (_selectedIconId) {
        return { iconId: _selectedIconId, color: _selectedColor || _ctx.channelColor, isInherited: false };
    }
    if (_ctx.inherited) {
        return {
            iconId: _ctx.inherited.iconId,
            color: _ctx.inherited.color || _ctx.channelColor,
            isInherited: true,
        };
    }
    return { iconId: '', color: _ctx.channelColor, isInherited: false };
}

function _renderModal(): void {
    if (!_ctx) return;

    const previewEl = document.getElementById('icon-picker-preview') as HTMLElement | null;
    const titleNameEl = document.getElementById('icon-picker-device-name') as HTMLElement | null;
    const eyebrowEl = document.getElementById('icon-picker-eyebrow') as HTMLElement | null;
    const subEl = document.getElementById('icon-picker-sub') as HTMLElement | null;
    const swatchEl = document.getElementById('icon-picker-swatch') as HTMLElement | null;
    const tabsEl = document.getElementById('icon-picker-tabs') as HTMLElement | null;
    const recentEl = document.getElementById('icon-picker-recent') as HTMLElement | null;
    const gridEl = document.getElementById('icon-picker-grid') as HTMLElement | null;
    const searchEl = document.getElementById('icon-picker-search') as HTMLInputElement | null;
    const removeBtn = document.getElementById('icon-picker-remove') as HTMLButtonElement | null;

    if (!previewEl || !tabsEl || !gridEl) return;

    const display = _resolveDisplayIcon();
    const effectiveColor = display.color;
    previewEl.style.setProperty('--ch', effectiveColor);
    previewEl.style.color = effectiveColor;
    previewEl.classList.toggle('is-inherited', display.isInherited && !!display.iconId);
    previewEl.classList.toggle('is-empty', !display.iconId);
    previewEl.innerHTML = display.iconId
        ? renderDeviceIconSvg(display.iconId, { size: 30 })
        : `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>`;

    // Header — entity type + name, plus inherited hint when applicable.
    const adapter = _adapters.get(_ctx.entityType)!;
    if (eyebrowEl) {
        eyebrowEl.textContent = adapter.typeLabel(_ctx.entityId);
    }
    if (titleNameEl) titleNameEl.textContent = _ctx.entityName;
    if (subEl) {
        if (display.isInherited) {
            const tmpl = t('device.icon.inherited_from') || 'Inherited from %s';
            subEl.textContent = tmpl.replace('%s', _ctx.inherited!.fromName);
            subEl.classList.add('is-inherited');
        } else {
            subEl.textContent = '';
            subEl.classList.remove('is-inherited');
        }
    }

    // Swatch reflects current effective color
    if (swatchEl) {
        swatchEl.style.background = effectiveColor;
        swatchEl.style.borderColor = effectiveColor;
    }

    if (searchEl && document.activeElement !== searchEl) {
        searchEl.value = _query;
    }

    tabsEl.innerHTML = _renderTabsHtml();

    if (recentEl) {
        const recent = _readRecent();
        if (recent.length === 0) {
            recentEl.style.display = 'none';
        } else {
            recentEl.style.display = '';
            recentEl.querySelector('.icon-picker-recent__strip')!.innerHTML = recent
                .map((id) => _iconTileHtml(getDeviceIconDef(id)))
                .filter(Boolean)
                .join('');
        }
    }

    gridEl.innerHTML = _renderGridHtml();

    // Remove button — meaning depends on whether the entity has an own icon
    // and whether an inherited fallback exists.
    if (removeBtn) {
        const hasOwn = !!_selectedIconId || !!_ctx.initialIconId;
        removeBtn.disabled = !hasOwn;
        // When clearing would reveal an inherited icon, label the button
        // "Use inherited" instead of "Remove icon" so the user knows what
        // happens next.
        const willInherit = _ctx.inherited != null;
        const labelKey = willInherit ? 'device.icon.use_inherited' : 'device.icon.remove';
        const fallback = willInherit ? 'Use inherited' : 'Remove icon';
        removeBtn.textContent = t(labelKey) !== labelKey ? t(labelKey) : fallback;
    }
}

function _renderTabsHtml(): string {
    const total = DEVICE_ICONS.length;
    const tabs: string[] = [];
    const allCls = _activeCategory === 'all' ? 'icon-picker-tab is-active' : 'icon-picker-tab';
    tabs.push(`<button type="button" class="${allCls}" data-cat="all">${escapeHtml(t('device.icon.cat.all') || 'All')} <span class="count">${total}</span></button>`);
    for (const cat of CATEGORIES) {
        const count = DEVICE_ICONS.filter((d) => d.category === cat.id).length;
        const cls = _activeCategory === cat.id ? 'icon-picker-tab is-active' : 'icon-picker-tab';
        const label = t(cat.i18n) || cat.label;
        tabs.push(`<button type="button" class="${cls}" data-cat="${cat.id}">${escapeHtml(label)} <span class="count">${count}</span></button>`);
    }
    return tabs.join('');
}

function _renderGridHtml(): string {
    const filtered = _query ? filterIcons(_query) : DEVICE_ICONS;
    const inCat = _activeCategory === 'all'
        ? filtered
        : filtered.filter((d) => d.category === _activeCategory);

    if (inCat.length === 0) {
        return `<div class="icon-picker-empty">${escapeHtml(t('device.icon.empty') || 'No icons match.')}</div>`;
    }

    if (_query || _activeCategory !== 'all') {
        return `<div class="icon-picker-grid">${inCat.map(_iconTileHtml).join('')}</div>`;
    }

    const groups = iconsByCategory();
    return groups
        .filter((g) => g.items.length > 0)
        .map((g) => {
            const label = t(g.i18n) || g.label;
            return `<div class="icon-picker-cat">${escapeHtml(label)}</div>
                    <div class="icon-picker-grid">${g.items.map(_iconTileHtml).join('')}</div>`;
        })
        .join('');
}

function _iconTileHtml(def: DeviceIconDef | null): string {
    if (!def) return '';
    const selected = def.id === _selectedIconId ? ' is-selected' : '';
    const labelKey = `device.icon.${def.id}`;
    const label = t(labelKey) !== labelKey ? t(labelKey) : def.label;
    return `<button type="button" class="icon-tile${selected}" data-icon-id="${def.id}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}">${renderDeviceIconSvg(def.id, { size: 20 })}</button>`;
}

// ────────────────────────────────────────────────────────────────
// Apply / remove
// ────────────────────────────────────────────────────────────────

async function _applyChange(nextIconId: string, nextColor: string): Promise<void> {
    if (!_ctx) return;
    const { entityType, entityId, initialIconId, initialColor } = _ctx;
    if (nextIconId === initialIconId && nextColor === initialColor) {
        closeIconPicker();
        return;
    }

    const adapter = _adapters.get(entityType)!;
    try {
        const body: Record<string, unknown> = { icon: nextIconId, icon_color: nextColor };
        // Discriminated routes (e.g. output-targets) need extra fields
        // — adapter declares them via ``bodyExtras``.
        if (adapter.bodyExtras) {
            Object.assign(body, adapter.bodyExtras(entityId));
        }
        const resp = await fetchWithAuth(adapter.endpoint(entityId), {
            method: 'PUT',
            body: JSON.stringify(body),
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            showToast((err && (err as any).detail) || t('device.icon.error.save_failed'), 'error');
            return;
        }
        if (nextIconId) _pushRecent(nextIconId);
        showToast(t('device.icon.saved') || 'Icon saved', 'success');
        await adapter.reload();
        closeIconPicker();
    } catch (error: any) {
        if (error?.isAuth) return;
        showToast(t('device.icon.error.save_failed') || 'Failed to save icon', 'error');
    }
}

async function _applyCurrentSelection(): Promise<void> {
    await _applyChange(_selectedIconId, _selectedColor);
}

async function _removeOwnIcon(): Promise<void> {
    // Clear the entity's own icon — for targets with an inherited fallback,
    // the card will then render the device's icon.
    await _applyChange('', '');
}

function _selectIcon(iconId: string): void {
    _selectedIconId = iconId;
    _renderModal();
}

function _setCategory(cat: IconCategory | 'all'): void {
    _activeCategory = cat;
    _renderModal();
}

function _setQuery(q: string): void {
    _query = q;
    if (q && _activeCategory !== 'all') _activeCategory = 'all';
    _renderModal();
}

function _toggleColorOverride(): void {
    if (!_ctx) return;
    if (_selectedColor) {
        _selectedColor = '';
    } else {
        _selectedColor = _ctx.channelColor;
    }
    _renderModal();
}

// ────────────────────────────────────────────────────────────────
// Modal-internal event wiring
// ────────────────────────────────────────────────────────────────

let _wired = false;

function _wireEvents(): void {
    if (_wired) return;
    _wired = true;

    const root = document.getElementById('icon-picker-modal');
    if (!root) return;

    root.addEventListener('click', (e) => {
        const target = e.target as HTMLElement;

        const tile = target.closest('.icon-tile') as HTMLElement | null;
        if (tile && tile.dataset.iconId) {
            _selectIcon(tile.dataset.iconId);
            return;
        }

        const tab = target.closest('.icon-picker-tab') as HTMLElement | null;
        if (tab && tab.dataset.cat) {
            _setCategory(tab.dataset.cat as IconCategory | 'all');
            return;
        }

        if (target.closest('#icon-picker-color-toggle')) {
            _toggleColorOverride();
            return;
        }
        if (target.closest('#icon-picker-apply')) {
            _applyCurrentSelection();
            return;
        }
        if (target.closest('#icon-picker-cancel') || target.closest('.icon-picker-close')) {
            closeIconPicker();
            return;
        }
        if (target.closest('#icon-picker-remove')) {
            _removeOwnIcon();
            return;
        }
    });

    const search = document.getElementById('icon-picker-search') as HTMLInputElement | null;
    if (search) {
        search.addEventListener('input', () => _setQuery(search.value));
    }

    root.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && (e.target as HTMLElement).tagName !== 'BUTTON') {
            e.preventDefault();
            _applyCurrentSelection();
        }
    });
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', _wireEvents, { once: true });
} else {
    _wireEvents();
}

// ────────────────────────────────────────────────────────────────
// Document-level click delegation. Triggers match
// ``[data-icon-picker-trigger="<entityType>:<entityId>"]``. Legacy
// triggers without a colon default to ``device:<id>`` so older cards
// keep working during the rollout.
// ────────────────────────────────────────────────────────────────

function _onDocumentClick(e: MouseEvent): void {
    const el = (e.target as HTMLElement | null)?.closest('[data-icon-picker-trigger]') as HTMLElement | null;
    if (!el) return;
    const raw = el.getAttribute('data-icon-picker-trigger') || '';
    if (!raw) return;
    const [typeOrId, id] = raw.includes(':') ? raw.split(':', 2) : ['device', raw];
    if (!id) return;
    if (!_adapters.has(typeOrId)) return;
    e.stopPropagation();
    openIconPicker(typeOrId, id);
}

document.addEventListener('click', _onDocumentClick);
