"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Map from "@/components/Map";
import Editor from "@/components/Editor";
import BackgroundLayersPanel from "@/components/BackgroundLayersPanel";
import TimelineBar from "@/components/TimelineBar";
import SelectedGeometryPanel from "@/components/SelectedGeometryPanel";
import { Entity, fetchEntities, searchEntitiesByName } from "@/api/entities";
import { ApiError } from "@/api/http";
import { fetchGeometriesByBBox } from "@/api/geometries";
import {
    createSection,
    createSectionCommit,
    fetchSections,
    fetchSectionCommits,
    openSectionEditor,
    restoreSectionCommit,
    Section,
    SectionCommit,
    SectionState,
    submitSection,
} from "@/api/sections";
import {
    Change,
    Feature,
    FeatureCollection,
    useEditorState,
} from "@/lib/useEditorState";
import {
    BACKGROUND_LAYER_OPTIONS,
    BackgroundLayerId,
    BackgroundLayerVisibility,
    DEFAULT_BACKGROUND_LAYER_VISIBILITY,
    HIDDEN_BACKGROUND_LAYER_VISIBILITY,
} from "@/lib/backgroundLayers";
import {
    DEFAULT_ENTITY_TYPE_ID,
    ENTITY_TYPE_OPTIONS,
    EntityTypeGroupId,
    findEntityTypeOption,
} from "@/lib/entityTypeOptions";

const EMPTY_FC: FeatureCollection = { type: "FeatureCollection", features: [] };
const WORLD_BBOX = {
    minLng: -180,
    minLat: -90,
    maxLng: 180,
    maxLat: 90,
} as const;
const CURRENT_YEAR = new Date().getUTCFullYear();
const FALLBACK_TIMELINE_RANGE: TimelineRange = {
    min: CURRENT_YEAR - 5000,
    max: CURRENT_YEAR + 100,
};
const TIMELINE_DEBOUNCE_MS = 180;
const BACKGROUND_LAYER_VISIBILITY_STORAGE_KEY = "uhm.backgroundLayerVisibility.v1";
const DEFAULT_SECTION_TITLE = "Main editor section";
const DEFAULT_EDITOR_USER_ID = "local-editor";

type TimelineRange = {
    min: number;
    max: number;
};

type EntityFormState = {
    name: string;
    slug: string;
    type_id: string;
};

type GeometryMetaFormState = {
    time_start: string;
    time_end: string;
    binding: string;
};

type PendingEntityCreate = {
    id: string;
    name: string;
    slug: string | null;
    type_id: string;
    status: number;
};

type EditorSnapshot = {
    schema_version: number;
    section: {
        id: string;
        title: string;
    };
    editor_feature_collection?: FeatureCollection;
    entities?: Array<Record<string, unknown>>;
    geometries?: Array<Record<string, unknown>>;
    link_scopes?: Array<Record<string, unknown>>;
};

