import { disposeGrid, loadAssetsFromList } from "../../grid/gridApi.js";
import {
    loadMajoorSettings,
    saveMajoorSettings,
    applySettingsToConfig,
} from "../../../app/settings/settingsCore.js";
import { EVENTS } from "../../../app/events.js";
import { t } from "../../../app/i18n.js";
import { setSelectionIds, getSelectedIdSet } from "../../grid/GridSelectionManager.js";
import { createRatingBadge } from "../../../components/Badges.js";
import { get } from "../../../api/client.js";
import { buildListURL } from "../../../api/endpoints.js";
import { createPopoverManager } from "../../panel/views/popoverManager.js";
import { APP_CONFIG } from "../../../app/config.js";
import { bindGridContextMenu, showTagsPopover } from "../../contextmenu/GridContextMenu.js";
import { createRatingHotkeysController } from "../../panel/controllers/ratingHotkeysController.js";
import { installGridKeyboard } from "../../grid/GridKeyboard.js";
import { bindCardOverlayButton } from "../../grid/StackGroupCards.js";
import { createMjrApp } from "../../../vue/createVueApp.js";
import VirtualAssetGridHost from "../../../vue/components/grid/VirtualAssetGridHost.vue";

const FEED_FETCH_LIMIT = 240;
const FEED_PAGE_SIZE = 120;
const FEED_PUSH_RENDER_DEBOUNCE_MS = 80;
let viewerOpenRequestModulePromise: Promise<typeof import("../../viewer/viewerOpenRequest.js")> | null = null;
let floatingViewerManagerModulePromise: Promise<typeof import("../../viewer/floatingViewerManager.js")> | null = null;

function loadViewerOpenRequestModule() {
    if (!viewerOpenRequestModulePromise) {
        viewerOpenRequestModulePromise = import("../../viewer/viewerOpenRequest.js");
    }
    return viewerOpenRequestModulePromise;
}

function loadFloatingViewerManagerModule() {
    if (!floatingViewerManagerModulePromise) {
        floatingViewerManagerModulePromise = import("../../viewer/floatingViewerManager.js");
    }
    return floatingViewerManagerModulePromise;
}

function _createFeedMemberCard(asset: any) {
    const card = document.createElement("button");
    card.type = "button";
    card.className = "mjr-asset-card mjr-card mjr-feed-group-member-card";
    card.style.cssText =
        "display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-end;min-height:110px;padding:8px;border:1px solid var(--mjr-border,#444);border-radius:8px;background:var(--mjr-surface,#202020);color:inherit;text-align:left;overflow:hidden;";
    try {
        card.dataset.mjrAssetId = String(asset?.id || "");
    } catch (e) {
        console.debug?.(e);
    }
    const name = document.createElement("div");
    name.className = "mjr-feed-group-member-name";
    name.style.cssText = "font-size:11px;font-weight:600;line-height:1.3;word-break:break-word;";
    name.textContent = String(asset?.filename || "Asset");
    card.appendChild(name);
    return card;
}

function _getRenderedCards(grid: any) {
    try {
        if (typeof grid?._mjrGetRenderedCards === "function") {
            const cards = grid._mjrGetRenderedCards();
            if (Array.isArray(cards)) return cards;
        }
    } catch (e) {
        console.debug?.(e);
    }
    try {
        return Array.from(grid?.querySelectorAll?.(".mjr-asset-card") || []);
    } catch (e) {
        console.debug?.(e);
        return [];
    }
}

function _applyFeedSelection(grid: any, ids: any, activeId = "") {
    const nextIds = Array.isArray(ids) ? ids.map(String).filter(Boolean) : [];
    return setSelectionIds(
        grid,
        nextIds,
        { activeId: activeId || nextIds[0] || "" },
        _getRenderedCards,
    );
}

