import { useNotification } from "@refinedev/core";
import type { Map as OlMap } from "ol";
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
import type { FlatStyleLike } from "ol/style/flat";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { config } from "@/config/config";
import { isLpsFlowProperty, toM3h } from "@utils/units";

import { LayerStyleController } from "../layerStyleController";
import { useData, useMap } from "../MapComponent";
import {
  createDefaultLayerStyleState,
  createDefaultLayerStyleStates,
  createEmptyStyleConfig,
} from "./styleEditorPresets";
import {
  buildContourDefinitions,
  buildDynamicStyleTemplate,
  buildStyleVariables,
  getDefaultCustomBreaks,
  getDefaultCustomColors,
  hydrateStoredLayerStyleStates,
  normalizeCustomBreaks,
  requiresStyleApply,
  resolveDimensions,
  resolveLayerStyle,
  resolveStyleColors,
  selectStoredLayerStyles,
  validateStyleConfig,
} from "./styleEditorUtils";
import type {
  AvailableProperty,
  ClassificationMethod,
  ColorType,
  DefaultLayerStyleId,
  LayerStyleState,
  ResolvedLayerStyle,
  StyleConfig,
  StyleEditorStateProps,
} from "./styleEditorTypes";
import type { LegendStyleConfig } from "./StyleLegend";

const UNIT_HEADLOSS_RANGE: [number, number] = [0, 5];
const STYLE_STORAGE_VERSION = 2;

const cloneStyleConfig = (styleConfig: StyleConfig): StyleConfig => ({
  ...styleConfig,
  customBreaks: [...(styleConfig.customBreaks || [])],
  customColors: [...(styleConfig.customColors || [])],
});

const configsEqual = (left?: StyleConfig, right?: StyleConfig) =>
  Boolean(left && right && JSON.stringify(left) === JSON.stringify(right));

const hasSameTemplate = (left: StyleConfig, right: StyleConfig) =>
  left.property === right.property && left.segments === right.segments;

const normalizeComputedStyleValue = (property: string, value: unknown) => {
  const numericValue = Number(value);
  if (!Number.isFinite(numericValue)) return Number.NaN;
  const displayValue = isLpsFlowProperty(property)
    ? toM3h(numericValue, "lps")
    : numericValue;
  return property === "flow" ? Math.abs(displayValue) : displayValue;
};

const isDefaultLayerId = (value: unknown): value is DefaultLayerStyleId =>
  value === "junctions" || value === "pipes";

const createLegendConfig = ({
  layerId,
  layerName,
  property,
  layerType,
  resolved,
}: {
  layerId: DefaultLayerStyleId;
  layerName: string;
  property: string;
  layerType: string;
  resolved: ResolvedLayerStyle;
}): LegendStyleConfig => ({
  layerId,
  layerName,
  property,
  colors: resolved.isConstant ? resolved.colors.slice(0, 1) : resolved.colors,
  type: layerType,
  dimensions: resolved.isConstant
    ? resolved.dimensions.slice(0, 1)
    : resolved.dimensions,
  breaks: resolved.boundaries,
  labels: resolved.labels,
});

const resolveStyleFromLegend = (
  styleConfig: StyleConfig,
  legendConfig: LegendStyleConfig,
): ResolvedLayerStyle | null => {
  if (legendConfig.breaks.length !== styleConfig.segments + 1) return null;
  return {
    boundaries: legendConfig.breaks,
    colors: resolveStyleColors(styleConfig),
    dimensions: resolveDimensions({ layerType: legendConfig.type, styleConfig }),
    labels: legendConfig.labels || [],
    isConstant: legendConfig.breaks.every(
      (value) => value === legendConfig.breaks[0],
    ),
  };
};

const createRenderInputs = (
  styleConfig: StyleConfig,
  resolved: ResolvedLayerStyle,
) => {
  const variables = buildStyleVariables(styleConfig, resolved);
  return {
    variables,
    signature: JSON.stringify({ styleConfig, variables }),
  };
};