export default function Page() {
    const [mode, setMode] = useState<"idle" | "draw" | "select" | "add-point" | "add-line" | "add-path" | "add-circle">("idle");
    const [initialData, setInitialData] = useState<FeatureCollection>(EMPTY_FC);
    const [isSaving, setIsSaving] = useState(false);
    const [isSubmitting, setIsSubmitting] = useState(false);
    const [isOpeningSection, setIsOpeningSection] = useState(false);
    const [availableSections, setAvailableSections] = useState<Section[]>([]);
    const [selectedSectionId, setSelectedSectionId] = useState("");
    const [newSectionTitle, setNewSectionTitle] = useState("");
    const [commitTitle, setCommitTitle] = useState("");
    const [commitNote, setCommitNote] = useState("");
    const [editorUserIdInput, setEditorUserIdInput] = useState(DEFAULT_EDITOR_USER_ID);
    const [activeSection, setActiveSection] = useState<Section | null>(null);
    const [sectionState, setSectionState] = useState<SectionState | null>(null);
    const [sectionCommits, setSectionCommits] = useState<SectionCommit[]>([]);
    const [lastSectionSnapshot, setLastSectionSnapshot] = useState<EditorSnapshot | null>(null);
    const [persistedEntities, setPersistedEntities] = useState<Entity[]>([]);
    const [pendingEntityCreates, setPendingEntityCreates] = useState<PendingEntityCreate[]>([]);
    const [createdEntities, setCreatedEntities] = useState<Array<{
        id: string;
        name: string;
        type_id?: string | null;
    }>>([]);
    const [entityStatus, setEntityStatus] = useState<string | null>(null);
    const [selectedFeatureId, setSelectedFeatureId] = useState<string | number | null>(null);
    const [entityForm, setEntityForm] = useState<EntityFormState>({
        name: "",
        slug: "",
        type_id: DEFAULT_ENTITY_TYPE_ID,
    });
    const [selectedGeometryEntityIds, setSelectedGeometryEntityIds] = useState<string[]>([]);
    const [geometryMetaForm, setGeometryMetaForm] = useState<GeometryMetaFormState>({
        time_start: "",
        time_end: "",
        binding: "",
    });
    const [isEntitySubmitting, setIsEntitySubmitting] = useState(false);
    const [entityFormStatus, setEntityFormStatus] = useState<string | null>(null);
    const [entitySearchQuery, setEntitySearchQuery] = useState("");
    const [entitySearchResults, setEntitySearchResults] = useState<Entity[]>([]);
    const [selectedSearchEntityId, setSelectedSearchEntityId] = useState<string | null>(null);
    const [isEntitySearchLoading, setIsEntitySearchLoading] = useState(false);
    const [timelineRange, setTimelineRange] = useState<TimelineRange>(FALLBACK_TIMELINE_RANGE);
    const [timelineWindowStart, setTimelineWindowStart] = useState<number>(FALLBACK_TIMELINE_RANGE.min);
    const [timelineWindowEnd, setTimelineWindowEnd] = useState<number>(FALLBACK_TIMELINE_RANGE.max);
    const [timelineYear, setTimelineYear] = useState<number>(() =>
        clampYearValue(CURRENT_YEAR, FALLBACK_TIMELINE_RANGE.min, FALLBACK_TIMELINE_RANGE.max)
    );
    const [timelineDraftYear, setTimelineDraftYear] = useState<number>(() =>
        clampYearValue(CURRENT_YEAR, FALLBACK_TIMELINE_RANGE.min, FALLBACK_TIMELINE_RANGE.max)
    );
    const [isTimelineReady, setIsTimelineReady] = useState(false);
    const [isTimelineLoading, setIsTimelineLoading] = useState(false);
    const [timelineStatus, setTimelineStatus] = useState<string | null>(null);
    const [backgroundVisibility, setBackgroundVisibility] = useState<BackgroundLayerVisibility>(
        () => ({ ...HIDDEN_BACKGROUND_LAYER_VISIBILITY })
    );
    const [isBackgroundVisibilityReady, setIsBackgroundVisibilityReady] = useState(false);
    const timelineFetchRequestRef = useRef(0);
    const entitySearchRequestRef = useRef(0);
    const timelineYearRef = useRef(timelineYear);
    const editorUserIdRef = useRef(DEFAULT_EDITOR_USER_ID);

    const editor = useEditorState(initialData);
    const editorUserId = normalizeEditorUserId(editorUserIdInput);
    const entities = useMemo(
        () => mergeEntitiesWithPending(persistedEntities, pendingEntityCreates),
        [persistedEntities, pendingEntityCreates]
    );
    const selectedFeature =
        selectedFeatureId === null
            ? null
            : editor.draft.features.find((feature) =>
                String(feature.properties.id) === String(selectedFeatureId)
            ) || null;

    const createdGeometries = useMemo(() => {
        const rows: Array<{
            id: string | number;
            geometryType: string;
            semanticType?: string | null;
            entityNames: string[];
        }> = [];

        for (const change of editor.changes.values()) {
            if (change.action !== "create") continue;
            const feature = change.feature;
            const entityNames = normalizeFeatureEntityIds(feature)
                .map((entityId) => entities.find((entity) => entity.id === entityId)?.name || entityId);

            rows.push({
                id: feature.properties.id,
                geometryType: feature.geometry.type,
                semanticType: feature.properties.type || getDefaultTypeIdForFeature(feature),
                entityNames,
            });
        }

        return rows;
    }, [editor.changes, entities]);

    useEffect(() => {
        timelineYearRef.current = timelineYear;
    }, [timelineYear]);

    useEffect(() => {
        editorUserIdRef.current = editorUserId;
    }, [editorUserId]);

    const openSectionForEditing = useCallback(async (sectionId: string) => {
        const editorPayload = await openSectionEditor(sectionId, editorUserIdRef.current);
        const snapshot = normalizeEditorSnapshot(editorPayload.snapshot);
        const commits = await fetchSectionCommits(sectionId);

        let nextInitialData = snapshot?.editor_feature_collection || null;
        if (!nextInitialData) {
            nextInitialData = await fetchGeometriesByBBox({
                ...WORLD_BBOX,
                time: timelineYearRef.current,
            });
        }

        setActiveSection(editorPayload.section);
        setSelectedSectionId(editorPayload.section.id);
        setSectionState(editorPayload.state);
        setLastSectionSnapshot(snapshot);
        setInitialData(nextInitialData);
        setSectionCommits(commits);
        setPendingEntityCreates([]);
        setCreatedEntities([]);
        setSelectedFeatureId(null);
        setEntityFormStatus(null);
    }, []);

    useEffect(() => {
        let disposed = false;

        async function loadSection() {
            try {
                setIsOpeningSection(true);
                const sections = await fetchSections();
                if (disposed) return;
                setAvailableSections(sections);

                const section = await resolveDefaultSection(sections, editorUserIdRef.current);
                if (disposed) return;
                setAvailableSections((prev) =>
                    prev.some((item) => item.id === section.id)
                        ? prev
                        : [section, ...prev]
                );

                try {
                    await openSectionForEditing(section.id);
                } catch (err) {
                    if (!(err instanceof ApiError) || err.status !== 409) {
                        throw err;
                    }

                    const fallbackSection = await resolveOpenableSectionAfterConflict(
                        sections,
                        section.id,
                        editorUserIdRef.current
                    );
                    if (disposed) return;
                    setAvailableSections((prev) =>
                        prev.some((item) => item.id === fallbackSection.id)
                            ? prev
                            : [fallbackSection, ...prev]
                    );
                    await openSectionForEditing(fallbackSection.id);
                    if (!disposed) {
                        setEntityStatus("Section mặc định đang bị lock, đã mở section khác để chỉnh sửa.");
                    }
                }
            } catch (err) {
                if (disposed) return;
                if (err instanceof ApiError) {
                    setEntityStatus(`Không tải được section editor: ${err.body || err.message}`);
                } else {
                    console.error("Load section editor failed", err);
                    setEntityStatus("Không tải được section editor.");
                }
            } finally {
                if (!disposed) {
                    setIsOpeningSection(false);
                }
            }
        }

        loadSection();

        return () => {
            disposed = true;
        };
    }, [openSectionForEditing]);

    useEffect(() => {
        let disposed = false;

        async function loadEntities() {
            try {
                const rows = await fetchEntities();
                if (disposed) return;

                setPersistedEntities(rows);
                setEntityStatus(null);
            } catch (err) {
                if (disposed) return;
                console.error("Load entities failed", err);
                setEntityStatus("Không tải được danh sách entity.");
            }
        }

        loadEntities();

        return () => {
            disposed = true;
        };
    }, []);

    useEffect(() => {
        if (!selectedFeature) {
            setEntitySearchResults([]);
            setSelectedSearchEntityId(null);
            setIsEntitySearchLoading(false);
            return;
        }

        const keyword = entitySearchQuery.trim();
        if (!keyword.length) {
            setEntitySearchResults([]);
            setSelectedSearchEntityId(null);
            setIsEntitySearchLoading(false);
            return;
        }

        let disposed = false;
        const requestId = ++entitySearchRequestRef.current;
        const timeoutId = window.setTimeout(async () => {
            setIsEntitySearchLoading(true);
            try {
                const rows = await searchEntitiesByName(keyword, { limit: 30 });
                if (disposed || requestId !== entitySearchRequestRef.current) return;

                const pendingMatches = pendingEntityCreates
                    .filter((entity) =>
                        entity.name.toLowerCase().includes(keyword.toLowerCase()) ||
                        (entity.slug || "").toLowerCase().includes(keyword.toLowerCase())
                    )
                    .map<Entity>((entity) => ({
                        id: entity.id,
                        name: entity.name,
                        slug: entity.slug,
                        type_id: entity.type_id,
                        status: entity.status,
                        geometry_count: 0,
                    }));

                const mergedRows = mergeEntitySearchResults(rows, pendingMatches);
                setEntitySearchResults(mergedRows);
                setSelectedSearchEntityId((prev) =>
                    prev && mergedRows.some((entity) => entity.id === prev)
                        ? prev
                        : mergedRows[0]?.id || null
                );
            } catch (err) {
                if (disposed || requestId !== entitySearchRequestRef.current) return;
                console.error("Search entity by name failed", err);
                const pendingMatches = pendingEntityCreates
                    .filter((entity) =>
                        entity.name.toLowerCase().includes(keyword.toLowerCase()) ||
                        (entity.slug || "").toLowerCase().includes(keyword.toLowerCase())
                    )
                    .map<Entity>((entity) => ({
                        id: entity.id,
                        name: entity.name,
                        slug: entity.slug,
                        type_id: entity.type_id,
                        status: entity.status,
                        geometry_count: 0,
                    }));
                setEntitySearchResults(pendingMatches);
                setSelectedSearchEntityId(pendingMatches[0]?.id || null);
            } finally {
                if (!disposed && requestId === entitySearchRequestRef.current) {
                    setIsEntitySearchLoading(false);
                }
            }
        }, 220);

        return () => {
            disposed = true;
            window.clearTimeout(timeoutId);
        };
    }, [entitySearchQuery, selectedFeature, pendingEntityCreates]);

    useEffect(() => {
        if (selectedFeatureId === null) return;
        const stillExists = editor.draft.features.some((feature) =>
            String(feature.properties.id) === String(selectedFeatureId)
        );
        if (!stillExists) {
            setSelectedFeatureId(null);
        }
    }, [editor.draft, selectedFeatureId]);

    useEffect(() => {
        if (!selectedFeature) {
            setSelectedGeometryEntityIds([]);
            setGeometryMetaForm({
                time_start: "",
                time_end: "",
                binding: "",
            });
            setEntitySearchQuery("");
            setEntitySearchResults([]);
            setSelectedSearchEntityId(null);
            setEntityFormStatus(null);
            return;
        }

        const featureEntityIds = normalizeFeatureEntityIds(selectedFeature);
        setSelectedGeometryEntityIds(featureEntityIds);
        setGeometryMetaForm({
            time_start: selectedFeature.properties.time_start != null
                ? String(selectedFeature.properties.time_start)
                : "",
            time_end: selectedFeature.properties.time_end != null
                ? String(selectedFeature.properties.time_end)
                : "",
            binding: normalizeFeatureBindingIds(selectedFeature).join(", "),
        });
        setEntitySearchQuery("");
        setEntitySearchResults([]);
        setSelectedSearchEntityId(null);
        setEntityFormStatus(null);
    }, [selectedFeature]);

    useEffect(() => {
        if (!selectedFeature) return;

        const allowedGroupIds = getAllowedEntityTypeGroupIdsForFeature(selectedFeature);
        const fallbackOption = ENTITY_TYPE_OPTIONS.find((option) =>
            allowedGroupIds.includes(option.groupId)
        );
        if (!fallbackOption) return;

        setEntityForm((prev) => {
            const currentOption = findEntityTypeOption(prev.type_id);
            const isCurrentAllowed = currentOption
                ? allowedGroupIds.includes(currentOption.groupId)
                : false;
            if (isCurrentAllowed || prev.type_id === fallbackOption.value) {
                return prev;
            }
            return {
                ...prev,
                type_id: fallbackOption.value,
            };
        });
    }, [selectedFeature]);

    useEffect(() => {
        let disposed = false;

        async function loadTimelineBounds() {
            setIsTimelineLoading(true);
            try {
                const data = await fetchGeometriesByBBox({
                    ...WORLD_BBOX,
                });
                if (disposed) return;

                const range = deriveTimelineRange(data);
                const initialYear = clampYear(CURRENT_YEAR, range);

                setTimelineRange(range);
                setTimelineWindowStart(range.min);
                setTimelineWindowEnd(range.max);
                setTimelineYear(initialYear);
                setTimelineDraftYear(initialYear);
                setTimelineStatus(null);
            } catch (err) {
                if (disposed) return;
                console.error("Load timeline bounds failed", err);
                const fallbackYear = clampYear(CURRENT_YEAR, FALLBACK_TIMELINE_RANGE);
                setTimelineRange(FALLBACK_TIMELINE_RANGE);
                setTimelineWindowStart(FALLBACK_TIMELINE_RANGE.min);
                setTimelineWindowEnd(FALLBACK_TIMELINE_RANGE.max);
                setTimelineYear(fallbackYear);
                setTimelineDraftYear(fallbackYear);
                setTimelineStatus("Không thể lấy phạm vi thời gian, đang dùng mốc mặc định.");
            } finally {
                if (!disposed) {
                    setIsTimelineLoading(false);
                    setIsTimelineReady(true);
                }
            }
        }

        loadTimelineBounds();

        return () => {
            disposed = true;
        };
    }, []);

    useEffect(() => {
        if (!isTimelineReady) return;

        const timeoutId = window.setTimeout(() => {
            if (timelineDraftYear !== timelineYear) {
                setTimelineYear(timelineDraftYear);
            }
        }, TIMELINE_DEBOUNCE_MS);

        return () => window.clearTimeout(timeoutId);
    }, [timelineDraftYear, timelineYear, isTimelineReady]);

    useEffect(() => {
        setBackgroundVisibility(loadBackgroundLayerVisibilityFromStorage());
        setIsBackgroundVisibilityReady(true);
    }, []);

    useEffect(() => {
        const lower = Math.min(timelineWindowStart, timelineWindowEnd);
        const upper = Math.max(timelineWindowStart, timelineWindowEnd);
        setTimelineDraftYear((prev) => clampYearValue(prev, lower, upper));
    }, [timelineWindowStart, timelineWindowEnd]);

    useEffect(() => {
        if (!isTimelineReady) return;

        let disposed = false;
        const requestId = ++timelineFetchRequestRef.current;

        async function loadByTimeline() {
            setIsTimelineLoading(true);
            setTimelineStatus(null);

            try {
                const data = await fetchGeometriesByBBox({
                    ...WORLD_BBOX,
                    time: timelineYear,
                });

                if (disposed || requestId !== timelineFetchRequestRef.current) return;
                if (!lastSectionSnapshot?.editor_feature_collection) {
                    setInitialData(data);
                }
            } catch (err) {
                if (err instanceof ApiError) {
                    console.error("Load timeline data failed", err.body);
                } else {
                    console.error("Load timeline data failed", err);
                }

                if (!disposed && requestId === timelineFetchRequestRef.current) {
                    setTimelineStatus("Không tải được geometry tại mốc thời gian đã chọn.");
                }
            } finally {
                if (!disposed && requestId === timelineFetchRequestRef.current) {
                    setIsTimelineLoading(false);
                }
            }
        }

        loadByTimeline();

        return () => {
            disposed = true;
        };
    }, [timelineYear, isTimelineReady, lastSectionSnapshot]);

    const handleCommitSection = async () => {
        if (!activeSection || !sectionState) {
            setEntityStatus("Chưa mở được section editor.");
            return;
        }

        const geometryChanges = editor.buildPayload();

        setIsSaving(true);
        setEntityStatus(null);
        try {
            const snapshot = buildEditorSnapshot({
                section: activeSection,
                draft: editor.draft,
                changes: geometryChanges,
                pendingEntities: pendingEntityCreates,
                previousSnapshot: lastSectionSnapshot,
                hasPersistedFeature: editor.hasPersistedFeature,
            });
            const result = await createSectionCommit(activeSection.id, {
                snapshot,
                created_by: editorUserId,
                expected_version: sectionState.version,
                expected_head_commit_id: sectionState.head_commit_id,
                title: commitTitle.trim() || `Commit ${new Date().toLocaleString()}`,
                note: commitNote.trim() || null,
            });

            setSectionState(result.state);
            setLastSectionSnapshot(snapshot);
            setInitialData(editor.draft);
            editor.clearChanges();
            setPendingEntityCreates([]);
            setCreatedEntities([]);
            setCommitTitle("");
            setCommitNote("");
            setSectionCommits(await fetchSectionCommits(activeSection.id));
            setEntityFormStatus(`Đã tạo commit #${result.commit.commit_no}.`);
        } catch (err) {
            if (err instanceof ApiError) {
                console.error("Commit failed", err.body);
                setEntityStatus(`Commit thất bại: ${err.body}`);
                return;
            }
            console.error("Commit error", err);
            setEntityStatus("Commit thất bại.");
        } finally {
            setIsSaving(false);
        }
    };

    const updateBackgroundVisibility = (
        updater: (prev: BackgroundLayerVisibility) => BackgroundLayerVisibility
    ) => {
        setBackgroundVisibility((prev) => {
            const next = updater(prev);
            persistBackgroundLayerVisibility(next);
            return next;
        });
    };

    const handleToggleBackgroundLayer = (id: BackgroundLayerId) => {
        updateBackgroundVisibility((prev) => ({
            ...prev,
            [id]: !prev[id],
        }));
    };

    const handleShowAllBackgroundLayers = () => {
        updateBackgroundVisibility(() => ({ ...DEFAULT_BACKGROUND_LAYER_VISIBILITY }));
    };

    const handleHideAllBackgroundLayers = () => {
        updateBackgroundVisibility(() => ({ ...HIDDEN_BACKGROUND_LAYER_VISIBILITY }));
    };

    const handleTimelineWindowStartChange = (nextYear: number) => {
        const next = clampYearValue(Math.trunc(nextYear), timelineRange.min, timelineRange.max);
        setTimelineWindowStart(Math.min(next, timelineWindowEnd));
    };

    const handleTimelineWindowEndChange = (nextYear: number) => {
        const next = clampYearValue(Math.trunc(nextYear), timelineRange.min, timelineRange.max);
        setTimelineWindowEnd(Math.max(next, timelineWindowStart));
    };

    const handleTimelineYearChange = (nextYear: number) => {
        const lower = Math.min(timelineWindowStart, timelineWindowEnd);
        const upper = Math.max(timelineWindowStart, timelineWindowEnd);
        setTimelineDraftYear(clampYearValue(Math.trunc(nextYear), lower, upper));
    };

    const handleEntityFormChange = (key: keyof EntityFormState, value: string) => {
        setEntityForm((prev) => ({ ...prev, [key]: value }));
    };

    const handleGeometryMetaFormChange = (key: "time_start" | "time_end" | "binding", value: string) => {
        setGeometryMetaForm((prev) => ({ ...prev, [key]: value }));
    };

    const handleEntityIdsChange = (values: string[]) => {
        setSelectedGeometryEntityIds(uniqueEntityIds(values));
    };

    const handleAddSelectedSearchEntity = () => {
        const entityId = selectedSearchEntityId ? selectedSearchEntityId.trim() : "";
        if (!entityId.length) {
            setEntityFormStatus("Hãy chọn một entity từ kết quả search trước.");
            return;
        }

        const next = uniqueEntityIds([...selectedGeometryEntityIds, entityId]);
        setSelectedGeometryEntityIds(next);
        setSelectedSearchEntityId(null);
        setEntityFormStatus(null);
    };

    const parseGeometryMetaFormRange = () => {
        const timeStart = parseOptionalYearInput(geometryMetaForm.time_start, "time_start");
        const timeEnd = parseOptionalYearInput(geometryMetaForm.time_end, "time_end");
        if (timeStart !== null && timeEnd !== null && timeStart > timeEnd) {
            throw new Error("time_start phải <= time_end.");
        }
        return { timeStart, timeEnd };
    };

    const resolveGeometryTypeFromEntityIds = (
        entityIds: string[],
        entityRows: Entity[] = entities
    ): string | null => {
        const primaryEntityId = entityIds[0] || null;
        if (!primaryEntityId) return null;
        const primaryEntity = entityRows.find((entity) => entity.id === primaryEntityId) || null;
        return primaryEntity?.type_id || null;
    };

    const patchSelectedGeometryMetadataLocally = (
        feature: Feature,
        timeStart: number | null,
        timeEnd: number | null,
        bindingIds: string[]
    ) => {
        editor.patchFeatureProperties(feature.properties.id, {
            time_start: timeStart,
            time_end: timeEnd,
            binding: bindingIds,
        });

        setGeometryMetaForm({
            time_start: timeStart != null ? String(timeStart) : "",
            time_end: timeEnd != null ? String(timeEnd) : "",
            binding: bindingIds.join(", "),
        });
    };

    const patchSelectedFeatureEntitiesLocally = (
        feature: Feature,
        entityIds: string[],
        entityRows: Entity[] = entities
    ) => {
        const primaryEntityId = entityIds[0] || null;
        const primaryEntity = primaryEntityId
            ? entityRows.find((entity) => entity.id === primaryEntityId) || null
            : null;
        const nextGeometryType = resolveGeometryTypeFromEntityIds(entityIds, entityRows) || feature.properties.type || null;
        const entityNames = entityIds
            .map((id) => entityRows.find((entity) => entity.id === id)?.name || "")
            .filter((name) => name.length > 0);

        editor.patchFeatureProperties(feature.properties.id, {
            type: nextGeometryType,
            entity_id: primaryEntityId,
            entity_ids: entityIds,
            entity_name: primaryEntity?.name || null,
            entity_names: entityNames,
            entity_type_id: primaryEntity?.type_id || null,
        });

        setSelectedGeometryEntityIds(entityIds);
    };

    const handleApplyGeometryMetadata = async () => {
        if (!selectedFeature) {
            setEntityFormStatus("Hãy chọn một geometry trước.");
            return;
        }

        let timeStart: number | null;
        let timeEnd: number | null;
        let bindingIds: string[];
        try {
            ({ timeStart, timeEnd } = parseGeometryMetaFormRange());
            bindingIds = parseBindingInput(geometryMetaForm.binding);
        } catch (err) {
            setEntityFormStatus(err instanceof Error ? err.message : "Thời gian không hợp lệ.");
            return;
        }

        setIsEntitySubmitting(true);
        setEntityFormStatus(null);
        try {
            patchSelectedGeometryMetadataLocally(selectedFeature, timeStart, timeEnd, bindingIds);
            setEntityFormStatus("Đã cập nhật thuộc tính GEO. Commit khi sẵn sàng.");
        } finally {
            setIsEntitySubmitting(false);
        }
    };

    const handleApplyEntitiesForSelectedGeometry = async () => {
        if (!selectedFeature) {
            setEntityFormStatus("Hãy chọn một geometry trước.");
            return;
        }

        const entityIds = uniqueEntityIds(selectedGeometryEntityIds);
        setIsEntitySubmitting(true);
        setEntityFormStatus(null);
        try {
            patchSelectedFeatureEntitiesLocally(selectedFeature, entityIds);
            setEntityFormStatus("Đã cập nhật danh sách entity. Commit khi sẵn sàng.");
        } catch (err) {
            if (err instanceof ApiError) {
                setEntityFormStatus(`Lưu thất bại: ${err.body}`);
            } else {
                setEntityFormStatus("Lưu thất bại.");
            }
        } finally {
            setIsEntitySubmitting(false);
        }
    };

    const handleCreateEntityOnly = async () => {
        const name = entityForm.name.trim();
        if (!name) {
            setEntityFormStatus("Tên entity là bắt buộc.");
            return;
        }

        const slug = entityForm.slug.trim() || null;
        const typeId = entityForm.type_id || DEFAULT_ENTITY_TYPE_ID;
        const normalizedName = name.toLowerCase();
        const duplicatedName = entities.some((entity) => entity.name.trim().toLowerCase() === normalizedName);
        if (duplicatedName) {
            setEntityFormStatus("Tên entity đã tồn tại.");
            return;
        }
        if (slug) {
            const normalizedSlug = slug.toLowerCase();
            const duplicatedSlug = entities.some((entity) =>
                (entity.slug || "").trim().toLowerCase() === normalizedSlug
            );
            if (duplicatedSlug) {
                setEntityFormStatus("Slug entity đã tồn tại.");
                return;
            }
        }

        const entityId = buildClientEntityId();
        const pendingCreate: PendingEntityCreate = {
            id: entityId,
            name,
            slug,
            type_id: typeId,
            status: 1,
        };

        setIsEntitySubmitting(true);
        setEntityFormStatus(null);
        try {
            setPendingEntityCreates((prev) => [pendingCreate, ...prev]);
            setCreatedEntities((prev) => {
                if (prev.some((item) => item.id === pendingCreate.id)) return prev;
                return [
                    {
                        id: pendingCreate.id,
                        name: pendingCreate.name,
                        type_id: pendingCreate.type_id || null,
                    },
                    ...prev,
                ];
            });

            setEntityForm((prev) => ({
                ...prev,
                name: "",
                slug: "",
            }));
            setEntityStatus(null);
            setEntityFormStatus("Đã thêm entity mới vào danh sách chờ Commit.");

            if (selectedFeature) {
                setEntitySearchQuery(pendingCreate.name);
                setSelectedSearchEntityId(pendingCreate.id);
            }
        } finally {
            setIsEntitySubmitting(false);
        }
    };

    const handleOpenSelectedSection = async () => {
        const sectionId = selectedSectionId.trim();
        if (!sectionId) {
            setEntityStatus("Hãy chọn section để mở.");
            return;
        }
        if (pendingSaveCount > 0) {
            const confirmed = window.confirm("Section hiện tại có thay đổi chưa Commit. Mở section khác sẽ bỏ các thay đổi này. Tiếp tục?");
            if (!confirmed) return;
        }

        setIsOpeningSection(true);
        setEntityStatus(null);
        try {
            await openSectionForEditing(sectionId);
            setEntityStatus("Đã mở section để chỉnh sửa.");
        } catch (err) {
            if (err instanceof ApiError) {
                setEntityStatus(`Mở section thất bại: ${err.body}`);
            } else {
                setEntityStatus("Mở section thất bại.");
            }
        } finally {
            setIsOpeningSection(false);
        }
    };

    const handleCreateAndOpenSection = async () => {
        const title = newSectionTitle.trim();
        if (!title) {
            setEntityStatus("Tên section là bắt buộc.");
            return;
        }
        if (pendingSaveCount > 0) {
            const confirmed = window.confirm("Section hiện tại có thay đổi chưa Commit. Tạo section mới sẽ bỏ các thay đổi này. Tiếp tục?");
            if (!confirmed) return;
        }

        setIsOpeningSection(true);
        setEntityStatus(null);
        try {
            const section = await createSection({
                title,
                user_id: editorUserId,
                created_by: editorUserId,
            });
            const sections = await fetchSections();
            setAvailableSections(sections);
            setNewSectionTitle("");
            await openSectionForEditing(section.id);
            setEntityStatus("Đã tạo và mở section mới.");
        } catch (err) {
            if (err instanceof ApiError) {
                setEntityStatus(`Tạo section thất bại: ${err.body}`);
            } else {
                setEntityStatus("Tạo section thất bại.");
            }
        } finally {
            setIsOpeningSection(false);
        }
    };

    const handleSubmitSection = async () => {
        if (!activeSection || !sectionState?.head_commit_id) {
            setEntityStatus("Chưa có commit để submit.");
            return;
        }
        if (pendingSaveCount > 0) {
            setEntityStatus("Hãy Commit các thay đổi trước khi Submit.");
            return;
        }

        setIsSubmitting(true);
        setEntityStatus(null);
        try {
            const submission = await submitSection(activeSection.id, {
                submitted_by: editorUserId,
                commit_id: sectionState.head_commit_id,
            });
            setSectionState((prev) => prev ? { ...prev, status: "submitted" } : prev);
            setEntityStatus(`Đã submit section, submission ${submission.id}.`);
        } catch (err) {
            if (err instanceof ApiError) {
                setEntityStatus(`Submit thất bại: ${err.body}`);
            } else {
                setEntityStatus("Submit thất bại.");
            }
        } finally {
            setIsSubmitting(false);
        }
    };

    const handleRestoreCommit = async (commitId: string) => {
        if (!activeSection || !sectionState) {
            setEntityStatus("Chưa mở được section editor.");
            return;
        }
        if (pendingSaveCount > 0) {
            setEntityStatus("Hãy Commit hoặc Undo thay đổi hiện tại trước khi Restore.");
            return;
        }

        setIsSaving(true);
        setEntityStatus(null);
        try {
            const result = await restoreSectionCommit(activeSection.id, {
                commit_id: commitId,
                created_by: editorUserId,
                expected_version: sectionState.version,
                expected_head_commit_id: sectionState.head_commit_id,
            });
            const editorPayload = await openSectionEditor(activeSection.id, editorUserId);
            const snapshot = normalizeEditorSnapshot(editorPayload.snapshot);
            setSectionState(result.state);
            setLastSectionSnapshot(snapshot);
            if (snapshot?.editor_feature_collection) {
                setInitialData(snapshot.editor_feature_collection);
            }
            setSectionCommits(await fetchSectionCommits(activeSection.id));
            setEntityFormStatus(`Đã restore thành commit #${result.commit.commit_no}.`);
        } catch (err) {
            if (err instanceof ApiError) {
                setEntityStatus(`Restore thất bại: ${err.body}`);
            } else {
                setEntityStatus("Restore thất bại.");
            }
        } finally {
            setIsSaving(false);
        }
    };

    const pendingSaveCount = editor.changeCount + pendingEntityCreates.length;
    const timelineDisabled = !isTimelineReady || isSaving || pendingSaveCount > 0;
    const timelineStatusText =
        pendingSaveCount > 0
            ? "Commit hoặc Undo hết thay đổi trước khi đổi mốc thời gian."
            : isSaving
                ? "Đang lưu thay đổi..."
                : timelineStatus;

    const handleCreateFeature = (feature: Feature) => {
        editor.createFeature(feature);
        setSelectedFeatureId(feature.properties.id);
    };

    return (
        <div style={{ display: "flex", minHeight: "100vh" }}>
            <Editor
                mode={mode}
                setMode={setMode}
                entityStatus={entityStatus}
                onUndo={editor.undo}
                onCommit={handleCommitSection}
                onSubmit={handleSubmitSection}
                onRestoreCommit={handleRestoreCommit}
                isSaving={isSaving}
                isSubmitting={isSubmitting}
                isOpeningSection={isOpeningSection}
                sectionTitle={activeSection?.title || "Đang tải section"}
                sectionStatus={sectionState?.status || "editing"}
                selectedSectionId={selectedSectionId}
                editorUserId={editorUserIdInput}
                sectionOptions={availableSections}
                newSectionTitle={newSectionTitle}
                commitTitle={commitTitle}
                commitNote={commitNote}
                onEditorUserIdChange={setEditorUserIdInput}
                onSelectedSectionIdChange={setSelectedSectionId}
                onNewSectionTitleChange={setNewSectionTitle}
                onCommitTitleChange={setCommitTitle}
                onCommitNoteChange={setCommitNote}
                onOpenSection={handleOpenSelectedSection}
                onCreateSection={handleCreateAndOpenSection}
                commitCount={sectionCommits.length}
                latestCommitLabel={sectionCommits[0] ? `Head #${sectionCommits[0].commit_no}` : null}
                commits={sectionCommits}
                changesCount={pendingSaveCount}
                undoStack={editor.undoStack}
                createdEntities={createdEntities}
                createdGeometries={createdGeometries}
            />

            <div style={{ flex: 1, position: "relative", minHeight: "100vh" }}>
                {isBackgroundVisibilityReady ? (
                    <Map
                        mode={mode}
                        draft={editor.draft}
                        selectedFeatureId={selectedFeatureId}
                        onSelectFeatureId={setSelectedFeatureId}
                        onCreateFeature={handleCreateFeature}
                        onDeleteFeature={editor.deleteFeature}
                        onUpdateFeature={editor.updateFeature}
                        backgroundVisibility={backgroundVisibility}
                    />
                ) : (
                    <div style={{ width: "100%", height: "100%", background: "#0b1220" }} />
                )}
                <TimelineBar
                    minYear={timelineRange.min}
                    maxYear={timelineRange.max}
                    windowStartYear={timelineWindowStart}
                    windowEndYear={timelineWindowEnd}
                    onWindowStartYearChange={handleTimelineWindowStartChange}
                    onWindowEndYearChange={handleTimelineWindowEndChange}
                    year={timelineDraftYear}
                    onYearChange={handleTimelineYearChange}
                    isLoading={isTimelineLoading}
                    disabled={timelineDisabled}
                    statusText={timelineStatusText}
                />
            </div>

            <BackgroundLayersPanel
                visibility={backgroundVisibility}
                onToggleLayer={handleToggleBackgroundLayer}
                onShowAll={handleShowAllBackgroundLayers}
                onHideAll={handleHideAllBackgroundLayers}
                topContent={
                    <SelectedGeometryPanel
                        selectedFeature={selectedFeature}
                        selectedFeatureEntitySummary={
                            selectedFeature
                                ? formatEntityNamesForDisplay(selectedFeature, entities)
                                : "Chưa gắn"
                        }
                        selectedFeatureBindingSummary={
                            selectedFeature
                                ? formatBindingIdsForDisplay(selectedFeature)
                                : "Không có"
                        }
                        entities={entities}
                        selectedGeometryEntityIds={selectedGeometryEntityIds}
                        onEntityIdsChange={handleEntityIdsChange}
                        entitySearchQuery={entitySearchQuery}
                        onEntitySearchQueryChange={setEntitySearchQuery}
                        entitySearchResults={entitySearchResults}
                        selectedSearchEntityId={selectedSearchEntityId}
                        onSelectSearchEntityId={setSelectedSearchEntityId}
                        onAddSelectedSearchEntity={handleAddSelectedSearchEntity}
                        isEntitySearchLoading={isEntitySearchLoading}
                        entityForm={entityForm}
                        onEntityFormChange={handleEntityFormChange}
                        entityTypeOptions={ENTITY_TYPE_OPTIONS}
                        geometryMetaForm={geometryMetaForm}
                        onGeometryMetaFormChange={handleGeometryMetaFormChange}
                        isEntitySubmitting={isEntitySubmitting}
                        onCreateEntityOnly={handleCreateEntityOnly}
                        onApplyGeometryMetadata={handleApplyGeometryMetadata}
                        onApplyEntitiesForSelectedGeometry={handleApplyEntitiesForSelectedGeometry}
                        changeCount={editor.changeCount}
                        entityFormStatus={entityFormStatus}
                    />
                }
            />
        </div>
    );
}