function _bindFeedSelection(host: any) {
    const grid = host?.grid;
    if (!grid) return;
    const onClick = (event: any) => {
        if (event.defaultPrevented) return;
        const card = event.target?.closest?.(".mjr-asset-card");
        if (!card || !grid.contains(card)) return;
        const interactive = event.target?.closest?.("a, button, input, textarea, select, label");
        if (interactive && interactive !== card && card.contains(interactive)) return;
        const clickedId = String(card.dataset?.mjrAssetId || "").trim();
        if (!clickedId) return;

        try {
            (window as any).__MJR_LAST_SELECTION_GRID__ = grid;
        } catch (e) {
            console.debug?.(e);
        }

        const cards = _getRenderedCards(grid);
        const clickedIndex = cards.indexOf(card);
        const existing = getSelectedIdSet(grid);

        if (event.shiftKey && clickedIndex >= 0) {
            const anchorIndex = Number.isFinite(Number(grid._mjrLastSelectedIndex))
                ? Number(grid._mjrLastSelectedIndex)
                : clickedIndex;
            const start = Math.min(anchorIndex, clickedIndex);
            const end = Math.max(anchorIndex, clickedIndex);
            const rangeIds = cards
                .slice(start, end + 1)
                .map((el) => String(el?.dataset?.mjrAssetId || "").trim())
                .filter(Boolean);
            _applyFeedSelection(grid, rangeIds, clickedId);
            grid._mjrLastSelectedIndex = clickedIndex;
            event.preventDefault();
            event.stopPropagation();
            return;
        }

        if (event.ctrlKey || event.metaKey) {
            if (existing.has(clickedId)) existing.delete(clickedId);
            else existing.add(clickedId);
            const nextIds = Array.from(existing);
            _applyFeedSelection(
                grid,
                nextIds,
                existing.has(clickedId) ? clickedId : nextIds[0] || "",
            );
            if (clickedIndex >= 0) grid._mjrLastSelectedIndex = clickedIndex;
            event.preventDefault();
            event.stopPropagation();
            return;
        }

        _applyFeedSelection(grid, [clickedId], clickedId);
        if (clickedIndex >= 0) grid._mjrLastSelectedIndex = clickedIndex;
        try {
            card.focus?.({ preventScroll: true });
        } catch (e) {
            console.debug?.(e);
        }
        event.preventDefault();
        event.stopPropagation();
    };

    grid.addEventListener("click", onClick, true);
    host._disposeSelection = () => {
        try {
            grid.removeEventListener("click", onClick, true);
        } catch (e) {
            console.debug?.(e);
        }
    };
}

const FEED_TAB_ID = "majoor-generated-feed";
const FEED_STATE = {
    hosts: new Set(),
    pendingAssets: new Map(),
};

function _getAssetGroupKey(asset: any) {
    if (!APP_CONFIG.EXECUTION_GROUPING_ENABLED) {
        return _assetKey(asset);
    }
    const stackId = String(asset?.stack_id || "").trim();
    if (stackId) return `stack:${stackId}`;
    const jobId = String(asset?.job_id || "").trim();
    if (jobId) return `job:${jobId}`;
    return _assetKey(asset);
}

function _groupFeedAssets(assets: any) {
    const groups = new Map();
    for (const raw of Array.isArray(assets) ? assets : []) {
        const asset = _normalizeAsset(raw);
        if (!asset) continue;
        const groupKey = _getAssetGroupKey(asset);
        let entry = groups.get(groupKey);
        if (!entry) {
            entry = { key: groupKey, members: [] };
            groups.set(groupKey, entry);
        }
        entry.members.push(asset);
    }

    const items: any[] = [];
    for (const entry of groups.values()) {
        entry.members.sort((a: any, b: any) => {
            const am = Number(a?.mtime || a?.created_at || 0) || 0;
            const bm = Number(b?.mtime || b?.created_at || 0) || 0;
            return bm - am;
        });
        const cover = entry.members[0];
        items.push({
            ...cover,
            _mjrFeedGroupKey: entry.key,
            _mjrFeedGroupCount: entry.members.length,
            _mjrFeedGroupAssets: entry.members.slice(),
        });
    }

    items.sort((a, b) => {
        const am = Number(a?.mtime || a?.created_at || 0) || 0;
        const bm = Number(b?.mtime || b?.created_at || 0) || 0;
        return bm - am;
    });
    return items;
}

function _getHostAssets(host: any) {
    return Array.from(host?.assetsByKey?.values?.() || []) as any[];
}

function _normalizeAssetIdentityPart(value: any) {
    return String(value ?? "")
        .trim()
        .toLowerCase();
}

function _isLivePlaceholderAsset(asset: any) {
    return (
        asset?._mjrLivePlaceholder === true ||
        asset?.is_live_placeholder === true ||
        String(asset?.id || "")
            .trim()
            .toLowerCase()
            .startsWith("live:")
    );
}

function _clearLivePlaceholderState(asset: any) {
    if (!asset || typeof asset !== "object") return asset;
    try {
        delete asset._mjrLivePlaceholder;
        delete asset._mjrLiveStatus;
        delete asset._mjrLiveLabel;
        delete asset.is_live_placeholder;
    } catch (e) {
        console.debug?.(e);
    }
    return asset;
}

export function getFeedAssetIdentityKey(asset: Record<string, any> | null): string {
    if (!asset || typeof asset !== "object") return "";
    const type = _normalizeAssetIdentityPart(asset?.type || asset?.source || "output");
    const rootId = _normalizeAssetIdentityPart(asset?.root_id || asset?.custom_root_id || "");
    const subfolder = _normalizeAssetIdentityPart(asset?.subfolder || "");
    const filename = _normalizeAssetIdentityPart(asset?.filename || "");
    if (filename) return `${type}|${rootId}|${subfolder}|${filename}`;
    const filepath = _normalizeAssetIdentityPart(
        asset?.filepath || asset?.path || asset?.fullpath || asset?.full_path || "",
    );
    if (!filepath) return "";
    return `${type}|${rootId}|path|${filepath}`;
}

