/**
 * Card presentation modes — per-surface size toggle (comfortable / compact / dense).
 *
 * Sibling to `dashboard-layout.ts` but slimmer: a single open registry of
 * surface keys ('devices', 'displays', 'dashboard-targets', …) each mapped
 * to one of three modes. CSS does the heavy lifting via the
 * `[data-card-mode="…"]` attribute (see card-modes.css).
 *
 * Boot sequence (matches dashboard-layout):
 *   1. hydrateCardModesFromCache()  — synchronous, called before first paint
 *   2. syncCardModesFromServer()    — async, runs after auth completes
 *
 * Persistence:
 *   - localStorage `card_modes_v1` cache for instant first paint
 *   - server `GET/PUT /preferences/card-modes` for cross-browser truth
 *   - PUT is debounced 300 ms; subscribers fire synchronously on save
 *
 * Surface keys are free-form strings — anything calling `setCardMode` is
 * implicitly registering that key. Defaults are returned for unknown keys.
 */
import { fetchWithAuth } from '../core/api.ts';
import { t } from '../core/i18n.ts';

const LS_KEY = 'card_modes_v1';
const SCHEMA_VERSION = 1;

export type CardMode = 'comfortable' | 'compact' | 'dense' | 'row';

export const CARD_MODES: readonly CardMode[] = ['comfortable', 'compact', 'dense', 'row'] as const;

const DEFAULT_MODE: CardMode = 'compact';

/** Per-surface defaults — overrides `DEFAULT_MODE` for specific surfaces
 *  whose preferred initial mode differs from (or must be pinned regardless
 *  of) the global default. Resolved by `getCardMode` only when the user
 *  has not set an explicit value for the surface. */
const SURFACE_DEFAULTS: Readonly<Record<string, CardMode>> = {
    'dashboard-running': 'compact',
};

export interface CardModePrefsV1 {
    version: 1;
    surfaces: Record<string, CardMode>;
}

const DEFAULT_PREFS: CardModePrefsV1 = {
    version: SCHEMA_VERSION,
    surfaces: {},
};

let _current: CardModePrefsV1 = _clone(DEFAULT_PREFS);
let _serverSyncedOnce = false;
let _saveTimer: ReturnType<typeof setTimeout> | null = null;
const _listeners = new Set<() => void>();

function _clone(prefs: CardModePrefsV1): CardModePrefsV1 {
    return { version: prefs.version, surfaces: { ...prefs.surfaces } };
}

function _isCardMode(v: unknown): v is CardMode {
    return v === 'comfortable' || v === 'compact' || v === 'dense' || v === 'row';
}

/** Normalise a parsed value back into a valid prefs object, dropping
 *  garbage values. Tolerates older/forward versions by treating the
 *  surfaces map as the only authoritative payload. */
function _normalise(parsed: unknown): CardModePrefsV1 {
    const out: CardModePrefsV1 = _clone(DEFAULT_PREFS);
    if (!parsed || typeof parsed !== 'object') return out;
    const obj = parsed as Record<string, unknown>;
    const surfaces = obj.surfaces;
    if (surfaces && typeof surfaces === 'object') {
        for (const [k, v] of Object.entries(surfaces as Record<string, unknown>)) {
            if (typeof k === 'string' && _isCardMode(v)) {
                out.surfaces[k] = v;
            }
        }
    }
    return out;
}

function _notify(): void {
    for (const fn of _listeners) {
        try { fn(); } catch (e) { console.error('card-modes listener', e); }
    }
}

/** Read the current prefs. Defensive copy. */
export function getCardModePrefs(): CardModePrefsV1 {
    return _clone(_current);
}

/** Effective mode for a surface — returns the user's configured value if
 *  set, else the per-surface default (`SURFACE_DEFAULTS`), else the
 *  global `DEFAULT_MODE`. */
export function getCardMode(surface: string): CardMode {
    return _current.surfaces[surface] ?? SURFACE_DEFAULTS[surface] ?? DEFAULT_MODE;
}

/** Persist a surface's mode. Updates cache, fires subscribers,
 *  schedules debounced server PUT. */
export function setCardMode(surface: string, mode: CardMode): void {
    if (!_isCardMode(mode)) return;
    if (_current.surfaces[surface] === mode) return;
    _current = {
        version: _current.version,
        surfaces: { ..._current.surfaces, [surface]: mode },
    };
    _persistLocal();
    _notify();
    _scheduleServerPush();
}

/** Subscribe to mode changes (any surface). Returns an unsubscribe fn. */
export function subscribeCardModes(fn: () => void): () => void {
    _listeners.add(fn);
    return () => _listeners.delete(fn);
}

function _persistLocal(): void {
    try { localStorage.setItem(LS_KEY, JSON.stringify(_current)); } catch { /* quota */ }
}

function _scheduleServerPush(): void {
    if (_saveTimer) clearTimeout(_saveTimer);
    _saveTimer = setTimeout(() => {
        _saveTimer = null;
        _pushToServer(_current).catch(e => console.warn('card-modes PUT failed', e));
    }, 300);
}