async function resolveDefaultSection(existingSections?: Section[], userId = DEFAULT_EDITOR_USER_ID): Promise<Section> {
    const sections = existingSections || await fetchSections();

    const existingDefault = sections.find((section) => section.title === DEFAULT_SECTION_TITLE);
    if (existingDefault) {
        return existingDefault;
    }

    return createSection({
        title: DEFAULT_SECTION_TITLE,
        user_id: userId,
        created_by: userId,
        description: "Default section used by the map editor UI.",
    });
}

async function resolveOpenableSectionAfterConflict(
    sections: Section[],
    blockedSectionId: string,
    userId: string
): Promise<Section> {
    const now = Date.now();
    const fallback = sections.find((section) =>
        section.id !== blockedSectionId &&
        section.state.status === "editing" &&
        (
            !section.state.locked_by ||
            section.state.locked_by === userId ||
            (
                section.state.lock_expires_at !== null &&
                Date.parse(section.state.lock_expires_at) <= now
            )
        )
    );
    if (fallback) return fallback;

    return createSection({
        title: `${DEFAULT_SECTION_TITLE} (${userId})`,
        user_id: userId,
        created_by: userId,
        description: "Fallback section created because the default section is locked.",
    });
}

function normalizeEditorUserId(value: string): string {
    const normalized = value.trim();
    return normalized || DEFAULT_EDITOR_USER_ID;
}