export function mergeFeedAssetIntoMap(assetsByKey: Map<string, any>, asset: Record<string, any> | null): Record<string, any> | null {
    const normalized = _normalizeAsset(asset);
    if (!assetsByKey || typeof assetsByKey.set !== "function" || !normalized) return null;
    const primaryKey = _assetKey(normalized);
    const identity = getFeedAssetIdentityKey(normalized);
    let existingKey = "";
    if (identity) {
        for (const [key, existingAsset] of assetsByKey.entries()) {
            if (getFeedAssetIdentityKey(existingAsset) === identity) {
                existingKey = key;
                break;
            }
        }
    }
    const existing = existingKey ? assetsByKey.get(existingKey) : null;
    const merged = existing ? { ...(existing as Record<string, any>), ...normalized } : normalized;
    if (!_isLivePlaceholderAsset(normalized)) _clearLivePlaceholderState(merged);
    if (existingKey && existingKey !== primaryKey) assetsByKey.delete(existingKey);
    assetsByKey.set(primaryKey, merged);
    return merged;
}

function _storeHostAsset(host: any, asset: any) {
    if (!host) return null;
    if (!host.assetsByKey) host.assetsByKey = new Map();
    return mergeFeedAssetIntoMap(host.assetsByKey, asset);
}

function _scheduleHostRender(host: any) {
    if (!host?.grid || !host.loaded) return;
    if (host._renderTimer) return;
    host._renderTimer = setTimeout(() => {
        host._renderTimer = null;
        void _renderHostAssets(host);
    }, FEED_PUSH_RENDER_DEBOUNCE_MS);
}

function _openGroupViewer(host: any, groupedAsset: any, startIndex = 0) {
    const assets = Array.isArray(groupedAsset?._mjrFeedGroupAssets)
        ? groupedAsset._mjrFeedGroupAssets.filter(Boolean)
        : [groupedAsset].filter(Boolean);
    if (!assets.length) return;
    const safeIndex = Math.max(0, Math.min(startIndex, assets.length - 1));
    const activeAsset = assets[safeIndex] || null;
    const activeId = String(activeAsset?.id || "").trim();
    if (host?.grid && activeId) {
        try {
            (window as any).__MJR_LAST_SELECTION_GRID__ = host.grid;
        } catch (e) {
            console.debug?.(e);
        }
        _applyFeedSelection(host.grid, [activeId], activeId);
    }
    void loadViewerOpenRequestModule()
        .then((mod) => mod.requestViewerOpen({ assets, index: safeIndex }))
        .catch((e) => console.debug?.(e));
}

function _buildGroupPopover(host: any, groupedAsset: any) {
    const popover = document.createElement("div");
    popover.className = "mjr-popover mjr-feed-group-popover";
    popover.style.cssText = "display:none; width:min(440px, 92vw); padding:10px;";

    const title = document.createElement("div");
    title.className = "mjr-feed-group-popover-title";
    title.textContent = t("bottomFeed.groupTitle", "Generation group");
    title.style.cssText = "font-size:12px; font-weight:700; opacity:0.92; margin-bottom:8px;";
    popover.appendChild(title);

    const grid = document.createElement("div");
    grid.className = "mjr-feed-group-popover-grid";
    grid.style.cssText =
        "display:grid; grid-template-columns:repeat(auto-fill, minmax(110px, 1fr)); gap:8px; max-height:360px; overflow-y:auto; overscroll-behavior:contain;";

    const assets = Array.isArray(groupedAsset?._mjrFeedGroupAssets)
        ? groupedAsset._mjrFeedGroupAssets
        : [];
    for (let index = 0; index < assets.length; index += 1) {
        const member = assets[index];
        const card = _createFeedMemberCard(member);
        card.classList.add("mjr-feed-group-member-card");
        if (index === 0) card.classList.add("mjr-feed-group-member-card--cover");
        card.addEventListener("click", (event) => {
            event.preventDefault();
            event.stopPropagation();
            host.popoverManager?.close?.(popover);
            _openGroupViewer(host, groupedAsset, index);
        });
        grid.appendChild(card);
    }

    popover.appendChild(grid);
    host.root.appendChild(popover);
    return popover;
}

function _decorateFeedCard(host: any, card: any, groupedAsset: any) {
    if (!card || !groupedAsset) return;
    const count = Number(groupedAsset?._mjrFeedGroupCount || 0) || 0;
    if (count <= 1) return;

    card.dataset.mjrFeedGrouped = "true";
    card.dataset.mjrFeedGroupCount = String(count);
    card.dataset.mjrStacked = "true";
    card.dataset.mjrStackCount = String(count);

    if (card.querySelector(".mjr-feed-group-button")) return;

    const badge = document.createElement("button");
    badge.type = "button";
    badge.className = "mjr-feed-group-button";
    badge.title = t("bottomFeed.groupOpen", "Show other assets from this generation");
    badge.setAttribute("aria-label", `${count} assets`);
    const icon = document.createElement("span");
    icon.className = "mjr-feed-group-button-icon pi pi-clone";
    badge.appendChild(icon);
    const countEl = document.createElement("span");
    countEl.className = "mjr-feed-group-button-count";
    countEl.textContent = String(count);
    badge.appendChild(countEl);
    badge.style.cssText = "position:absolute; top:34px; right:6px; z-index:11;";
    bindCardOverlayButton(badge);

    const popover = _buildGroupPopover(host, groupedAsset);
    badge.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        host.popoverManager?.toggle?.(popover, badge);
    });

    card.appendChild(badge);
}