export const useStyleEditor = ({
  layerStyleStates,
  setLayerStyleStates,
  workspace,
}: StyleEditorStateProps) => {
  const map = useMap();
  const data = useData();
  const { open } = useNotification();
  const activeMaps = useMemo<OlMap[]>(
    () => (data?.maps?.length ? data.maps : map ? [map] : []),
    [data, map],
  );
  const compareMap = data?.compareMap;
  const currentJunctionCalData = data?.currentJunctionCalData;
  const currentPipeCalData = data?.currentPipeCalData;
  const compareJunctionCalData = data?.compareJunctionCalData;
  const comparePipeCalData = data?.comparePipeCalData;
  const elevationRange = data?.elevationRange;
  const diameterRange = data?.diameterRange;
  const forceStyleAutoApplyVersion = data?.forceStyleAutoApplyVersion ?? 0;
  const setJunctionText = data?.setJunctionText;
  const setPipeText = data?.setPipeText;
  const setShowJunctionTextLayer = data?.setShowJunctionTextLayer;
  const setShowPipeTextLayer = data?.setShowPipeTextLayer;
  const setShowJunctionId = data?.setShowJunctionId;
  const setShowPipeId = data?.setShowPipeId;
  const setContourLayerAvailable = data?.setContourLayerAvailable;
  const setContours = data?.setContours;
  const setWaterflowLayerAvailable = data?.setWaterflowLayerAvailable;
  const setShowWaterflowLayer = data?.setShowWaterflowLayer;

  const renderLayers = useMemo(
    () =>
      (map?.getAllLayers() || []).filter(
        (layer): layer is WebGLVectorTileLayer =>
          layer instanceof WebGLVectorTileLayer &&
          (layer.get("value") === "junctions" || layer.get("value") === "pipes"),
      ),
    [map],
  );
  const [selectedRenderLayerState, setSelectedRenderLayer] =
    useState<WebGLVectorTileLayer>();
  const selectedRenderLayer =
    renderLayers.find(
      (layer) => layer.get("value") === selectedRenderLayerState?.get("value"),
    ) || renderLayers.find((layer) => layer.get("value") === "junctions") || renderLayers[0];
  const [styleConfig, setStyleConfig] = useState(() =>
    createDefaultLayerStyleState("junctions").styleConfig,
  );
  const [isApplying, setIsApplying] = useState(false);
  const [persistenceReady, setPersistenceReady] = useState(false);
  const latestLayerStyleStatesRef = useRef(layerStyleStates);
  const controllersRef = useRef(new Map<string, LayerStyleController>());
  const renderInputsRef = useRef(
    new Map<
      string,
      { records: readonly any[]; signature: string }
    >(),
  );
  const draftsRef = useRef(new Map<string, StyleConfig>());
  const renderRevisionRef = useRef(new Map<DefaultLayerStyleId, number>());
  const lastForceStyleAutoApplyVersionRef = useRef(0);

  useEffect(() => {
    latestLayerStyleStatesRef.current = layerStyleStates;
  }, [layerStyleStates]);

  const getMapKey = useCallback((targetMap: OlMap, layerId: string) => {
    const mapUid = (targetMap as unknown as { ol_uid?: string }).ol_uid || "map";
    return `${mapUid}:${layerId}`;
  }, []);

  const getRenderLayersById = useCallback(
    (layerId: string) =>
      activeMaps.flatMap((targetMap) =>
        targetMap
          .getAllLayers()
          .filter(
            (layer): layer is WebGLVectorTileLayer =>
              layer instanceof WebGLVectorTileLayer && layer.get("value") === layerId,
          ),
      ),
    [activeMaps],
  );

  const getLayerForMap = useCallback((targetMap: OlMap, layerId: string) =>
    targetMap
      .getAllLayers()
      .find(
        (layer): layer is WebGLVectorTileLayer =>
          layer instanceof WebGLVectorTileLayer && layer.get("value") === layerId,
      ), []);

  const getController = useCallback(
    (targetMap: OlMap, layerId: string) => {
      const key = getMapKey(targetMap, layerId);
      let controller = controllersRef.current.get(key);
      if (controller) return controller;
      const layer = getLayerForMap(targetMap, layerId);
      if (!layer) return null;
      controller = new LayerStyleController({
        layer,
        map: targetMap,
        defaultStyle: config.MAP_DEFAULT_STYLE as FlatStyleLike,
        stateNamespace: targetMap === compareMap ? "compare" : "primary",
      });
      controllersRef.current.set(key, controller);
      return controller;
    },
    [compareMap, getLayerForMap, getMapKey],
  );

  const getDataForMap = useCallback(
    (targetMap: OlMap, layerId: DefaultLayerStyleId) => {
      if (layerId === "junctions") {
        return targetMap === compareMap
          ? compareJunctionCalData || []
          : currentJunctionCalData || [];
      }
      return targetMap === compareMap
        ? comparePipeCalData || []
        : currentPipeCalData || [];
    },
    [
      compareJunctionCalData,
      compareMap,
      comparePipeCalData,
      currentJunctionCalData,
      currentPipeCalData,
    ],
  );

  const availableProperties = useMemo<AvailableProperty[]>(
    () => (selectedRenderLayer?.get("properties") || []) as AvailableProperty[],
    [selectedRenderLayer],
  );

  const upsertLayerStyleState = useCallback(
    (nextState: LayerStyleState) => {
      setLayerStyleStates((previous) => {
        const index = previous.findIndex((state) => state.layerId === nextState.layerId);
        if (index >= 0) {
          const current = previous[index];
          if (JSON.stringify(current) === JSON.stringify(nextState)) return previous;
        }
        const next =
          index < 0
            ? [...previous, nextState]
            : previous.map((state, stateIndex) =>
                stateIndex === index ? nextState : state,
              );
        latestLayerStyleStatesRef.current = next;
        return next;
      });
    },
    [setLayerStyleStates],
  );

  const getDefaultBreaks = useCallback(
    (segments: number, property: string, layer = selectedRenderLayer) =>
      getDefaultCustomBreaks({
        segments,
        property,
        layerId: layer?.get("value"),
        elevationRange,
        diameterRange,
        currentJunctionCalData,
        currentPipeCalData,
      }),
    [
      currentJunctionCalData,
      currentPipeCalData,
      diameterRange,
      elevationRange,
      selectedRenderLayer,
    ],
  );

  const getClassificationValues = useCallback(
    (layerId: DefaultLayerStyleId, property: string) => {
      if (layerId === "junctions" && property === "elevation" && elevationRange) {
        return [...elevationRange];
      }
      if (layerId === "pipes" && property === "diameter" && diameterRange) {
        return [...diameterRange];
      }
      if (layerId === "pipes" && property === "unit_headloss") {
        return [...UNIT_HEADLOSS_RANGE];
      }
      const records = layerId === "junctions" ? currentJunctionCalData : currentPipeCalData;
      return (records || [])
        .map((item: any) => normalizeComputedStyleValue(property, item.value))
        .filter(Number.isFinite);
    },
    [currentJunctionCalData, currentPipeCalData, diameterRange, elevationRange],
  );

  const syncAuxiliaryLayers = useCallback(
    (layerId: DefaultLayerStyleId, nextConfig: StyleConfig | null) => {
      const active = Boolean(nextConfig);
      if (layerId === "junctions") {
        setJunctionText?.(nextConfig?.property || "");
        setShowJunctionTextLayer?.(active && Boolean(nextConfig?.showLabels));
        setShowJunctionId?.(active && Boolean(nextConfig?.showId));
        setContourLayerAvailable?.(active && nextConfig?.property === "pressure");
        if (!active) setContours?.([]);
        return;
      }
      const isFlow = active && nextConfig?.property === "flow";
      setPipeText?.(nextConfig?.property || "");
      setShowPipeTextLayer?.(active && Boolean(nextConfig?.showLabels));
      setShowPipeId?.(active && Boolean(nextConfig?.showId));
      setWaterflowLayerAvailable?.(isFlow);
      if (!isFlow) setShowWaterflowLayer?.(false);
    },
    [
      setContourLayerAvailable,
      setContours,
      setJunctionText,
      setPipeText,
      setShowJunctionId,
      setShowJunctionTextLayer,
      setShowPipeId,
      setShowPipeTextLayer,
      setShowWaterflowLayer,
      setWaterflowLayerAvailable,
    ],
  );

  const syncContoursForStyle = useCallback(
    (
      layerId: DefaultLayerStyleId,
      nextConfig: StyleConfig,
      resolved: ResolvedLayerStyle,
    ) => {
      if (layerId !== "junctions") return;
      setContours?.(
        nextConfig.property === "pressure"
          ? buildContourDefinitions({
              styleConfig: nextConfig,
              breaks: resolved.boundaries,
              colors: resolved.colors,
            })
          : [],
      );
    },
    [setContours],
  );

  const renderAppliedStyle = useCallback(
    async (layerId: DefaultLayerStyleId, nextConfig: StyleConfig) => {
      const targetLayer = getRenderLayersById(layerId)[0];
      if (!targetLayer) return false;
      const values = getClassificationValues(layerId, nextConfig.property);
      const resolved = resolveLayerStyle({
        layerType: targetLayer.get("type") || (layerId === "junctions" ? "point" : "linestring"),
        styleConfig: nextConfig,
        values,
      });
      if (!resolved) return false;

      const revision = (renderRevisionRef.current.get(layerId) || 0) + 1;
      renderRevisionRef.current.set(layerId, revision);
      setIsApplying(true);
      const layerType = targetLayer.get("type") || (layerId === "junctions" ? "point" : "linestring");
      const template = buildDynamicStyleTemplate({
        layerType,
        property: nextConfig.property,
        classCount: nextConfig.segments,
      });
      const { variables, signature: renderSignature } = createRenderInputs(
        nextConfig,
        resolved,
      );
      const usesNativeProperty =
        nextConfig.property === "elevation" || nextConfig.property === "diameter";

      const commits = activeMaps.map(async (targetMap) => {
        const key = getMapKey(targetMap, layerId);
        const records = getDataForMap(targetMap, layerId);
        const previousInput = renderInputsRef.current.get(key);
        if (
          controllersRef.current.has(key) &&
          previousInput?.records === records &&
          previousInput.signature === renderSignature
        ) {
          return;
        }
        const controller = getController(targetMap, layerId);
        if (!controller) return;
        const options = {
          property: nextConfig.property,
          classCount: nextConfig.segments,
          template,
          variables,
        };
        if (usesNativeProperty) {
          controller.applyNative(options);
          renderInputsRef.current.set(key, { records, signature: renderSignature });
          return;
        }
        const stateById = new Map<string, number>();
        records.forEach((record: any) => {
          const id = record.ID ?? record.id;
          if (id === undefined || id === null) return;
          const value = normalizeComputedStyleValue(nextConfig.property, record.value);
          if (Number.isFinite(value)) stateById.set(String(id), value);
        });
        const committed = await controller.applyRuntime(options, stateById);
        if (committed !== false) {
          renderInputsRef.current.set(key, { records, signature: renderSignature });
        }
      });
      await Promise.all(commits);
      if (revision !== renderRevisionRef.current.get(layerId)) return false;

      const layerName = targetLayer.get("name") || (layerId === "junctions" ? "节点" : "管道");
      const properties = (targetLayer.get("properties") || []) as AvailableProperty[];
      const propertyLabel =
        properties.find((item) => item.value === nextConfig.property)?.name || nextConfig.property;
      const legendConfig = createLegendConfig({
        layerId,
        layerName,
        property: propertyLabel,
        layerType,
        resolved,
      });
      upsertLayerStyleState({
        layerId,
        layerName,
        styleConfig: cloneStyleConfig(nextConfig),
        legendConfig,
        isActive: true,
      });
      syncContoursForStyle(layerId, nextConfig, resolved);
      setIsApplying(false);
      return true;
    },
    [
      activeMaps,
      getClassificationValues,
      getController,
      getDataForMap,
      getMapKey,
      getRenderLayersById,
      syncContoursForStyle,
      upsertLayerStyleState,
    ],
  );

  const activateStyle = useCallback(
    async (
      layerId: DefaultLayerStyleId,
      nextConfig: StyleConfig,
      notify = true,
    ) => {
      const validation = validateStyleConfig(nextConfig);
      if (!validation.valid) {
        if (notify) open?.({ type: "error", message: validation.errors[0] });
        return false;
      }
      const targetLayer = getRenderLayersById(layerId)[0];
      if (!targetLayer) {
        if (notify) open?.({ type: "error", message: "未找到目标图层，无法应用样式。" });
        return false;
      }
      const previous = latestLayerStyleStatesRef.current.find((state) => state.layerId === layerId);
      const layerName = targetLayer.get("name") || (layerId === "junctions" ? "节点" : "管道");
      const layerType = targetLayer.get("type") || (layerId === "junctions" ? "point" : "linestring");
      const canUpdateVariablesOnly =
        previous?.isActive &&
        hasSameTemplate(previous.styleConfig, nextConfig) &&
        activeMaps.every((targetMap) => {
          const controller = controllersRef.current.get(getMapKey(targetMap, layerId));
          return controller?.hasTemplate();
        });

      if (canUpdateVariablesOnly) {
        const resolved = resolveLayerStyle({
          layerType,
          styleConfig: nextConfig,
          values: getClassificationValues(layerId, nextConfig.property),
        });
        if (resolved) {
          const { variables } = createRenderInputs(nextConfig, resolved);
          activeMaps.forEach((targetMap) =>
            controllersRef.current
              .get(getMapKey(targetMap, layerId))
              ?.updateVariables(variables),
          );
          const properties = (targetLayer.get("properties") || []) as AvailableProperty[];
          const propertyLabel =
            properties.find((item) => item.value === nextConfig.property)?.name ||
            nextConfig.property;
          upsertLayerStyleState({
            layerId,
            layerName,
            styleConfig: cloneStyleConfig(nextConfig),
            legendConfig: createLegendConfig({
              layerId,
              layerName,
              property: propertyLabel,
              layerType,
              resolved,
            }),
            isActive: true,
          });
          syncContoursForStyle(layerId, nextConfig, resolved);
          draftsRef.current.set(layerId, cloneStyleConfig(nextConfig));
          syncAuxiliaryLayers(layerId, nextConfig);
          if (notify) {
            open?.({
              type: "success",
              message: `${layerId === "junctions" ? "节点" : "管道"}图层样式已应用。`,
            });
          }
          return true;
        }
      }
      upsertLayerStyleState({
        layerId,
        layerName,
        styleConfig: cloneStyleConfig(nextConfig),
        legendConfig: previous?.legendConfig || {
          layerId,
          layerName,
          property: nextConfig.property,
          colors: [],
          type: layerType,
          dimensions: [],
          breaks: [],
        },
        isActive: true,
      });
      draftsRef.current.set(layerId, cloneStyleConfig(nextConfig));
      syncAuxiliaryLayers(layerId, nextConfig);
      const rendered = await renderAppliedStyle(layerId, nextConfig);
      if (notify) {
        open?.({
          type: rendered ? "success" : "progress",
          message: rendered
            ? `${layerId === "junctions" ? "节点" : "管道"}图层样式已应用。`
            : "样式已保存，将在数据就绪后渲染。",
        });
      }
      return true;
    },
    [
      activeMaps,
      getClassificationValues,
      getMapKey,
      getRenderLayersById,
      open,
      renderAppliedStyle,
      syncContoursForStyle,
      syncAuxiliaryLayers,
      upsertLayerStyleState,
    ],
  );

  const resetLayer = useCallback(
    (layerId: DefaultLayerStyleId, notify = true) => {
      activeMaps.forEach((targetMap) => {
        const key = getMapKey(targetMap, layerId);
        controllersRef.current.get(key)?.reset();
        controllersRef.current.delete(key);
        renderInputsRef.current.delete(key);
      });
      const defaults = createDefaultLayerStyleState(layerId);
      setLayerStyleStates((previous) => {
        const next = previous.map((state) => (state.layerId === layerId ? defaults : state));
        latestLayerStyleStatesRef.current = next;
        return next;
      });
      draftsRef.current.delete(layerId);
      syncAuxiliaryLayers(layerId, null);
      if (selectedRenderLayer?.get("value") === layerId) {
        setStyleConfig(cloneStyleConfig(defaults.styleConfig));
      }
      if (notify) {
        open?.({
          type: "success",
          message: `${layerId === "junctions" ? "节点" : "管道"}图层样式已重置。`,
        });
      }
    },
    [activeMaps, getMapKey, open, selectedRenderLayer, setLayerStyleStates, syncAuxiliaryLayers],
  );

  const normalizeExternalStyleConfig = useCallback(
    (layerId: DefaultLayerStyleId, overrides?: Partial<StyleConfig>) => {
      const applied = latestLayerStyleStatesRef.current.find((state) => state.layerId === layerId);
      const base = applied?.styleConfig || createDefaultLayerStyleState(layerId).styleConfig;
      const next: StyleConfig = {
        ...base,
        ...overrides,
        customBreaks: overrides?.customBreaks
          ? [...overrides.customBreaks]
          : [...(base.customBreaks || [])],
        customColors: overrides?.customColors
          ? [...overrides.customColors]
          : [...(base.customColors || [])],
      };
      const classCountChanged =
        overrides?.segments !== undefined && overrides.segments !== base.segments;
      if (
        next.colorType === "custom" &&
        overrides?.customColors === undefined &&
        (classCountChanged || base.colorType !== "custom")
      ) {
        next.customColors = getDefaultCustomColors(next.segments, next.customColors);
      }
      if (
        next.classificationMethod === "custom_breaks" &&
        overrides?.customBreaks === undefined &&
        (classCountChanged || base.classificationMethod !== "custom_breaks")
      ) {
        const fallback = getDefaultBreaks(
          next.segments,
          next.property,
          getRenderLayersById(layerId)[0],
        );
        next.customBreaks = normalizeCustomBreaks(fallback, next.segments);
      }
      return next;
    },
    [getDefaultBreaks, getRenderLayersById],
  );

  const applyExternalStyle = useCallback(
    (layerId: DefaultLayerStyleId, overrides?: Partial<StyleConfig>) => {
      const targetLayer = getRenderLayersById(layerId)[0];
      if (!targetLayer) {
        open?.({ type: "error", message: "未找到目标图层，无法应用样式。" });
        return;
      }
      const next = normalizeExternalStyleConfig(layerId, overrides);
      setSelectedRenderLayer(targetLayer);
      setStyleConfig(cloneStyleConfig(next));
      void activateStyle(layerId, next);
    },
    [activateStyle, getRenderLayersById, normalizeExternalStyleConfig, open],
  );

  const handleApply = useCallback(() => {
    const layerId = selectedRenderLayer?.get("value");
    if (!isDefaultLayerId(layerId)) return;
    void activateStyle(layerId, styleConfig);
  }, [activateStyle, selectedRenderLayer, styleConfig]);

  const handleReset = useCallback(() => {
    const layerId = selectedRenderLayer?.get("value");
    if (isDefaultLayerId(layerId)) resetLayer(layerId);
  }, [resetLayer, selectedRenderLayer]);

  const handleLayerChange = useCallback(
    (layerId: string) => {
      const currentLayerId = selectedRenderLayer?.get("value");
      if (isDefaultLayerId(currentLayerId)) {
        draftsRef.current.set(currentLayerId, cloneStyleConfig(styleConfig));
        const applied = latestLayerStyleStatesRef.current.find(
          (state) => state.layerId === currentLayerId && state.isActive,
        );
        if (applied) {
          const resolved = resolveStyleFromLegend(
            applied.styleConfig,
            applied.legendConfig,
          );
          if (resolved) {
            const { variables } = createRenderInputs(applied.styleConfig, resolved);
            activeMaps.forEach((targetMap) =>
              getController(targetMap, currentLayerId)?.updateVariables(variables),
            );
          }
        }
      }
      const nextLayer = renderLayers.find((layer) => layer.get("value") === layerId);
      setSelectedRenderLayer(nextLayer);
      if (!nextLayer) return;
      const draft = draftsRef.current.get(layerId);
      const applied = latestLayerStyleStatesRef.current.find((state) => state.layerId === layerId);
      const fallback = isDefaultLayerId(layerId)
        ? createDefaultLayerStyleState(layerId).styleConfig
        : createEmptyStyleConfig();
      setStyleConfig(cloneStyleConfig(draft || applied?.styleConfig || fallback));
    },
    [activeMaps, getController, renderLayers, selectedRenderLayer, styleConfig],
  );

  const handlePropertyChange = useCallback(
    (property: string) => {
      setStyleConfig((previous) => ({
        ...previous,
        property,
        customBreaks:
          previous.classificationMethod === "custom_breaks"
            ? getDefaultBreaks(previous.segments, property)
            : previous.customBreaks,
      }));
    },
    [getDefaultBreaks],
  );

  const handleClassificationMethodChange = useCallback(
    (classificationMethod: ClassificationMethod) => {
      setStyleConfig((previous) => ({
        ...previous,
        classificationMethod,
        customBreaks:
          classificationMethod === "custom_breaks"
            ? getDefaultBreaks(previous.segments, previous.property)
            : previous.customBreaks,
      }));
    },
    [getDefaultBreaks],
  );

  const handleSegmentsChange = useCallback(
    (segments: number) => {
      setStyleConfig((previous) => ({
        ...previous,
        segments,
        customBreaks:
          previous.classificationMethod === "custom_breaks"
            ? getDefaultBreaks(segments, previous.property)
            : previous.customBreaks,
        customColors: getDefaultCustomColors(segments, previous.customColors),
      }));
    },
    [getDefaultBreaks],
  );

  const handleCustomBreakChange = useCallback((index: number, value: string) => {
    setStyleConfig((previous) => {
      const boundaries = [...(previous.customBreaks || [])];
      while (boundaries.length < previous.segments + 1) boundaries.push(0);
      boundaries[index] = value.trim() === "" ? Number.NaN : Number(value);
      return { ...previous, customBreaks: boundaries };
    });
  }, []);

  const handleColorTypeChange = useCallback((colorType: ColorType) => {
    setStyleConfig((previous) => ({
      ...previous,
      colorType,
      customColors:
        colorType === "custom"
          ? getDefaultCustomColors(previous.segments, previous.customColors)
          : previous.customColors,
    }));
  }, []);

  const selectedLayerId = selectedRenderLayer?.get("value");
  const appliedStyleConfig = isDefaultLayerId(selectedLayerId)
    ? layerStyleStates.find((state) => state.layerId === selectedLayerId && state.isActive)
        ?.styleConfig
    : undefined;
  const validation = useMemo(() => validateStyleConfig(styleConfig), [styleConfig]);
  const isDirty = requiresStyleApply(appliedStyleConfig, styleConfig);

  useEffect(() => {
    if (!appliedStyleConfig || !isDefaultLayerId(selectedLayerId)) return;
    if (requiresStyleApply(appliedStyleConfig, styleConfig)) return;
    if (!validation.valid) return;
    const state = layerStyleStates.find((item) => item.layerId === selectedLayerId);
    if (!state || state.legendConfig.breaks.length !== styleConfig.segments + 1) return;
    if (configsEqual(styleConfig, appliedStyleConfig)) return;
    const resolved = resolveStyleFromLegend(styleConfig, state.legendConfig);
    if (!resolved) return;
    const { variables, signature: renderSignature } = createRenderInputs(
      styleConfig,
      resolved,
    );
    activeMaps.forEach((targetMap) => {
      const key = getMapKey(targetMap, selectedLayerId);
      getController(targetMap, selectedLayerId)?.updateVariables(variables);
      const previousInput = renderInputsRef.current.get(key);
      if (previousInput) {
        renderInputsRef.current.set(key, {
          ...previousInput,
          signature: renderSignature,
        });
      }
    });
    const nextStyleConfig = cloneStyleConfig(styleConfig);
    draftsRef.current.set(selectedLayerId, nextStyleConfig);
    upsertLayerStyleState({
      ...state,
      styleConfig: nextStyleConfig,
      legendConfig: {
        ...state.legendConfig,
        colors: resolved.isConstant ? resolved.colors.slice(0, 1) : resolved.colors,
        dimensions: resolved.isConstant
          ? resolved.dimensions.slice(0, 1)
          : resolved.dimensions,
      },
    });
    syncAuxiliaryLayers(selectedLayerId, nextStyleConfig);
    syncContoursForStyle(selectedLayerId, styleConfig, resolved);
  }, [
    activeMaps,
    appliedStyleConfig,
    getController,
    getMapKey,
    layerStyleStates,
    selectedLayerId,
    styleConfig,
    syncAuxiliaryLayers,
    syncContoursForStyle,
    upsertLayerStyleState,
    validation.valid,
  ]);

  useEffect(() => {
    if (!persistenceReady) return;
    latestLayerStyleStatesRef.current
      .filter((state) => state.isActive && isDefaultLayerId(state.layerId))
      .forEach((state) => void renderAppliedStyle(state.layerId as DefaultLayerStyleId, state.styleConfig));
  }, [
    activeMaps,
    compareJunctionCalData,
    comparePipeCalData,
    currentJunctionCalData,
    currentPipeCalData,
    diameterRange,
    elevationRange,
    persistenceReady,
    renderAppliedStyle,
  ]);

  useEffect(() => {
    const activeMapKeys = new Set(
      activeMaps.map((targetMap) => getMapKey(targetMap, "").replace(/:$/, "")),
    );
    controllersRef.current.forEach((controller, key) => {
      const mapKey = key.slice(0, key.lastIndexOf(":"));
      if (activeMapKeys.has(mapKey)) return;
      controller.dispose();
      controllersRef.current.delete(key);
      renderInputsRef.current.delete(key);
    });
  }, [activeMaps, getMapKey]);

  useEffect(() => {
    if (
      forceStyleAutoApplyVersion <= 0 ||
      forceStyleAutoApplyVersion === lastForceStyleAutoApplyVersionRef.current
    ) return;
    lastForceStyleAutoApplyVersionRef.current = forceStyleAutoApplyVersion;
    (["junctions", "pipes"] as DefaultLayerStyleId[]).forEach((layerId) => {
      const defaults = createDefaultLayerStyleState(layerId).styleConfig;
      void activateStyle(layerId, defaults, false);
      if (selectedRenderLayer?.get("value") === layerId) {
        setStyleConfig(cloneStyleConfig(defaults));
      }
    });
  }, [activateStyle, forceStyleAutoApplyVersion, selectedRenderLayer]);

  const storageKey = `${workspace}:map-layer-style:v${STYLE_STORAGE_VERSION}`;
  useEffect(() => {
    // Restoring an external browser preference intentionally initializes React state.
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setPersistenceReady(false);
    let restored = createDefaultLayerStyleStates();
    try {
      const raw = window.localStorage.getItem(storageKey);
      if (raw) {
        restored = hydrateStoredLayerStyleStates(
          JSON.parse(raw),
          STYLE_STORAGE_VERSION,
        );
      }
    } catch (error) {
      console.warn("Restore layer styles failed", error);
    }
    latestLayerStyleStatesRef.current = restored;
    setLayerStyleStates(restored);
    const restoredSelection = restored.find((state) => state.layerId === "junctions");
    if (restoredSelection) setStyleConfig(cloneStyleConfig(restoredSelection.styleConfig));
    syncAuxiliaryLayers("junctions", null);
    syncAuxiliaryLayers("pipes", null);
    setPersistenceReady(true);
  }, [setLayerStyleStates, storageKey, syncAuxiliaryLayers]);

  useEffect(() => {
    if (!persistenceReady) return;
    const layers = selectStoredLayerStyles(layerStyleStates);
    try {
      window.localStorage.setItem(
        storageKey,
        JSON.stringify({ version: STYLE_STORAGE_VERSION, layers }),
      );
    } catch (error) {
      console.warn("Save layer styles failed", error);
    }
  }, [layerStyleStates, persistenceReady, storageKey]);

  useEffect(
    () => () => {
      controllersRef.current.forEach((controller) => controller.dispose());
      controllersRef.current.clear();
      renderInputsRef.current.clear();
    },
    [],
  );

  return {
    isReady: Boolean(data),
    renderLayers,
    selectedRenderLayer,
    styleConfig,
    setStyleConfig,
    availableProperties,
    validationErrors: validation.errors,
    isDirty,
    isApplying,
    handleLayerChange,
    handlePropertyChange,
    handleClassificationMethodChange,
    handleSegmentsChange,
    handleCustomBreakChange,
    handleColorTypeChange,
    handleApply,
    handleReset,
    applyExternalStyle,
    resetExternalStyle: resetLayer,
  };
};