async function _pushToServer(prefs: CardModePrefsV1): Promise<void> {
    try {
        await fetchWithAuth('/preferences/card-modes', {
            method: 'PUT',
            body: JSON.stringify(prefs),
        });
    } catch (e) {
        console.warn('card-modes server PUT failed', e);
    }
}

/** Hydrate from localStorage cache. Synchronous — safe to call before
 *  auth so the first paint already uses the user's saved modes. */
export function hydrateCardModesFromCache(): CardModePrefsV1 {
    try {
        const raw = localStorage.getItem(LS_KEY);
        if (raw) {
            _current = _normalise(JSON.parse(raw));
        }
    } catch (e) {
        console.warn('card-modes cache parse failed', e);
    }
    return _clone(_current);
}

/** Pull prefs from server (post-auth). Replaces local cache when server
 *  has a saved value; otherwise pushes local cache up so other browsers
 *  inherit it. Safe to call repeatedly — only runs the round-trip once. */
export async function syncCardModesFromServer(): Promise<void> {
    if (_serverSyncedOnce) return;
    try {
        const resp = await fetchWithAuth('/preferences/card-modes');
        if (!resp || !resp.ok) return;
        const data = await resp.json();
        if (data && typeof data === 'object' && (data as Record<string, unknown>).version) {
            _current = _normalise(data);
            _persistLocal();
            _notify();
        } else {
            // Server has nothing — push what we have so the next browser
            // picks up the same view.
            if (Object.keys(_current.surfaces).length > 0) {
                await _pushToServer(_current);
            }
        }
        _serverSyncedOnce = true;
    } catch (e) {
        console.warn('card-modes server sync failed', e);
    }
}

// ─────────────────────────────────────────────────────────────────────────
// DOM helpers
// ─────────────────────────────────────────────────────────────────────────

/** Apply `data-card-mode` to a host element based on the current pref
 *  for `surface`. Idempotent; safe to call on the same element after a
 *  mode change to re-sync. */
export function applyCardModeAttr(host: HTMLElement, surface: string): void {
    host.setAttribute('data-card-mode', getCardMode(surface));
}

/** Bind a host element to a surface's mode. Re-applies the attribute
 *  whenever the pref for that surface changes. Returns an unsubscribe
 *  function for use in a cleanup hook. */
export function bindCardModeAttr(host: HTMLElement, surface: string): () => void {
    applyCardModeAttr(host, surface);
    return subscribeCardModes(() => applyCardModeAttr(host, surface));
}

// ─────────────────────────────────────────────────────────────────────────
// Toggle UI
// ─────────────────────────────────────────────────────────────────────────

export interface MountCardModeToggleOpts {
    /** Element to append the toggle to. */
    container: HTMLElement;
    /** Surface key whose mode this toggle controls. */
    surface: string;
    /** Optional host element to receive the `data-card-mode` attribute.
     *  Defaults to `container.parentElement ?? container`. Pass the grid
     *  container (or any common ancestor of the cards) so CSS overrides
     *  cascade correctly. */
    host?: HTMLElement;
    /** Position hint for screen readers + analytics. */
    label?: string;
}

const _MODE_BUTTONS: ReadonlyArray<{ v: CardMode; lbl: string; key: string }> = [
    { v: 'comfortable', lbl: 'C', key: 'card_mode.comfortable' },
    { v: 'compact',     lbl: 'M', key: 'card_mode.compact' },
    { v: 'dense',       lbl: 'D', key: 'card_mode.dense' },
    { v: 'row',         lbl: 'R', key: 'card_mode.row' },
];

/** Mount a segmented `C / M / D` toggle that drives the given surface.
 *  Returns a teardown function that removes the toggle and unsubscribes. */
export function mountCardModeToggle(opts: MountCardModeToggleOpts): () => void {
    const host = opts.host ?? opts.container.parentElement ?? opts.container;
    const surface = opts.surface;

    const wrap = document.createElement('div');
    wrap.className = 'card-mode-toggle';
    wrap.setAttribute('role', 'radiogroup');
    wrap.setAttribute('aria-label', opts.label || t('card_mode.tooltip') || 'Card size');

    const buttons: HTMLButtonElement[] = _MODE_BUTTONS.map(b => {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'card-mode-toggle__btn';
        btn.dataset.cardMode = b.v;
        btn.setAttribute('role', 'radio');
        btn.textContent = b.lbl;
        btn.title = t(b.key) || b.v;
        btn.setAttribute('aria-label', t(b.key) || b.v);
        btn.addEventListener('click', () => setCardMode(surface, b.v));
        wrap.appendChild(btn);
        return btn;
    });

    const refresh = () => {
        const current = getCardMode(surface);
        for (const btn of buttons) {
            const isActive = btn.dataset.cardMode === current;
            btn.classList.toggle('is-active', isActive);
            btn.setAttribute('aria-checked', isActive ? 'true' : 'false');
        }
        applyCardModeAttr(host, surface);
    };

    refresh();
    opts.container.appendChild(wrap);
    const unsubscribe = subscribeCardModes(refresh);

    return () => {
        unsubscribe();
        wrap.remove();
    };
}