function _enhanceGroupedCards(host: any, groupedAssets: any) {
    const groupedById = new Map<string, any>(
        (Array.isArray(groupedAssets) ? groupedAssets : [])
            .map((asset): [string, any] => [String(asset?.id || "").trim(), asset])
            .filter(([id]) => !!id),
    );

    for (const card of _getRenderedCards(host?.grid)) {
        const assetId = String(card?.dataset?.mjrAssetId || "").trim();
        const groupedAsset = groupedById.get(assetId);
        if (!groupedAsset) continue;
        _decorateFeedCard(host, card, groupedAsset);
    }
}

async function _renderHostAssets(host: any) {
    if (!host?.grid) return;
    try {
        host.popoverManager?.closeAll?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        const stale = host.root?.querySelectorAll?.(".mjr-feed-group-popover") || [];
        for (const el of stale) el.remove?.();
    } catch (e) {
        console.debug?.(e);
    }
    const groupedAssets = _groupFeedAssets(_getHostAssets(host));
    const res = await loadAssetsFromList(host.grid, groupedAssets, {
        title: t("bottomFeed.title", "Generated Feed"),
        reset: true,
    });
    _enhanceGroupedCards(host, groupedAssets);
    const count = Number((res as any)?.count || 0);
    if (count <= 0) {
        host.empty.textContent = t("bottomFeed.empty", "No generated assets yet.");
        host.empty.style.display = "";
    } else {
        host.empty.style.display = "none";
    }
}

function _assetKey(asset: any) {
    const id = String(asset?.id || "").trim();
    if (id) return `id:${id}`;
    const filename = String(asset?.filename || "").trim();
    const subfolder = String(asset?.subfolder || "").trim();
    const type = String(asset?.source || asset?.type || "").trim();
    return `file:${type}:${subfolder}:${filename}`;
}

function _detectKindFromExt(filename: any) {
    const ext = String(filename || "")
        .split(".")
        .pop()
        ?.toUpperCase() ?? "";
    const videos = new Set(["MP4", "WEBM", "MOV", "AVI", "MKV"]);
    const audio = new Set(["MP3", "WAV", "OGG", "FLAC", "AAC", "M4A"]);
    const model3d = new Set(["OBJ", "FBX", "GLB", "GLTF", "STL", "PLY", "SPLAT", "KSPLAT", "SPZ"]);
    if (videos.has(ext)) return "video";
    if (audio.has(ext)) return "audio";
    if (model3d.has(ext)) return "model3d";
    return "image";
}

function _normalizeAsset(asset: any) {
    if (!asset || typeof asset !== "object") return null;
    const normalized = { ...asset };
    if (!normalized.type && normalized.source) normalized.type = normalized.source;
    if (!normalized.source && normalized.type) normalized.source = normalized.type;
    if (!normalized.kind) {
        normalized.kind = _detectKindFromExt(normalized.ext || normalized.filename || "");
    }
    return normalized;
}

function _rememberPendingAsset(asset: any) {
    const normalized = _normalizeAsset(asset);
    if (!normalized) return null;
    const key = _assetKey(normalized);
    if (!key) return null;
    const identity = getFeedAssetIdentityKey(normalized);
    let identityExisting: any = null;
    if (identity) {
        for (const [pendingKey, pending] of FEED_STATE.pendingAssets.entries()) {
            if (pendingKey !== key && getFeedAssetIdentityKey(pending) === identity) {
                identityExisting = pending;
                FEED_STATE.pendingAssets.delete(pendingKey);
            }
        }
    }
    const existing = FEED_STATE.pendingAssets.get(key) || identityExisting || null;
    const merged = existing ? { ...(existing as Record<string, any>), ...normalized } : normalized;
    if (!_isLivePlaceholderAsset(normalized)) _clearLivePlaceholderState(merged);
    FEED_STATE.pendingAssets.set(key, merged);
    if (FEED_STATE.pendingAssets.size > 64) {
        const oldest = FEED_STATE.pendingAssets.keys().next().value;
        if (oldest) FEED_STATE.pendingAssets.delete(oldest);
    }
    return merged;
}

function _getPendingAssets() {
    return Array.from(FEED_STATE.pendingAssets.values());
}

async function _applyPendingAssets(host: any) {
    if (!host?.grid) return;
    for (const asset of _getPendingAssets()) {
        _storeHostAsset(host, asset);
    }
    await _renderHostAssets(host);
}