function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
    if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
    const snapshot = raw as EditorSnapshot;
    if (
        snapshot.editor_feature_collection &&
        snapshot.editor_feature_collection.type === "FeatureCollection" &&
        Array.isArray(snapshot.editor_feature_collection.features)
    ) {
        return snapshot;
    }
    return {
        ...snapshot,
        editor_feature_collection: undefined,
    };
}

function buildEditorSnapshot(options: {
    section: Section;
    draft: FeatureCollection;
    changes: Change[];
    pendingEntities: PendingEntityCreate[];
    previousSnapshot: EditorSnapshot | null;
    hasPersistedFeature: (id: Feature["properties"]["id"]) => boolean;
}): EditorSnapshot {
    const changedIds = new Set(options.changes.map((change) => String(change.action === "create" ? change.feature.properties.id : change.id)));
    const deletedIds = new Set(
        options.changes
            .filter((change): change is Extract<Change, { action: "delete" }> => change.action === "delete")
            .map((change) => String(change.id))
    );
    const currentDraftIds = new Set(options.draft.features.map((feature) => String(feature.properties.id)));
    const previousFeatures = new globalThis.Map<string, Feature>();
    for (const feature of options.previousSnapshot?.editor_feature_collection?.features || []) {
        previousFeatures.set(String(feature.properties.id), feature);
        if (!currentDraftIds.has(String(feature.properties.id))) {
            deletedIds.add(String(feature.properties.id));
        }
    }
    const previousGeometryOps = new globalThis.Map<string, string>();
    for (const item of options.previousSnapshot?.geometries || []) {
        const id = typeof item.id === "string" || typeof item.id === "number" ? String(item.id) : "";
        const operation = typeof item.operation === "string" ? item.operation : "";
        if (id && operation) previousGeometryOps.set(id, operation);
    }

    const pendingEntityIds = new Set(options.pendingEntities.map((entity) => entity.id));
    const entityRows = new globalThis.Map<string, Record<string, unknown>>();
    for (const item of options.previousSnapshot?.entities || []) {
        const id = typeof item.id === "string" || typeof item.id === "number" ? String(item.id) : "";
        if (id) entityRows.set(id, { ...item });
    }
    for (const entity of options.pendingEntities) {
        entityRows.set(entity.id, {
            id: entity.id,
            operation: "create",
            name: entity.name,
            slug: entity.slug,
            description: null,
            type_id: entity.type_id,
            status: entity.status,
            is_deleted: 0,
        });
    }

    for (const feature of options.draft.features) {
        for (const entityId of normalizeFeatureEntityIds(feature)) {
            if (entityRows.has(entityId)) continue;
            entityRows.set(entityId, {
                id: entityId,
                operation: "reference",
                name: feature.properties.entity_names?.[0] || feature.properties.entity_name || entityId,
                slug: null,
                description: null,
                type_id: feature.properties.entity_type_id || feature.properties.type || DEFAULT_ENTITY_TYPE_ID,
                status: 1,
                is_deleted: 0,
            });
        }
    }

    const geometries: Array<Record<string, unknown>> = options.draft.features.map((feature) => {
        const id = String(feature.properties.id);
        const previousOperation = previousGeometryOps.get(id);
        const previousFeature = previousFeatures.get(id);
        const changedFromPreviousSnapshot = previousFeature
            ? JSON.stringify(previousFeature) !== JSON.stringify(feature)
            : false;
        const operation = previousOperation === "create"
            ? "create"
            : !previousFeature && (options.previousSnapshot || !options.hasPersistedFeature(feature.properties.id))
                ? "create"
                : changedIds.has(id) || changedFromPreviousSnapshot
                    ? "update"
                    : "reference";
        const bbox = getFeatureBBox(feature);
        return {
            id,
            operation,
            type: feature.properties.type || getDefaultTypeIdForFeature(feature),
            draw_geometry: feature.geometry,
            binding: normalizeFeatureBindingIds(feature),
            time_start: feature.properties.time_start ?? null,
            time_end: feature.properties.time_end ?? null,
            bbox: bbox
                ? {
                    min_lng: bbox.minLng,
                    min_lat: bbox.minLat,
                    max_lng: bbox.maxLng,
                    max_lat: bbox.maxLat,
                }
                : null,
            is_deleted: 0,
        };
    });

    for (const id of deletedIds) {
        geometries.push({
            id,
            operation: "delete",
            is_deleted: 1,
        });
    }

    const linkScopes = options.draft.features
        .map((feature) => ({
            geometry_id: String(feature.properties.id),
            operation: "replace",
            entity_ids: normalizeFeatureEntityIds(feature),
        }))
        .filter((scope) => scope.entity_ids.length > 0);

    return {
        schema_version: 1,
        section: {
            id: options.section.id,
            title: options.section.title,
        },
        editor_feature_collection: JSON.parse(JSON.stringify(options.draft)) as FeatureCollection,
        entities: Array.from(entityRows.values()).map((entity) => {
            const id = String(entity.id || "");
            if (pendingEntityIds.has(id)) return entity;
            return entity;
        }),
        geometries,
        link_scopes: linkScopes,
    };
}

