import { useCallback, useEffect, useRef, useState } from "react";
import { loadPlaceholderInsertOptions, } from "../services/editService";
import { getPageComponents, } from "../services/contentService";
export function normalizeComponentId(id) {
    return (id || "").replace(/[{}-]/g, "").toLowerCase();
}
function isResolvableItemId(id) {
    return /^[0-9a-f]{32}$/.test(normalizeComponentId(id));
}
function getDisplayName(item) {
    return item?.displayName || item?.name || undefined;
}
export function itemMatchesDescriptor(item, descriptor) {
    const itemLanguage = (item.language || "").toLowerCase();
    const descriptorLanguage = (descriptor.language || "").toLowerCase();
    return (normalizeComponentId(item.id) === normalizeComponentId(descriptor.id) &&
        (!descriptorLanguage || itemLanguage === descriptorLanguage) &&
        (descriptor.version === 0 || item.version === descriptor.version));
}
export function resolveComponentName({ skeletonName, renderingName, skeletonType, componentTypeItem, datasourceTemplateName, datasourceItem, primaryRenderedItem, }) {
    return (skeletonName ||
        renderingName ||
        skeletonType ||
        getDisplayName(componentTypeItem) ||
        datasourceTemplateName ||
        getDisplayName(datasourceItem) ||
        getDisplayName(primaryRenderedItem) ||
        "unknown");
}
export function resolveComponentType({ skeletonType, renderingType, renderingItemName, componentTypeItem, datasourceTemplateName, fallbackName, }) {
    return (skeletonType ||
        renderingType ||
        renderingItemName ||
        getDisplayName(componentTypeItem) ||
        datasourceTemplateName ||
        fallbackName);
}
function getComponentTypeItemDescriptor(typeId, contextItem) {
    if (!isResolvableItemId(typeId)) {
        return undefined;
    }
    return {
        id: typeId || "",
        language: contextItem.language,
        version: 0,
        database: contextItem.database,
    };
}
export function resolveRenderedPageItemDescriptor(skeletonItem, pageItemDescriptor) {
    return {
        ...pageItemDescriptor,
        ...skeletonItem,
        database: skeletonItem.database || pageItemDescriptor?.database,
    };
}
export function usePageModel(itemsRepository, pageItemDescriptor, parheliaSettings, layoutMode = "final") {
    const [page, setPage] = useState();
    const [pageSkeleton, setPageSkeleton] = useState();
    const [insertOptions, setInsertOptions] = useState([]);
    const [insertOptionsRefreshToken, setInsertOptionsRefreshToken] = useState(0);
    // Cache insert options by page + layout mode + placeholder. The same placeholder
    // key can resolve differently for different pages/layout layers.
    const insertOptionsCache = useRef({});
    // The bridge re-emits the skeleton for any observed DOM mutation (hover
    // classes, style toggles, ...) without diffing, so identical snapshots
    // arrive frequently. Drop them here — every accepted skeleton triggers a
    // full model rebuild including pagecomponents/itemstubs fetches. Real edits
    // always pass: the refresh apply rewrites edit_rev in the iframe URL, which
    // the skeleton carries as editRevision.
    const lastSkeletonJsonRef = useRef(undefined);
    const setPageSkeletonDeduped = useCallback((skeleton) => {
        let serialized;
        if (skeleton !== undefined) {
            try {
                // The wire skeleton is acyclic by construction (the bridge strips
                // parentComponent/DOM refs before posting).
                serialized = JSON.stringify(skeleton);
            }
            catch {
                // Not serializable — treat as changed rather than drop the update.
            }
        }
        if (serialized !== undefined &&
            serialized === lastSkeletonJsonRef.current) {
            return;
        }
        lastSkeletonJsonRef.current = serialized;
        setPageSkeleton(skeleton);
    }, []);
    // 2) whenever the actual target page or layout mode changes, reset skeleton, page and clear cache
    useEffect(() => {
        setPage(undefined);
        setPageSkeleton(undefined);
        lastSkeletonJsonRef.current = undefined;
        insertOptionsCache.current = {};
        // Keep the empty-array identity when already empty; an unconditional fresh
        // [] re-renders, which loops if the caller passes a new descriptor object
        // per render.
        setInsertOptions((previous) => (previous.length ? [] : previous));
    }, [pageItemDescriptor, layoutMode]);
    const invalidateInsertOptionsCache = useCallback(() => {
        insertOptionsCache.current = {};
        setInsertOptions([]);
        setInsertOptionsRefreshToken((value) => value + 1);
    }, []);
    // 3) collect all placeholder-keys in a skeleton
    const collectPlaceholderKeys = (skeleton) => {
        const keys = [];
        const collect = (c) => {
            c.placeholders.forEach((ph) => {
                keys.push(ph.key);
                ph.components.forEach(collect);
            });
        };
        collect(skeleton.rootComponent);
        return keys;
    };
    const getInsertOptionsCacheKey = (item, placeholderKey) => [
        item.database || "master",
        item.id,
        item.language,
        item.version,
        layoutMode,
        placeholderKey,
    ].join(":");
    // 4) load only missing insertOptions
    useEffect(() => {
        let cancelled = false;
        const loadInsertOptions = async () => {
            if (!pageSkeleton)
                return;
            const skeleton = pageSkeleton;
            const allKeys = collectPlaceholderKeys(skeleton);
            // find which keys we haven’t cached yet
            const missingKeys = allKeys.filter((k) => !(getInsertOptionsCacheKey(skeleton.item, k) in
                insertOptionsCache.current));
            if (missingKeys.length) {
                const newlyFetched = await loadPlaceholderInsertOptions(skeleton.item, missingKeys, layoutMode);
                if (cancelled) {
                    return;
                }
                // stash them in our cache
                newlyFetched.forEach((opt) => {
                    insertOptionsCache.current[getInsertOptionsCacheKey(skeleton.item, opt.key)] = opt;
                });
            }
            // now build the full list in the original placeholder order
            const optionsInOrder = allKeys
                .map((k) => insertOptionsCache.current[getInsertOptionsCacheKey(skeleton.item, k)])
                .filter((opt) => !!opt);
            // Keep the previous array identity when the content is unchanged (cache
            // entries are stable references). The page-model rebuild effect depends
            // on this reference; a fresh-but-equal list would trigger a second,
            // redundant rebuild for every skeleton update.
            setInsertOptions((previous) => previous.length === optionsInOrder.length &&
                optionsInOrder.every((option, index) => option === previous[index])
                ? previous
                : optionsInOrder);
        };
        loadInsertOptions();
        return () => {
            cancelled = true;
        };
    }, [pageSkeleton, layoutMode, insertOptionsRefreshToken]);
    // updatePageModel awaits several requests with no cancellation; overlapping
    // runs could otherwise setPage out of order. Only the latest run may commit.
    const updateRunRef = useRef(0);
    const updatePageModel = async () => {
        if (!itemsRepository) {
            return;
        }
        const runId = ++updateRunRef.current;
        const isStale = () => updateRunRef.current !== runId;
        //const start = performance.now();
        const itemsToFetch = [];
        if (!pageSkeleton) {
            setPage(undefined);
            return;
        }
        // Prevent building page model if skeleton is from a different page than the current pageItemDescriptor.
        // This avoids showing incorrect "Shared" badges when the skeleton hasn't been updated yet for the current page.
        if (pageItemDescriptor && pageSkeleton.item.id !== pageItemDescriptor.id) {
            return;
        }
        const effectivePageItemDescriptor = resolveRenderedPageItemDescriptor(pageSkeleton.item, pageItemDescriptor);
        let pageComponentsById = new Map();
        let pageComponentsByRenderingItemId = new Map();
        try {
            const pageComponents = await getPageComponents(effectivePageItemDescriptor);
            pageComponentsById = new Map(pageComponents
                .filter((component) => !!component.id)
                .map((component) => [normalizeComponentId(component.id), component]));
            pageComponentsByRenderingItemId = new Map(pageComponents
                .filter((component) => !!component.renderingItemId)
                .map((component) => [
                normalizeComponentId(component.renderingItemId),
                component,
            ]));
        }
        catch {
            // Missing layout metadata only degrades labels; the rendered markers still
            // contain enough structure to build the editable page model.
        }
        if (isStale()) {
            return;
        }
        itemsToFetch.push(pageSkeleton.item);
        // Classic renderings use typeId as the rendering item; Alpaca uses it as
        // the component item's template. Either way, it can provide the type label.
        const collectTypeItem = (typeId) => {
            const descriptor = getComponentTypeItemDescriptor(typeId, effectivePageItemDescriptor);
            if (descriptor) {
                itemsToFetch.push(descriptor);
            }
        };
        const collectItems = (component) => {
            collectTypeItem(component.typeId);
            if (component.datasourceItem)
                itemsToFetch.push({
                    id: component.datasourceItem.id,
                    language: component.datasourceItem.language,
                    version: component.datasourceItem.version,
                });
            component.placeholders
                .flatMap((x) => x.components)
                .forEach((c) => collectItems(c));
            component.items.forEach((i) => itemsToFetch.push(i));
        };
        collectItems(pageSkeleton.rootComponent);
        for (const component of pageComponentsById.values()) {
            if (component.dataSourceItem) {
                itemsToFetch.push(component.dataSourceItem);
            }
            collectTypeItem(component.renderingItemId);
        }
        // Pass pageItem to getItemsStubs so it can be included in the request for authorization
        // This is critical for limited preview users - datasources need pageItem context to be authorized
        let itemStubs;
        try {
            itemStubs = await itemsRepository.getItemsStubs(itemsToFetch, effectivePageItemDescriptor);
        }
        catch (e) {
            return;
        }
        if (isStale()) {
            return;
        }
        const getItem = (descriptor) => itemStubs.find((x) => itemMatchesDescriptor(x, descriptor));
        const pageItem = await itemsRepository.getItem(pageSkeleton.item);
        if (isStale()) {
            return;
        }
        if (!pageItem) {
            setPage(undefined);
            return;
        }
        const buildModel = (skeleton, parentPlaceholder) => {
            const pageComponent = pageComponentsById.get(normalizeComponentId(skeleton.id));
            const renderingTypeComponent = pageComponent ||
                pageComponentsByRenderingItemId.get(normalizeComponentId(skeleton.typeId));
            const serverDatasourceItem = pageComponent?.dataSourceItem;
            const datasourceSkeleton = skeleton.datasourceItem ||
                (serverDatasourceItem
                    ? skeleton.items.find((item) => itemMatchesDescriptor(item, serverDatasourceItem)) || { ...serverDatasourceItem, renderedFieldIds: [] }
                    : undefined);
            const datasourceItemTemp = datasourceSkeleton
                ? getItem(datasourceSkeleton)
                : undefined;
            const datasourceItem = datasourceSkeleton && datasourceItemTemp
                ? { ...datasourceSkeleton, ...datasourceItemTemp }
                : undefined;
            const renderingName = renderingTypeComponent?.renderingItemName ||
                renderingTypeComponent?.name ||
                renderingTypeComponent?.type;
            const componentTypeItemDescriptor = getComponentTypeItemDescriptor(skeleton.typeId || pageComponent?.renderingItemId, effectivePageItemDescriptor);
            const componentTypeItem = componentTypeItemDescriptor
                ? getItem(componentTypeItemDescriptor)
                : undefined;
            const mapItem = (x) => {
                const item = getItem(x);
                if (!item)
                    return null;
                return { ...x, ...item };
            };
            const items = skeleton.items.map(mapItem).filter((x) => x !== null);
            const primaryRenderedItem = items[0];
            const name = resolveComponentName({
                skeletonName: skeleton.name || undefined,
                renderingName,
                skeletonType: skeleton.type || undefined,
                componentTypeItem,
                datasourceTemplateName: datasourceItem?.templateName,
                datasourceItem,
                primaryRenderedItem,
            });
            const placeholders = [];
            const component = {
                ...skeleton,
                name,
                icon: datasourceItem?.icon,
                datasourceItem,
                items,
                placeholders,
                // Prefer server-provided shared state to avoid client-side path
                // heuristics misclassifying page-local datasources as shared.
                isShared: skeleton.isShared ?? false,
                hasPersonalization: skeleton.hasPersonalization ?? pageComponent?.hasPersonalization,
                parentPlaceholder,
                editable: skeleton.editable ?? true,
                type: resolveComponentType({
                    skeletonType: skeleton.type || undefined,
                    renderingType: renderingTypeComponent?.type,
                    renderingItemName: renderingTypeComponent?.renderingItemName,
                    componentTypeItem,
                    datasourceTemplateName: datasourceItem?.templateName,
                    fallbackName: name,
                }),
                typeId: skeleton.typeId ||
                    pageComponent?.renderingItemId ||
                    datasourceItem?.templateId ||
                    "",
                canBeMoved: !datasourceItem ||
                    datasourceItem?.id !== parentPlaceholder?.parentComponent?.id,
            };
            component.placeholders = skeleton.placeholders.map((x) => {
                const options = insertOptions.find((y) => y.key === x.key);
                const editable = x.editable ?? options?.editable ?? false;
                const insOpts = options?.insertOptions ?? [];
                const placeholder = {
                    ...x,
                    components: [],
                    canSynchronize: false,
                    parentComponent: component,
                    insertOptions: insOpts,
                    editable,
                    placeholderSettingsItem: options?.placeholderSettingsItem,
                };
                placeholder.components = x.components
                    .map((y) => buildModel(y, placeholder))
                    .filter((x) => x !== undefined);
                return placeholder;
            });
            return component;
        };
        const rootComponent = buildModel(pageSkeleton.rootComponent);
        if (!rootComponent) {
            setPage(undefined);
            return;
        }
        const page = {
            item: pageItem,
            rootComponent,
            //    revision: itemsRepository.revision,
            editRevision: pageSkeleton.editRevision,
        };
        setPage(page);
    };
    useEffect(() => {
        updatePageModel();
        // itemsRepository is recreated whenever its internal revision bumps (item
        // edits, save acks). Before the skeleton dedup above, the bridge's
        // mutation-driven skeleton re-posts re-ran this effect constantly and
        // incidentally picked up fresh repository data; with those re-posts
        // deduped, the repository itself must trigger the rebuild.
    }, [pageSkeleton, insertOptions, itemsRepository]);
    return {
        setPageSkeleton: setPageSkeletonDeduped,
        page,
        invalidateInsertOptionsCache,
    };
}
//# sourceMappingURL=pageModelBuilder.js.map