async function _loadHistory(host: any) {
    if (!host?.grid) return;
    host.empty.style.display = "none";
    host.empty.textContent = t("bottomFeed.loading", "Loading recent assets...");
    try {
        host.assetsByKey = new Map();
        const assets: any[] = [];
        let offset = 0;
        while (offset < FEED_FETCH_LIMIT) {
            const url = buildListURL({
                q: "*",
                limit: Math.min(FEED_PAGE_SIZE, FEED_FETCH_LIMIT - offset),
                offset,
                scope: "output",
                sort: "mtime_desc",
                includeTotal: offset === 0,
            });
            const page = await get(url, { timeoutMs: 30_000 });
            if (!page?.ok) {
                throw new Error(String(page?.error || "Failed to load feed assets"));
            }
            const rows = Array.isArray(page?.data?.assets) ? page.data.assets : [];
            if (!rows.length) break;
            for (const row of rows) {
                const normalized = _storeHostAsset(host, row);
                if (normalized) assets.push(normalized);
            }
            if (rows.length < FEED_PAGE_SIZE) break;
            offset += rows.length;
        }

        // Merge pending assets into the host store before the single render pass.
        // This avoids the double-reset that happened when _applyPendingAssets called
        // _renderHostAssets (reset:true) after loadAssetsFromList had already done so,
        // which discarded the _enhanceGroupedCards work done between the two calls.
        for (const asset of _getPendingAssets()) {
            _storeHostAsset(host, asset);
        }
        // Pending assets are now absorbed into host.assetsByKey  -  clear the global buffer
        // so future hosts don't replay the same entries (would cause duplicates).
        FEED_STATE.pendingAssets.clear();

        const allAssets = _getHostAssets(host);
        const grouped = _groupFeedAssets(allAssets);
        const res = await loadAssetsFromList(host.grid, grouped, {
            title: t("bottomFeed.title", "Generated Feed"),
            reset: true,
        });
        _enhanceGroupedCards(host, grouped);

        const count = Number((res as any)?.count || 0);
        if (count <= 0) {
            host.empty.textContent = t("bottomFeed.empty", "No generated assets yet.");
            host.empty.style.display = "";
        } else {
            host.empty.style.display = "none";
        }
    } catch (e) {
        console.debug?.(e);
        host.empty.textContent = t("bottomFeed.loadFailed", "Failed to load generated assets.");
        host.empty.style.display = "";
    } finally {
        host.loaded = true;
    }
}

async function _refreshAllHosts() {
    for (const host of Array.from(FEED_STATE.hosts) as Record<string, any>[]) {
        try {
            await _loadHistory(host);
        } catch (e) {
            console.debug?.(e);
        }
    }
}

export function refreshGeneratedFeedHosts(): void {
    void _refreshAllHosts();
}

function _buildEmpty(host: any) {
    const empty = document.createElement("div");
    empty.textContent = t("bottomFeed.loading", "Loading recent assets...");
    empty.style.cssText = "font-size:11px; opacity:0.65; padding:4px 2px 8px 2px;";
    host.root.appendChild(empty);
    host.empty = empty;
}

function _prepareBottomPanelContainer(container: any) {
    if (!container) return;
    const targets = [container, container.parentElement, container.parentElement?.parentElement];
    for (const el of targets) {
        if (!el || !(el instanceof HTMLElement)) continue;
        try {
            el.style.minHeight = "0";
        } catch (e) {
            console.debug?.(e);
        }
    }
    try {
        container.style.height = "100%";
        container.style.minHeight = "0";
        container.style.overflow = "hidden";
        container.style.display = "flex";
        container.style.flexDirection = "column";
    } catch (e) {
        console.debug?.(e);
    }
}

function _applyFeedSettingsClasses(grid: any) {
    if (!grid) return;
    const toggle = (cls: any, enabled: any) => {
        if (enabled) grid.classList.add(cls);
        else grid.classList.remove(cls);
    };
    toggle("mjr-feed-show-info", APP_CONFIG.FEED_SHOW_INFO);
    toggle("mjr-feed-show-filename", APP_CONFIG.FEED_SHOW_FILENAME);
    toggle("mjr-feed-show-dimensions", APP_CONFIG.FEED_SHOW_DIMENSIONS);
    toggle("mjr-feed-show-date", APP_CONFIG.FEED_SHOW_DATE);
    toggle("mjr-feed-show-gentime", APP_CONFIG.FEED_SHOW_GENTIME);
    toggle("mjr-show-hover-info", APP_CONFIG.GRID_SHOW_HOVER_INFO);
    toggle("mjr-feed-show-dot", APP_CONFIG.FEED_SHOW_WORKFLOW_DOT);
    toggle("mjr-feed-show-badges-ext", APP_CONFIG.FEED_SHOW_BADGES_EXTENSION);
    toggle("mjr-feed-show-badges-rating", APP_CONFIG.FEED_SHOW_BADGES_RATING);
    toggle("mjr-feed-show-badges-tags", APP_CONFIG.FEED_SHOW_BADGES_TAGS);
}