function getFeatureBBox(feature: Feature): { minLng: number; minLat: number; maxLng: number; maxLat: number } | null {
    const points = collectCoordinatePairs(feature.geometry.coordinates);
    if (!points.length) return null;
    let minLng = Number.POSITIVE_INFINITY;
    let minLat = Number.POSITIVE_INFINITY;
    let maxLng = Number.NEGATIVE_INFINITY;
    let maxLat = Number.NEGATIVE_INFINITY;
    for (const [lng, lat] of points) {
        minLng = Math.min(minLng, lng);
        minLat = Math.min(minLat, lat);
        maxLng = Math.max(maxLng, lng);
        maxLat = Math.max(maxLat, lat);
    }
    return { minLng, minLat, maxLng, maxLat };
}

function collectCoordinatePairs(value: unknown): Array<[number, number]> {
    if (!Array.isArray(value)) return [];
    if (
        value.length >= 2 &&
        typeof value[0] === "number" &&
        typeof value[1] === "number" &&
        Number.isFinite(value[0]) &&
        Number.isFinite(value[1])
    ) {
        return [[value[0], value[1]]];
    }
    return value.flatMap((item) => collectCoordinatePairs(item));
}

function deriveTimelineRange(collection: FeatureCollection): TimelineRange {
    let min = Number.POSITIVE_INFINITY;
    let max = Number.NEGATIVE_INFINITY;

    for (const feature of collection.features) {
        const { time_start, time_end } = feature.properties;

        if (isYearNumber(time_start)) {
            min = Math.min(min, time_start);
            max = Math.max(max, time_start);
        }

        if (isYearNumber(time_end)) {
            min = Math.min(min, time_end);
            max = Math.max(max, time_end);
        }
    }

    if (!Number.isFinite(min) || !Number.isFinite(max)) {
        return FALLBACK_TIMELINE_RANGE;
    }

    return {
        min: Math.floor(min),
        max: Math.ceil(max),
    };
}

