/**
 * Typed DOM helpers — replace `as HTMLInputElement` casts and bang
 * non-null assertions with helpers that fail loudly on misuse.
 *
 * Use `getInput`/`getSelect`/`getEl` when the element is required and a
 * missing/wrong-type element is a developer error. Use the `tryGet…`
 * variants when absence is legitimate (optional UI, conditional render).
 *
 * Also exports a minimal `reconcileList` reconciler used by bindable
 * widgets to avoid blowing away DOM with `innerHTML = ''`.
 */

export function getInput(id: string): HTMLInputElement {
    const el = document.getElementById(id);
    if (!el) throw new Error(`Input #${id} not found in DOM`);
    if (!(el instanceof HTMLInputElement)) {
        throw new Error(`Element #${id} is not an HTMLInputElement (got ${el.tagName})`);
    }
    return el;
}

export function tryGetInput(id: string): HTMLInputElement | null {
    const el = document.getElementById(id);
    if (!el || !(el instanceof HTMLInputElement)) return null;
    return el;
}

export function getSelect(id: string): HTMLSelectElement {
    const el = document.getElementById(id);
    if (!el) throw new Error(`Select #${id} not found in DOM`);
    if (!(el instanceof HTMLSelectElement)) {
        throw new Error(`Element #${id} is not an HTMLSelectElement (got ${el.tagName})`);
    }
    return el;
}

export function tryGetSelect(id: string): HTMLSelectElement | null {
    const el = document.getElementById(id);
    if (!el || !(el instanceof HTMLSelectElement)) return null;
    return el;
}

export function getButton(id: string): HTMLButtonElement {
    const el = document.getElementById(id);
    if (!el) throw new Error(`Button #${id} not found in DOM`);
    if (!(el instanceof HTMLButtonElement)) {
        throw new Error(`Element #${id} is not an HTMLButtonElement (got ${el.tagName})`);
    }
    return el;
}

export function getEl<T extends HTMLElement>(id: string, ctor: new () => T): T {
    const el = document.getElementById(id);
    if (!el) throw new Error(`Element #${id} not found in DOM`);
    if (!(el instanceof ctor)) {
        throw new Error(`Element #${id} is not a ${ctor.name} (got ${el.tagName})`);
    }
    return el;
}

export function tryGetEl<T extends HTMLElement>(id: string, ctor: new () => T): T | null {
    const el = document.getElementById(id);
    if (!el || !(el instanceof ctor)) return null;
    return el;
}

export function queryInput(root: ParentNode, selector: string): HTMLInputElement {
    const el = root.querySelector(selector);
    if (!el) throw new Error(`Input matching ${selector} not found`);
    if (!(el instanceof HTMLInputElement)) {
        throw new Error(`Element ${selector} is not an HTMLInputElement`);
    }
    return el;
}

export function tryQueryInput(root: ParentNode, selector: string): HTMLInputElement | null {
    const el = root.querySelector(selector);
    if (!el || !(el instanceof HTMLInputElement)) return null;
    return el;
}

export function querySelect(root: ParentNode, selector: string): HTMLSelectElement {
    const el = root.querySelector(selector);
    if (!el) throw new Error(`Select matching ${selector} not found`);
    if (!(el instanceof HTMLSelectElement)) {
        throw new Error(`Element ${selector} is not an HTMLSelectElement`);
    }
    return el;
}

/**
 * Minimal keyed list reconciler.
 *
 * Avoids the `container.innerHTML = items.map(...).join('')` pattern
 * which (a) re-creates DOM and detaches all event listeners, and (b)
 * makes XSS regressions easy because every interpolation must be
 * escaped manually.
 *
 * @param container Parent element whose children will be reconciled.
 * @param items Source list.
 * @param keyOf Stable identity function.
 * @param build Build a new element for an item.
 * @param update Optional updater for an existing element.
 */
export function reconcileList<T>(
    container: HTMLElement,
    items: readonly T[],
    keyOf: (item: T, index: number) => string,
    build: (item: T, index: number) => HTMLElement,
    update?: (el: HTMLElement, item: T, index: number) => void,
): void {
    const existing = new Map<string, HTMLElement>();
    for (const child of Array.from(container.children)) {
        if (child instanceof HTMLElement) {
            const key = child.dataset.reconcileKey;
            if (key !== undefined) existing.set(key, child);
        }
    }

    const next: HTMLElement[] = [];
    items.forEach((item, idx) => {
        const key = keyOf(item, idx);
        let el = existing.get(key);
        if (el) {
            existing.delete(key);
            if (update) update(el, item, idx);
        } else {
            el = build(item, idx);
            el.dataset.reconcileKey = key;
        }
        next.push(el);
    });

    // Remove leftovers
    for (const el of existing.values()) el.remove();

    // Re-order / insert
    next.forEach((el, idx) => {
        const current = container.children[idx];
        if (current !== el) container.insertBefore(el, current ?? null);
    });
}