function _buildGrid(host: any, externalGridWrapper = null) {
    const wrapper =
        externalGridWrapper ||
        (() => {
            const el = document.createElement("div");
            return el;
        })();
    wrapper.classList.add("mjr-am-grid-scroll");
    wrapper.style.position = "relative";
    wrapper.style.flex = "1 1 auto";
    wrapper.style.minHeight = "0";
    wrapper.style.height = "100%";
    wrapper.style.overflow = "auto";
    wrapper.style.scrollbarGutter = "stable";
    wrapper.style.overscrollBehavior = "contain";
    (wrapper.style as any).webkitOverflowScrolling = "touch";
    const markActive = () => {
        try {
            (window as any).__MJR_LAST_SELECTION_GRID__ = host.grid;
        } catch (e) {
            console.debug?.(e);
        }
    };
    const openMfv = () => {
        markActive();
        try {
            window.dispatchEvent(new Event(EVENTS.MFV_OPEN));
        } catch (e) {
            console.debug?.(e);
        }
    };
    const mountTarget = document.createElement("div");
    wrapper.appendChild(mountTarget);
    host.root.appendChild(wrapper);

    const { app } = createMjrApp(VirtualAssetGridHost, {
        scrollElement: wrapper,
        virtualize: false,
        applyDefaultSettingsClasses: false,
        // MFV listens on window selection events; feed selection changes must be forwarded too.
        emitWindowSelectionEvents: true,
        onCardRendered: (card: any, asset: any) => {
            try {
                _decorateFeedCard(host, card, asset);
            } catch (e) {
                console.debug?.(e);
            }
        },
        onCardDblclick: () => {
            openMfv();
        },
    });
    const vm = app.mount(mountTarget);
    const grid = (vm as any)?.gridContainer || mountTarget.querySelector?.(".mjr-grid") || null;
    host.gridApp = app;
    host.gridVm = vm;
    host.grid = grid;
    if (!grid) return;

    grid.style.minHeight = "100%";
    grid.style.height = "auto";
    grid.dataset.mjrScope = "output";
    grid.dataset.mjrQuery = "*";
    grid.dataset.mjrSort = "mtime_desc";
    grid.dataset.mjrBottomFeed = "true";
    const feedMinSize = Number(APP_CONFIG.FEED_GRID_MIN_SIZE || APP_CONFIG.GRID_MIN_SIZE || 120);
    grid.dataset.mjrFeedMinItemWidth = String(feedMinSize);
    grid.style.setProperty("--mjr-grid-min-size", `${feedMinSize}px`);
    grid.addEventListener("keydown", (event: any) => {
        const lower = event?.key?.toLowerCase?.() || "";
        if (event?.ctrlKey || event?.metaKey || event?.altKey) return;
        if (lower === "v") {
            event.preventDefault?.();
            event.stopPropagation?.();
            openMfv();
            return;
        }
        if (lower === "c") {
            event.preventDefault?.();
            event.stopPropagation?.();
            markActive();
            try {
                void loadFloatingViewerManagerModule()
                    .then(async ({ floatingViewerManager }) => {
                        await floatingViewerManager.open();
                        floatingViewerManager.toggleCompareAB();
                    })
                    .catch((err) => console.debug?.(err));
            } catch (e) {
                console.debug?.(e);
            }
        }
    });
    grid.addEventListener("pointerdown", markActive, true);
    grid.addEventListener("focusin", markActive, true);
    host._disposeFeedFocus = () => {
        try {
            grid.removeEventListener("pointerdown", markActive, true);
        } catch (e) {
            console.debug?.(e);
        }
        try {
            grid.removeEventListener("focusin", markActive, true);
        } catch (e) {
            console.debug?.(e);
        }
    };
}

function _getFeedSelectedAssets(host: any) {
    const grid = host?.grid;
    if (!grid) return [];
    const selectedIds = getSelectedIdSet(grid);
    if (!selectedIds.size) return [];
    const assets = _getHostAssets(host);
    return assets.filter((asset) => selectedIds.has(String(asset?.id || "")));
}

function _getFeedActiveAsset(host: any) {
    const grid = host?.grid;
    if (!grid) return null;
    const activeId = String(grid.dataset?.mjrSelectedAssetId || "").trim();
    if (!activeId) return _getFeedSelectedAssets(host)[0] || null;
    return _getHostAssets(host).find((asset) => String(asset?.id || "") === activeId) || null;
}

function _safeCssEscapeAttr(value: any) {
    const str = String(value ?? "");
    try {
        if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
            return CSS.escape(str);
        }
    } catch (e) {
        console.debug?.(e);
    }
    return str.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1");
}

function _bindFeedKeyboard(host: any) {
    const grid = host?.grid;
    if (!grid) return;

    const onAssetChanged = () => {
        try {
            _scheduleHostRender(host);
        } catch (e) {
            console.debug?.(e);
        }
    };

    host.keyboard = installGridKeyboard({
        gridContainer: grid,
        getState: () => ({ assets: _groupFeedAssets(_getHostAssets(host)) }),
        getSelectedAssets: () => _getFeedSelectedAssets(host),
        getActiveAsset: () => _getFeedActiveAsset(host),
        onOpenTagsEditor: (asset) => {
            const activeCard =
                grid.querySelector?.(
                    `.mjr-asset-card[data-mjr-asset-id="${_safeCssEscapeAttr(asset?.id || "")}"]`,
                ) || null;
            let rect: any = null;
            try {
                rect = activeCard?.getBoundingClientRect?.() || grid.getBoundingClientRect();
            } catch (e) {
                console.debug?.(e);
            }
            const x = Math.round((rect?.left || 0) + Math.min((rect?.width || 0) * 0.5, 160));
            const y = Math.round((rect?.top || 0) + Math.min((rect?.height || 0) * 0.5, 120));
            showTagsPopover(x, y, asset, onAssetChanged);
        },
        onAssetChanged,
    });
    host.keyboard.bind();

    host.ratingHotkeys = createRatingHotkeysController({
        gridContainer: grid,
        createRatingBadge,
    });
    host.ratingHotkeys.bind();
}