function clampYear(year: number, range: TimelineRange): number {
    return clampYearValue(year, range.min, range.max);
}

function clampYearValue(year: number, minYear: number, maxYear: number): number {
    const lower = Math.min(minYear, maxYear);
    const upper = Math.max(minYear, maxYear);
    if (year < lower) return lower;
    if (year > upper) return upper;
    return year;
}

function isYearNumber(value: number | null | undefined): value is number {
    return typeof value === "number" && Number.isFinite(value);
}

function getDefaultTypeIdForFeature(feature: Feature): string {
    const preset = feature.properties.geometry_preset;
    if (preset === "line") return "defense_line";
    if (preset === "point") return "city";
    if (preset === "circle-area") return "war";
    if (preset === "polygon") return DEFAULT_ENTITY_TYPE_ID;

    const geometryType = feature.geometry.type;
    if (geometryType === "LineString" || geometryType === "MultiLineString") {
        return "defense_line";
    }
    if (geometryType === "Point" || geometryType === "MultiPoint") {
        return "city";
    }
    return DEFAULT_ENTITY_TYPE_ID;
}

function getAllowedEntityTypeGroupIdsForFeature(feature: Feature): EntityTypeGroupId[] {
    const defaultTypeId = getDefaultTypeIdForFeature(feature);
    const defaultTypeOption = findEntityTypeOption(defaultTypeId);
    if (defaultTypeOption) {
        return [defaultTypeOption.groupId];
    }
    return ["polygon"];
}

