import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useEffect, useMemo, useRef, useState } from "react";
import { CheckCircle2, CircleDashed, Loader2, Plus } from "lucide-react";
import { Button } from "../../components/ui/button";
import { useEditContext } from "../client/editContext";
import { getRuleElements, setActiveRuleForComponent, } from "../services/personalizationService";
import { ActionList } from "./ActionList";
import { ConditionTreeEditor } from "./ConditionTreeEditor";
import { RuleList } from "./RuleList";
import { createId, createRule } from "./utils";
function cloneRule(value) {
    return JSON.parse(JSON.stringify(value));
}
function normalizeId(value) {
    return (value || "").trim().replace(/[{}]/g, "").toLowerCase();
}
const EMPTY_RULE_SET = { rules: [] };
export function PersonalizationPanel({ component, pageItem, readonly, ruleSet: ruleSetProp, ruleSetForKey, ruleSetLoadGen, appliedRuleId, onAppliedRuleChange, onRuleSetChange, onExternalReload, }) {
    const editContext = useEditContext();
    const [elements, setElements] = useState([]);
    const [elementsLoaded, setElementsLoaded] = useState(false);
    // selectedRuleId == the rule currently *applied* to the preview (drives the
    // checkmark and the rule we POST to the backend).
    // expandedRuleId == the accordion body currently shown in the editor. The
    // two are decoupled so the user can collapse a rule's body without losing
    // their applied selection, and can switch the applied rule independently
    // via the status-icon button.
    const [selectedRuleId, setSelectedRuleId] = useState();
    const [expandedRuleId, setExpandedRuleId] = useState();
    const [saving, setSaving] = useState(false);
    const [dirty, setDirty] = useState(false);
    const [replaceUnsupported, setReplaceUnsupported] = useState(false);
    const dirtyRef = useRef(false);
    const savingRef = useRef(false);
    const ruleSet = ruleSetProp ?? EMPTY_RULE_SET;
    useEffect(() => {
        dirtyRef.current = dirty;
    }, [dirty]);
    useEffect(() => {
        savingRef.current = saving;
    }, [saving]);
    // The page model rebuilds after every iframe refresh, so depend on stable
    // scalars rather than the pageItem object reference — otherwise the effect
    // refires on every refresh and the panel flickers.
    const componentId = component?.id;
    const pageItemId = pageItem?.id;
    const pageItemLanguage = pageItem?.language;
    const pageItemVersion = pageItem?.version;
    const componentLoadKey = componentId && pageItemId
        ? `${pageItemId}|${pageItemLanguage || ""}|${pageItemVersion || ""}|${componentId}`
        : undefined;
    const loaded = elementsLoaded &&
        !!ruleSetProp &&
        !!componentLoadKey &&
        ruleSetForKey === componentLoadKey;
    useEffect(() => {
        if (!editContext?.addSocketMessageListener || !componentId || !pageItemId) {
            return;
        }
        return editContext.addSocketMessageListener((message) => {
            if (message.type !== "edit-operation" &&
                message.type !== "operation:completed") {
                return;
            }
            const operation = message.payload || {};
            if (operation.type !== "update-rendering-rules") {
                return;
            }
            const operationComponentId = operation.componentId || operation.ComponentId;
            if (operationComponentId &&
                normalizeId(operationComponentId) !== normalizeId(componentId)) {
                return;
            }
            const mainItem = operation.mainItem || operation.MainItem;
            if (mainItem?.id &&
                normalizeId(mainItem.id) !== normalizeId(pageItemId)) {
                return;
            }
            if (mainItem?.language &&
                pageItemLanguage &&
                mainItem.language !== pageItemLanguage) {
                return;
            }
            if (mainItem?.version &&
                pageItemVersion &&
                Number(mainItem.version) !== Number(pageItemVersion)) {
                return;
            }
            if (dirtyRef.current || savingRef.current) {
                return;
            }
            onExternalReload?.();
        });
    }, [
        editContext?.addSocketMessageListener,
        componentId,
        pageItemId,
        pageItemLanguage,
        pageItemVersion,
        onExternalReload,
    ]);
    useEffect(() => {
        let cancelled = false;
        void getRuleElements()
            .then((nextElements) => {
            if (cancelled)
                return;
            setElements(nextElements);
            setElementsLoaded(true);
        })
            .catch((error) => {
            console.error("Failed to load personalization elements", error);
            if (!cancelled) {
                setElements([]);
                setElementsLoaded(true);
            }
        });
        return () => {
            cancelled = true;
        };
    }, []);
    // When a fresh rule set is loaded (parent fetch completes), seed selection
    // from activeRuleId / first rule. Skip on local edits — those bump the
    // ruleSet identity but not the load generation counter.
    const initializedLoadRef = useRef(undefined);
    useEffect(() => {
        if (!loaded || !ruleSetProp)
            return;
        const initKey = `${componentLoadKey}:${ruleSetLoadGen ?? 0}`;
        if (initializedLoadRef.current === initKey)
            return;
        initializedLoadRef.current = initKey;
        const nextRules = ruleSetProp.rules || [];
        setReplaceUnsupported(false);
        let nextSelected;
        setSelectedRuleId((current) => {
            if (appliedRuleId === null) {
                nextSelected = undefined;
                return undefined;
            }
            if (typeof appliedRuleId === "string") {
                const appliedRule = nextRules.find((rule) => normalizeId(rule.uid) === normalizeId(appliedRuleId));
                nextSelected = appliedRule?.uid;
                return nextSelected;
            }
            if (typeof ruleSetProp.activeRuleId === "string") {
                const activeRule = nextRules.find((rule) => normalizeId(rule.uid) === normalizeId(ruleSetProp.activeRuleId));
                nextSelected = activeRule?.uid;
                return nextSelected;
            }
            if (current && nextRules.some((rule) => rule.uid === current)) {
                nextSelected = current;
                return current;
            }
            nextSelected = nextRules[0]?.uid;
            return nextSelected;
        });
        // On a fresh load, seed expansion to mirror the applied rule so the user
        // sees its conditions/actions immediately. From that point on expansion
        // is driven independently by the user.
        setExpandedRuleId(nextSelected);
    }, [appliedRuleId, loaded, ruleSetProp, componentLoadKey, ruleSetLoadGen]);
    useEffect(() => {
        if (!loaded) {
            onAppliedRuleChange?.(undefined);
            return;
        }
        const validSelectedRuleId = selectedRuleId &&
            ruleSet.rules.some((rule) => rule.uid === selectedRuleId)
            ? selectedRuleId
            : undefined;
        onAppliedRuleChange?.(validSelectedRuleId ?? null);
    }, [loaded, onAppliedRuleChange, ruleSet.rules, selectedRuleId]);
    // Tracks the last rule id we pushed to the server per component, so we can
    // distinguish the initial seed (page already rendering with that rule) from
    // a user-driven change that needs a preview refresh.
    const lastAppliedRuleRef = useRef({});
    useEffect(() => {
        if (!loaded || !componentId || !pageItem || !editContext?.sessionId) {
            return;
        }
        // After a component switch the new ruleSet arrives before the seed effect
        // has reset selectedRuleId. If we ran now, the next pass (once the seed
        // catches up) would see ruleId differ from the previously-stamped value
        // and spuriously trigger a preview refresh. Wait until selectedRuleId is
        // either cleared or actually present in the loaded ruleSet.
        if (selectedRuleId &&
            !ruleSet.rules.some((r) => r.uid === selectedRuleId)) {
            return;
        }
        const previous = lastAppliedRuleRef.current;
        const ruleChanged = previous.key === componentLoadKey && previous.ruleId !== selectedRuleId;
        lastAppliedRuleRef.current = {
            key: componentLoadKey,
            ruleId: selectedRuleId,
        };
        void setActiveRuleForComponent(pageItem, componentId, selectedRuleId, editContext.sessionId)
            .then(() => {
            if (ruleChanged) {
                editContext.requestRefresh?.("immediate");
            }
        })
            .catch((error) => {
            console.error("Failed to set active personalization rule", error);
        });
        // Depend on stable page/component scalars; pageItem identity changes on refresh.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [
        componentId,
        componentLoadKey,
        editContext?.sessionId,
        loaded,
        pageItemId,
        pageItemLanguage,
        pageItemVersion,
        selectedRuleId,
    ]);
    const selectedRule = useMemo(() => ruleSet.rules.find((rule) => rule.uid === selectedRuleId), [ruleSet.rules, selectedRuleId]);
    const protectedMode = !!ruleSet.unsupported && !replaceUnsupported;
    const disabled = readonly || !loaded || saving || protectedMode;
    const updateRules = (rules) => {
        onRuleSetChange?.((current) => ({
            ...current,
            rules,
            unsupported: replaceUnsupported ? false : current.unsupported,
        }));
        setDirty(true);
    };
    useEffect(() => {
        if (!dirty || disabled || !component?.id || !editContext?.operations)
            return;
        const timeout = window.setTimeout(async () => {
            setSaving(true);
            try {
                await editContext.operations.updateRenderingRules(component.id, {
                    rules: ruleSet.rules,
                });
                setDirty(false);
            }
            catch {
                // executeOp already surfaced the backend error toast; keep the panel dirty.
            }
            finally {
                setSaving(false);
            }
        }, 800);
        return () => window.clearTimeout(timeout);
    }, [component?.id, dirty, disabled, editContext?.operations, ruleSet.rules]);
    const addRule = () => {
        const nextRule = createRule(`Rule ${ruleSet.rules.length + 1}`);
        updateRules([...ruleSet.rules, nextRule]);
        setSelectedRuleId(nextRule.uid);
        setExpandedRuleId(nextRule.uid);
    };
    const replaceExistingPersonalization = () => {
        const nextRule = createRule("New rule");
        setReplaceUnsupported(true);
        onRuleSetChange?.(() => ({
            rules: [nextRule],
            unsupported: false,
            unsupportedReasons: [],
        }));
        setSelectedRuleId(nextRule.uid);
        setExpandedRuleId(nextRule.uid);
        setDirty(true);
    };
    if (!component || !pageItem) {
        return (_jsx("div", { className: "text-neutral-grey-50 p-4 text-xs", children: "Select a component to configure personalization." }));
    }
    if (!loaded) {
        return (_jsxs("div", { className: "text-neutral-grey-50 flex items-center gap-2 p-4 text-xs", children: [_jsx(Loader2, { className: "size-3 animate-spin" }), "Loading personalization..."] }));
    }
    const renderRuleBody = (rule) => (_jsxs("div", { className: "space-y-4", children: [_jsxs("section", { className: "space-y-2", children: [_jsxs("h4", { className: "text-2xs text-neutral-grey-50 flex items-center gap-2 font-semibold tracking-wide", children: [_jsx("span", { className: "bg-feedback-green inline-block size-1.5 rounded-full" }), "When"] }), _jsx(ConditionTreeEditor, { condition: rule.conditions, elements: elements, disabled: disabled, onChange: (conditions) => updateRules(ruleSet.rules.map((existing) => existing.uid === rule.uid
                            ? { ...existing, conditions }
                            : existing)) })] }), _jsxs("section", { className: "space-y-2", children: [_jsxs("h4", { className: "text-2xs text-neutral-grey-50 flex items-center gap-2 font-semibold tracking-wide", children: [_jsx("span", { className: "bg-highlight-100 inline-block size-1.5 rounded-full" }), "Then"] }), _jsx(ActionList, { actions: rule.actions || [], elements: elements, disabled: disabled, onChange: (actions) => updateRules(ruleSet.rules.map((existing) => existing.uid === rule.uid ? { ...existing, actions } : existing)) })] })] }));
    return (_jsxs("div", { className: "space-y-4 p-3 text-xs", "data-testid": "personalization-panel", children: [_jsxs("header", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-neutral-grey-100 text-sm font-semibold", children: "Personalization" }), component.name && (_jsx("div", { className: "text-2xs text-neutral-grey-50 truncate", title: component.name, children: component.name }))] }), _jsx(SaveIndicator, { saving: saving, dirty: dirty })] }), readonly && (_jsx("div", { className: "border-border-default bg-neutral-grey-5 text-2xs text-neutral-grey-50 rounded border p-3", children: "Personalization is read-only in the current editor mode." })), protectedMode && (_jsxs("div", { className: "border-feedback-orange bg-feedback-orange-light text-2xs text-feedback-orange space-y-2 rounded border p-3", children: [_jsx("div", { className: "text-xs font-medium", children: "Existing personalization is protected" }), _jsx("div", { children: "This rendering contains rules that Parhelia cannot fully edit yet. Saving is disabled to avoid overwriting Sitecore data." }), ruleSet.unsupportedReasons?.length ? (_jsx("ul", { className: "list-disc space-y-1 pl-4", children: ruleSet.unsupportedReasons.map((reason) => (_jsx("li", { children: reason }, reason))) })) : null, _jsx(Button, { type: "button", size: "xs", variant: "outline", disabled: readonly, onClick: replaceExistingPersonalization, children: "Replace existing personalization" })] })), ruleSet.rules.length === 0 ? (_jsxs("div", { className: "border-border-default space-y-3 rounded border border-dashed p-4 text-center", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-neutral-grey-100 text-xs font-medium", children: "No personalization yet" }), _jsx("div", { className: "text-2xs text-neutral-grey-50", children: "Add rules to show different content to different audiences. Each rule combines conditions with actions." })] }), _jsxs(Button, { type: "button", size: "xs", variant: "default", disabled: disabled, onClick: addRule, children: [_jsx(Plus, { className: "size-3" }), "Add first rule"] })] })) : (_jsxs(_Fragment, { children: [_jsx(RuleList, { rules: ruleSet.rules, selectedRuleId: expandedRuleId, appliedRuleId: selectedRule?.uid, disabled: disabled, onToggle: (uid) => setExpandedRuleId((current) => current === uid ? undefined : uid), onApply: (uid) => setSelectedRuleId(uid), onRename: (uid, name) => updateRules(ruleSet.rules.map((rule) => rule.uid === uid ? { ...rule, name } : rule)), onDelete: (uid) => {
                            const nextRules = ruleSet.rules.filter((rule) => rule.uid !== uid);
                            updateRules(nextRules);
                            if (selectedRuleId === uid) {
                                setSelectedRuleId(nextRules[0]?.uid);
                            }
                            if (expandedRuleId === uid) {
                                setExpandedRuleId(nextRules[0]?.uid);
                            }
                        }, onDuplicate: (uid) => {
                            const source = ruleSet.rules.find((rule) => rule.uid === uid);
                            if (!source)
                                return;
                            const duplicate = {
                                ...cloneRule(source),
                                uid: createId(),
                                name: `${source.name || "Rule"} copy`,
                            };
                            updateRules([...ruleSet.rules, duplicate]);
                            setSelectedRuleId(duplicate.uid);
                            setExpandedRuleId(duplicate.uid);
                        }, onMove: (uid, direction) => {
                            const index = ruleSet.rules.findIndex((rule) => rule.uid === uid);
                            const target = index + direction;
                            if (index < 0 || target < 0 || target >= ruleSet.rules.length)
                                return;
                            const nextRules = [...ruleSet.rules];
                            const [moved] = nextRules.splice(index, 1);
                            if (!moved)
                                return;
                            nextRules.splice(target, 0, moved);
                            updateRules(nextRules);
                        }, onReorder: (fromIndex, toIndex) => {
                            if (fromIndex < 0 ||
                                toIndex < 0 ||
                                fromIndex >= ruleSet.rules.length ||
                                toIndex >= ruleSet.rules.length ||
                                fromIndex === toIndex) {
                                return;
                            }
                            const nextRules = [...ruleSet.rules];
                            const [moved] = nextRules.splice(fromIndex, 1);
                            if (!moved)
                                return;
                            nextRules.splice(toIndex, 0, moved);
                            updateRules(nextRules);
                        }, renderRuleBody: renderRuleBody }), _jsxs(Button, { type: "button", size: "xs", variant: "outline", disabled: disabled, onClick: addRule, className: "w-full justify-center", children: [_jsx(Plus, { className: "size-3" }), "Add rule"] }), _jsxs("button", { type: "button", disabled: disabled, onClick: () => setSelectedRuleId(undefined), title: !selectedRule
                            ? "Currently applied"
                            : "Show the default rendering (no rule applied)", className: `flex w-full items-center gap-1 rounded border px-1.5 py-1.5 text-left text-xs transition-colors ${!selectedRule
                            ? "border-component-default bg-feedback-blue-light/40"
                            : "border-border-default hover:bg-neutral-grey-5 bg-white"} disabled:cursor-not-allowed disabled:opacity-60`, children: [_jsx("span", { "aria-hidden": true, className: "size-5 shrink-0" }), _jsx("span", { "aria-hidden": true, className: "size-5 shrink-0" }), _jsx("span", { "aria-hidden": true, className: "flex size-5 shrink-0 items-center justify-center", children: !selectedRule ? (_jsx(CheckCircle2, { className: "text-component-default size-3.5" })) : (_jsx(CircleDashed, { className: "text-neutral-grey-50 size-3.5" })) }), _jsx("span", { className: "text-neutral-grey-100 min-w-0 flex-1 truncate font-medium", children: "Default rendering" }), _jsx("span", { className: "text-2xs text-neutral-grey-50 shrink-0 pr-1", children: "No personalization" })] })] }))] }));
}
function SaveIndicator({ saving, dirty }) {
    if (saving) {
        return (_jsxs("span", { className: "text-2xs text-neutral-grey-50 inline-flex shrink-0 items-center gap-1", children: [_jsx(Loader2, { className: "size-3 animate-spin" }), "Saving"] }));
    }
    if (dirty) {
        return (_jsxs("span", { className: "text-2xs text-feedback-orange inline-flex shrink-0 items-center gap-1", children: [_jsx(CircleDashed, { className: "size-3" }), "Modified"] }));
    }
    return (_jsxs("span", { className: "text-2xs text-feedback-green inline-flex shrink-0 items-center gap-1", children: [_jsx(CheckCircle2, { className: "size-3" }), "Saved"] }));
}
//# sourceMappingURL=PersonalizationPanel.js.map