const FEED_SIZE_MIN = 60;
const FEED_SIZE_MAX = 600;
const FEED_SIZE_STEP = 10;

function _applyFeedMinSize(host: any, rawSize: any) {
    const clamped = Math.max(
        FEED_SIZE_MIN,
        Math.min(FEED_SIZE_MAX, Math.round(Number(rawSize) / FEED_SIZE_STEP) * FEED_SIZE_STEP),
    );
    // Apply CSS variable immediately on the grid element for live feedback.
    if (host?.grid) {
        try {
            host.grid.style.setProperty("--mjr-grid-min-size", `${clamped}px`);
        } catch (e) {
            console.debug?.(e);
        }
    }
    // Persist to settings and update APP_CONFIG.
    APP_CONFIG.FEED_GRID_MIN_SIZE = clamped;
    try {
        const settings = loadMajoorSettings();
        settings.feed.minSize = clamped;
        saveMajoorSettings(settings);
        applySettingsToConfig(settings);
    } catch (e) {
        console.debug?.(e);
    }
    // Notify Vue grid to recompute columnCount.
    try {
        window.dispatchEvent(
            new CustomEvent("mjr-settings-changed", { detail: { key: "feed.minSize" } }),
        );
    } catch (e) {
        console.debug?.(e);
    }
    // Update toolbar display.
    if (host?._sizeToolbar) {
        try {
            const slider = host._sizeToolbar.querySelector(".mjr-feed-size-slider");
            const display = host._sizeToolbar.querySelector(".mjr-feed-size-display");
            if (slider) slider.value = String(clamped);
            if (display) display.textContent = `${clamped}px`;
        } catch (e) {
            console.debug?.(e);
        }
    }
    return clamped;
}

function _buildSizeControl(host: any) {
    const currentSize = Math.max(
        FEED_SIZE_MIN,
        Number(APP_CONFIG.FEED_GRID_MIN_SIZE || 120) || 120,
    );

    const toolbar = document.createElement("div");
    toolbar.className = "mjr-feed-toolbar";
    toolbar.style.cssText =
        "display:flex; align-items:center; gap:5px; padding:0 2px 5px 2px; flex-shrink:0; user-select:none;";

    const label = document.createElement("span");
    label.textContent = t("bottomFeed.cardSize", "Cards:");
    label.style.cssText = "font-size:11px; opacity:0.65; white-space:nowrap; flex-shrink:0;";
    toolbar.appendChild(label);

    const btnMinus = document.createElement("button");
    btnMinus.type = "button";
    btnMinus.className = "mjr-feed-size-btn";
    btnMinus.textContent = "-";
    btnMinus.title = t("bottomFeed.cardSizeDecrease", "Decrease card size");
    btnMinus.style.cssText =
        "width:20px; height:20px; padding:0; border:1px solid var(--mjr-border,#555); border-radius:4px; background:var(--mjr-surface,#2a2a2a); color:inherit; cursor:pointer; font-size:14px; line-height:1; flex-shrink:0; display:flex; align-items:center; justify-content:center;";
    toolbar.appendChild(btnMinus);

    const slider = document.createElement("input");
    slider.type = "range";
    slider.className = "mjr-feed-size-slider";
    slider.min = String(FEED_SIZE_MIN);
    slider.max = String(FEED_SIZE_MAX);
    slider.step = String(FEED_SIZE_STEP);
    slider.value = String(currentSize);
    slider.title = t("bottomFeed.cardSizeSlider", "Card size");
    slider.style.cssText =
        "flex:1 1 auto; min-width:40px; max-width:180px; height:4px; cursor:pointer; accent-color:var(--mjr-accent,#64b5f6);";
    toolbar.appendChild(slider);

    const btnPlus = document.createElement("button");
    btnPlus.type = "button";
    btnPlus.className = "mjr-feed-size-btn";
    btnPlus.textContent = "+";
    btnPlus.title = t("bottomFeed.cardSizeIncrease", "Increase card size");
    btnPlus.style.cssText = btnMinus.style.cssText;
    toolbar.appendChild(btnPlus);

    const display = document.createElement("span");
    display.className = "mjr-feed-size-display";
    display.textContent = `${currentSize}px`;
    display.style.cssText =
        "font-size:11px; opacity:0.65; white-space:nowrap; min-width:36px; text-align:right; flex-shrink:0; font-variant-numeric:tabular-nums;";
    toolbar.appendChild(display);

    btnMinus.addEventListener("click", () => {
        _applyFeedMinSize(host, Number(slider.value) - FEED_SIZE_STEP);
    });
    btnPlus.addEventListener("click", () => {
        _applyFeedMinSize(host, Number(slider.value) + FEED_SIZE_STEP);
    });
    slider.addEventListener("input", () => {
        _applyFeedMinSize(host, Number(slider.value));
    });

    host.root.appendChild(toolbar);
    host._sizeToolbar = toolbar;
}