function parseOptionalYearInput(raw: string, fieldName: string): number | null {
    const value = raw.trim();
    if (!value.length) return null;
    const parsed = Number(value);
    if (!Number.isFinite(parsed)) {
        throw new Error(`${fieldName} phải là số.`);
    }
    return Math.trunc(parsed);
}

function normalizeFeatureEntityIds(feature: Feature): string[] {
    const fromArray = Array.isArray(feature.properties.entity_ids)
        ? feature.properties.entity_ids.filter((id): id is string => typeof id === "string" && id.trim().length > 0)
        : [];

    if (fromArray.length) {
        return uniqueEntityIds(fromArray);
    }

    const single = feature.properties.entity_id;
    if (typeof single === "string" && single.trim().length > 0) {
        return [single.trim()];
    }

    return [];
}

function normalizeFeatureBindingIds(feature: Feature): string[] {
    const rawBinding = feature.properties.binding;
    if (!Array.isArray(rawBinding)) return [];
    return uniqueEntityIds(rawBinding
        .map((id) => {
            if (typeof id !== "string" && typeof id !== "number") return "";
            return String(id).trim();
        })
        .filter((id) => id.length > 0));
}

function parseBindingInput(raw: string): string[] {
    if (!raw.trim().length) return [];
    return uniqueEntityIds(
        raw
            .split(/[,\n]/)
            .map((item) => item.trim())
            .filter((item) => item.length > 0)
    );
}

function formatBindingIdsForDisplay(feature: Feature): string {
    const bindingIds = normalizeFeatureBindingIds(feature);
    if (!bindingIds.length) return "Không có";
    return bindingIds.join(", ");
}

function uniqueEntityIds(ids: string[]): string[] {
    const deduped: string[] = [];
    const seen = new Set<string>();
    for (const rawId of ids) {
        const id = rawId.trim();
        if (!id || seen.has(id)) continue;
        seen.add(id);
        deduped.push(id);
    }
    return deduped;
}

function formatEntityNamesForDisplay(feature: Feature, entities: Entity[]): string {
    const entityIds = normalizeFeatureEntityIds(feature);
    if (!entityIds.length) return "Chưa gắn";

    const names = entityIds
        .map((id) => entities.find((entity) => entity.id === id)?.name || id)
        .filter((name) => name.trim().length > 0);
    return names.join(", ");
}

function mergeEntitiesWithPending(
    persistedEntities: Entity[],
    pendingCreates: PendingEntityCreate[]
): Entity[] {
    if (!pendingCreates.length) {
        return persistedEntities;
    }

    const seen = new Set<string>();
    const pendingAsEntities: Entity[] = [];
    for (const pending of pendingCreates) {
        if (seen.has(pending.id)) continue;
        seen.add(pending.id);
        pendingAsEntities.push({
            id: pending.id,
            name: pending.name,
            slug: pending.slug,
            type_id: pending.type_id,
            status: pending.status,
            geometry_count: 0,
            created_at: undefined,
            updated_at: undefined,
        });
    }

    const nextPersisted = persistedEntities.filter((entity) => !seen.has(entity.id));
    return [...pendingAsEntities, ...nextPersisted];
}

function mergeEntitySearchResults(
    remoteRows: Entity[],
    localRows: Entity[]
): Entity[] {
    const merged: Entity[] = [];
    const seen = new Set<string>();

    for (const row of localRows) {
        if (!row.id || seen.has(row.id)) continue;
        seen.add(row.id);
        merged.push(row);
    }

    for (const row of remoteRows) {
        if (!row.id || seen.has(row.id)) continue;
        seen.add(row.id);
        merged.push(row);
    }

    return merged;
}

function buildClientEntityId(): string {
    if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
        return crypto.randomUUID();
    }
    return `entity-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
}

function loadBackgroundLayerVisibilityFromStorage(): BackgroundLayerVisibility {
    if (typeof window === "undefined") {
        return { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY };
    }

    try {
        const raw = window.localStorage.getItem(BACKGROUND_LAYER_VISIBILITY_STORAGE_KEY);
        if (!raw) {
            return { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY };
        }

        const parsed = JSON.parse(raw) as unknown;
        const normalized = normalizeBackgroundLayerVisibility(parsed);
        return normalized || { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY };
    } catch (err) {
        console.warn("Load background layer visibility from storage failed", err);
        return { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY };
    }
}

function persistBackgroundLayerVisibility(visibility: BackgroundLayerVisibility) {
    if (typeof window === "undefined") return;
    try {
        window.localStorage.setItem(
            BACKGROUND_LAYER_VISIBILITY_STORAGE_KEY,
            JSON.stringify(visibility)
        );
    } catch (err) {
        console.warn("Persist background layer visibility failed", err);
    }
}

function normalizeBackgroundLayerVisibility(raw: unknown): BackgroundLayerVisibility | null {
    if (!raw || typeof raw !== "object") return null;

    const source = raw as Record<string, unknown>;
    const next: BackgroundLayerVisibility = {
        ...DEFAULT_BACKGROUND_LAYER_VISIBILITY,
    };

    for (const layer of BACKGROUND_LAYER_OPTIONS) {
        const value = source[layer.id];
        if (typeof value === "boolean") {
            next[layer.id] = value;
        }
    }

    return next;
}