function _makeHost(container: any, external = {}) {
    const root = document.createElement("div");
    root.className = "mjr-assets-manager mjr-bottom-feed";
    root.style.cssText =
        "padding:6px; height:100%; min-height:0; box-sizing:border-box; display:flex; flex-direction:column; overflow:hidden;";
    container.replaceChildren(root);

    const host: Record<string, any> = {
        container,
        root,
        grid: null,
        empty: null,
        loaded: false,
        assetsByKey: new Map(),
        popoverManager: null,
    };
    _buildSizeControl(host);
    _buildEmpty(host);
    _buildGrid(host, (external as any).gridWrapper ?? null);
    _bindFeedSelection(host);
    host.popoverManager = createPopoverManager(root);

    // Bind the same context menu as the main grid.
    // The feed is output-only so scope is always "output"  -  no browser/custom-root
    // items will appear. Delete, rename, rate, tag, find-similar all work normally.
    host._disposeContextMenu = bindGridContextMenu({
        gridContainer: host.grid,
        getState: () => ({ scope: "output" }),
    });
    _bindFeedKeyboard(host);

    // Apply feed display settings and listen for changes
    _applyFeedSettingsClasses(host.grid);
    const onSettingsChanged = () => {
        try {
            _applyFeedSettingsClasses(host.grid);
        } catch (e) {
            console.debug?.(e);
        }
    };
    window.addEventListener("mjr-settings-changed", onSettingsChanged);
    host._disposeFeedSettings = () => {
        try {
            window.removeEventListener("mjr-settings-changed", onSettingsChanged);
        } catch (e) {
            console.debug?.(e);
        }
    };

    return host;
}

export function pushGeneratedAsset(asset: Record<string, any> | null): void {
    const normalized = _rememberPendingAsset(asset);
    if (!normalized) return;
    for (const host of Array.from(FEED_STATE.hosts) as Record<string, any>[]) {
        try {
            if (!host?.grid) continue;
            _storeHostAsset(host, normalized);
            if (!host.loaded) continue;
            host.empty.style.display = "none";
            _scheduleHostRender(host);
        } catch (e) {
            console.debug?.(e);
        }
    }
}

export function createGeneratedFeedHost(container: HTMLElement, external: Record<string, any> = {}): Record<string, any> {
    _prepareBottomPanelContainer(container);
    const host = _makeHost(container, external);
    try {
        (container as any)._mjrGeneratedFeedHost = host;
    } catch (e) {
        console.debug?.(e);
    }
    FEED_STATE.hosts.add(host);
    void _loadHistory(host);
    return host;
}

export function disposeGeneratedFeedHost(host: Record<string, any> | null): void {
    const resolvedHost = host?._mjrGeneratedFeedHost || host || null;
    if (!resolvedHost) return;
    const container = resolvedHost.container || null;
    if (container) {
        try {
            delete (container as any)._mjrGeneratedFeedHost;
        } catch (e) {
            try {
                (container as any)._mjrGeneratedFeedHost = null;
            } catch (err) {
                console.debug?.(err);
            }
            console.debug?.(e);
        }
    }
    FEED_STATE.hosts.delete(resolvedHost);
    try {
        resolvedHost._disposeSelection?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        resolvedHost._disposeFeedFocus?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        resolvedHost._disposeFeedSettings?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        resolvedHost._disposeContextMenu?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        resolvedHost.keyboard?.dispose?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        resolvedHost.ratingHotkeys?.dispose?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        resolvedHost.popoverManager?.dispose?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        resolvedHost.gridApp?.unmount?.();
    } catch (e) {
        console.debug?.(e);
    }
    try {
        if (resolvedHost._renderTimer) {
            clearTimeout(resolvedHost._renderTimer);
            resolvedHost._renderTimer = null;
        }
    } catch (e) {
        console.debug?.(e);
    }
    try {
        disposeGrid(resolvedHost.grid);
    } catch (e) {
        console.debug?.(e);
    }
    try {
        resolvedHost.container?.replaceChildren?.();
    } catch (e) {
        console.debug?.(e);
    }
}

export function getGeneratedFeedBottomPanelTab(): Record<string, any> {
    return {
        id: FEED_TAB_ID,
        title: t("bottomFeed.title", "Generated Feed"),
        icon: "pi pi-images",
        type: "custom",
        render: (el: any) => createGeneratedFeedHost(el),
        destroy: (hostOrEl: any) => disposeGeneratedFeedHost(hostOrEl),
    };
}
