import { onWindowResize } from "@web3r/flowerkit/evt";
import type { ReactElement, FC } from "react";
import {
  useRef,
  lazy,
  Suspense,
  useImperativeHandle,
  useState,
  useCallback,
  useMemo,
  useEffect,
} from "react";
import type {
  IMapState, IMapOptions, Clusterer, IMapBoundsOptions, IEvent, Placemark,
} from "yandex-maps";
import { mapDefaults } from "./config";
import type {
  IMap, IMapRef,
  TAPI,
  TCluster,
  TLangs, TMapContext,
  TMapInstance,
  TPoint,
} from "./config/types";
import { MapContext } from "./context";
import {
  getUpdateBounds, getDefaultState, setComputedWidth, isClustersUpdated,
} from "./lib";
import { getInitialClusters, getNormalizedClusters } from "./lib/clusterer";
import type { IComponentBaseProps } from "../../types/components";
import { useCN } from "../../utils/react/useCN";
import { useIO } from "../../utils/react/useIO";
import { useLocalisationLanguages } from "../../utils/react/useLocalisationLanguages";
import { useSSGRender } from "../../utils/react/useSSGRender";
import "./style.pcss";

const YandexMap = lazy(() => import(/* webpackChunkName: "yaMap" */"./ui/Map"));

type TNumberArrayLike = number[] | number[][] | null | undefined;

const getFlatNumberArray = (value: TNumberArrayLike): number[] | null => {
  if (Array.isArray(value) && value.every((item) => typeof item === "number" && Number.isFinite(item))) {
    return value as number[];
  }
  return null;
};

const isEqualNumberArray = (first: TNumberArrayLike, second: TNumberArrayLike): boolean => {
  const normalizedFirst = getFlatNumberArray(first);
  const normalizedSecond = getFlatNumberArray(second);
  if (!normalizedFirst || !normalizedSecond || normalizedFirst.length !== normalizedSecond.length) {
    return false;
  }
  return normalizedFirst.every((value, index) => value === normalizedSecond[index]);
};

/**
 * Компонент карты. Служит оберткой над Yandex.Maps.

 * @returns {ReactElement}
 */
const Map: FC<IMap & IComponentBaseProps> = (props): ReactElement => {

  const {
    baseClass = mapDefaults.baseClass,
    extraClasses = mapDefaults.extraClasses,
    extraAttrs = mapDefaults.extraAttrs,
    children = mapDefaults.children,
    onAction = mapDefaults.onAction,
    id = mapDefaults.id,
    isAsync = mapDefaults.isAsync,
    points = mapDefaults.points,
    clusters = mapDefaults.clusters,
    modules = mapDefaults.modules,
    center = mapDefaults.center,
    zoom = mapDefaults.zoom,
    placeholderSlot = mapDefaults.placeholderSlot,
    onError = mapDefaults.onError,
    onReady = mapDefaults.onReady,
    options = mapDefaults.options,
    margin = mapDefaults.margin,
    isDisabled = mapDefaults.isDisabled,
    api = mapDefaults.api,
    panelSlot = mapDefaults.panelSlot,
    maxActivePoints = mapDefaults.maxActivePoints,
    onMount = mapDefaults.onMount,
    onUnmount = mapDefaults.onUnmount,
    pointOptions = mapDefaults.pointOptions,
    clusterOptions = mapDefaults.clusterOptions,
    isAllPointsVisible = mapDefaults.isAllPointsVisible,
    ref,
  } = props || {};

  // User clusters
  const userClusters = useMemo(() => {
    return getInitialClusters(clusters, points, clusterOptions, pointOptions);
  }, [ clusters, points, clusterOptions, pointOptions ]);

  // Classnames
  const { getCN } = useCN(baseClass);

  // Container ref
  const containerRef = useRef<HTMLDivElement | null>(null);

  // Instance ref
  const instanceRef = useRef<TMapInstance>(null);

  // Current balloon ref
  const balloonRef = useRef<Placemark["balloon"]>(null);

  // Callback refs to avoid re-creating handlers
  const onReadyRef = useRef(onReady);
  const onErrorRef = useRef(onError);

  // Update refs when props change
  useEffect(() => {
    onReadyRef.current = onReady;
    onErrorRef.current = onError;
  }, [ onReady, onError ]);

  // IO
  const { observer } = useIO({
    callback: (entry: IntersectionObserverEntry[]) => {
      if (entry) {
        entry.forEach(({ isIntersecting }) => {
          if (isIntersecting) {
            setIsVisible(true);
            setIsLoading(true);
          }
        });
      }
    },
  });

  // Panel state
  const [ panelData, setPanelData ] = useState<TPoint | null>(null);

  // Visibility state
  const [ isVisible, setIsVisible ] = useState<boolean>(!isAsync);

  // Disabled state
  const [ isMapDisabled, setIsMapDisabled ] = useState<boolean>(!!isDisabled);

  // Loading state
  const [ isLoading, setIsLoading ] = useState<boolean>(false);

  // Updating state
  const [ isUpdating, setIsUpdating ] = useState<boolean>(false);

  // Error state
  const [ isError, setIsError ] = useState<boolean>(false);

  // Current lang
  const { lang } = useLocalisationLanguages({ isNormalize: true });

  // Current query to API
  const currentQuery = useMemo(() => {
    return {
      apikey: api?.key ?? "",
      lang: lang as TLangs,
    };
  }, [ api, lang ]);

  // Current map inner state
  const [ currentState, setCurrentState ] = useState<IMapState>(() => getDefaultState({ zoom, center, margin }));
  const prevStateRef = useRef<IMapState>(currentState);

  // Current map inner options
  const currentOptions = useMemo<IMapOptions>(() => {
    return {
      ...mapDefaults.options,
      ...options,
    };
  }, [ options ]);

  // Current map clusters
  const [ currentClusters, setCurrentClusters ] = useState<TCluster[]>(userClusters);

  // Refs on prev clusters state
  const prevClusters = useRef<TCluster[]>(currentClusters);
  const prevActivePoints = useRef<string[]>(currentClusters.flatMap((c) => c.points.filter((p) => p.state?.isActive)).map((p) => p.id));

  // Log when clusters change and close previous balloon
  useEffect(() => {
    const activePoints = currentClusters.flatMap((c) => c.points.filter((p) => p.state?.isActive)).map((p) => p.id);

    // Если активная метка изменилась, закроем старый балун асинхронно
    const prevActivePointIds = prevActivePoints.current;
    if (JSON.stringify(activePoints) !== JSON.stringify(prevActivePointIds) && balloonRef.current) {
      (async () => {
        try {
          if (balloonRef.current?.isOpen?.()) {
            await balloonRef.current.close();
          }
          // Очищаем ссылку после закрытия
          balloonRef.current = null;
        } catch (err) {
          console.warn("[Map] Failed to close balloon:", err);
          balloonRef.current = null;
        }
      })();
    }
    prevActivePoints.current = activePoints;
  }, [ currentClusters ]);

  // State utils
  const stateActions = useMemo(() => ({
    /**
     * Управляет активностью боковой панели.
     * Используйте `null` для закрытия панели.
     */
    setPanelData,
    /**
     * Обновляет массив кластеров.
     * Для коррекции границ карты требуется обновить критические параметры: уникальные id либо количество точек.
     */
    setClusters: (nextClusters: TCluster[] | ((prevClusters: TCluster[]) => TCluster[]), isShouldNormalize: boolean = true) => {
      setCurrentClusters((prevClusters) => {
        const computed = typeof nextClusters === "function" ? nextClusters(prevClusters) : nextClusters;
        return isShouldNormalize ? getNormalizedClusters(computed, clusterOptions, pointOptions) : computed;
      });
    },
    /**
     * Обновляет флаг активности всей карты
     */
    setIsDisabled: setIsMapDisabled,
    /**
     * Обновляет состояние указанной точки.
     */
    setPointState: (idPoint: TPoint["id"], state: Partial<TPoint["state"]>) => {
      if (typeof idPoint === "string" && idPoint.length) {
        setCurrentClusters((prevClustersState) => {
          let isFound = false;
          const nextClustersState = prevClustersState.map((cluster) => {
            const index = cluster.points.findIndex(({ id }) => id === idPoint);
            if (index === -1) {
              return cluster;
            }
            isFound = true;
            return {
              ...cluster,
              points: cluster.points.map((point, pointIndex) => {
                if (pointIndex !== index) {
                  return point;
                }
                return {
                  ...point,
                  state: {
                    ...point.state,
                    ...(state || {}),
                  },
                };
              }),
            };
          });
          if (!isFound) {
            console.error(`[Map] Can't find point with id "${idPoint}"`);
          }
          return nextClustersState;
        });
      } else {
        console.error("[Map] Argument must be non-empty string");
      }
    },
  }), [ clusterOptions, pointOptions ]);

  // Update handle
  const handleUpdate = useCallback(() => {
    const mapInstance = instanceRef.current;
    if (!mapInstance) {
      return;
    }
    const center = mapInstance.getCenter({
      useMapMargin: true,
    });
    const zoom = mapInstance.getZoom();
    const margin = mapInstance.margin.getMargin();
    setCurrentState((prevState) => {
      const nextCenter = getFlatNumberArray(center) ?? prevState.center;
      const nextMargin = getFlatNumberArray(margin) ?? prevState.margin;
      const isEqual = zoom === prevState.zoom
        && isEqualNumberArray(nextCenter, prevState.center)
        && isEqualNumberArray(nextMargin, prevState.margin);
      if (isEqual) {
        return prevState;
      }
      const newState = {
        ...prevState,
        center: nextCenter,
        zoom,
        margin: nextMargin,
      };
      console.debug(`[Map] Updating state (zoom from "${prevState.zoom}" to "${newState.zoom}", center from "${prevState.center}" to "${newState.center}", margin from "${prevState.margin}" to "${newState.margin}")`);
      prevStateRef.current = prevState;
      return newState;
    });
  }, []);

  // Error handle
  const handleError = useCallback((error: Error | string) => {
    setIsLoading(false);
    setIsError(true);
    setIsUpdating(false);
    console.error("[Map] Something is wrong", error);
    if (typeof onErrorRef.current === "function") {
      onErrorRef.current({
        instance: instanceRef.current,
        containerRef,
        error,
      });
    }
  }, []);

  // Ready handle
  const handleReady = useCallback(async (instance: TAPI, clusters: Clusterer[]) => {
    setIsLoading(false);
    setIsError(false);
    setIsUpdating(true);
    const MapInstance = instanceRef.current;
    if (MapInstance) {
      MapInstance.options.set("customProps", { isAllPointsVisible });
      await getUpdateBounds(MapInstance)
        .catch((err: Error | string) => {
          console.warn(`[Maps] ${(err instanceof Error ? err.message : err.toString())}`);
        })
        .finally(() => {
          setIsUpdating(false);
          handleUpdate();
          if (typeof onReadyRef.current === "function") {
            onReadyRef.current({
              clusters,
              instance: MapInstance,
              containerRef,
              ymaps: instance,
            });
          }
        });
    } else {
      handleError(new Error("Can't find map instance"));
    }
  }, [ handleError, handleUpdate, isAllPointsVisible ]);

  // Change ref
  useImperativeHandle(ref, (): IMapRef => {
    return {
      el: containerRef.current,
      ref: instanceRef,
      utils: {
        /**
         * Обновляет карту для видимости всех точек
         * @param options{IMapBoundsOptions}
         * @returns {Promise<TMapInstance>}
         */
        getUpdateBounds: (options: IMapBoundsOptions): Promise<TMapInstance> => getUpdateBounds(instanceRef.current, options).then(() => {
          handleUpdate();
          return instanceRef.current;
        }),
      },
      getData: () => ({
        clusters: currentClusters,
        prevClusters: prevClusters.current,
      }),
      stateActions,
    };
  }, [ currentClusters, handleUpdate, stateActions ]);

  // Set IO
  useEffect(() => {
    if (observer && containerRef.current && isAsync) {
      observer.observe(containerRef.current);
      return () => observer.disconnect();
    }
  }, [ isAsync, observer ]);

  useEffect(() => {
    if (observer && isLoading && isAsync) {
      observer.disconnect();
    }
  }, [ isAsync, isLoading, observer ]);

  useEffect(() => {

    setComputedWidth(containerRef.current);

    const { removeListener } = onWindowResize(async () => {
      const mapInstance = instanceRef.current;
      if (!mapInstance) {
        return;
      }
      setIsUpdating(true);
      setComputedWidth(containerRef.current);
      try {
        await getUpdateBounds(mapInstance);
        handleUpdate();
      } catch (err) {
        console.error(err);
      } finally {
        setIsUpdating(false);
      }
    });

    if (typeof onMount === "function") {
      onMount();
    }

    return () => {
      removeListener();

      if (typeof onUnmount === "function") {
        onUnmount();
      }
    };
  }, [ handleUpdate, onMount, onUnmount ]);

  // update clusters & points from props
  useEffect(() => {
    if (userClusters !== prevClusters.current) {
      setIsUpdating(true);
      setCurrentClusters((prev) => {
        console.debug("[Map] Update clusters from:", prev, "to:", userClusters);
        return userClusters;
      });
    }
  }, [ userClusters ]);

  // set bounds after clusters update from state
  useEffect(() => {
    const mapInstance = instanceRef.current;
    const isChanged = isClustersUpdated(currentClusters, prevClusters.current);

    if (!isChanged || !mapInstance) {
      return;
    }

    const Objects = mapInstance.geoObjects;
    let isDisposed = false;
    let onObjectAdds: ((event: object | IEvent<unknown, unknown>) => void) | null = null;

    const onFinally = () => {
      if (isDisposed) {
        return;
      }
      prevClusters.current = currentClusters;
      setIsUpdating(false);
    };

    const updateBounds = async () => {
      try {
        await getUpdateBounds(mapInstance);
        handleUpdate();
      } catch (err) {
        console.error(err);
      } finally {
        onFinally();
      }
    };

    setIsUpdating(true);

    if (currentClusters.some((cluster) => cluster.points.length)) {
      if (Objects.getLength() === currentClusters.length) {
        void updateBounds();
      } else {
        onObjectAdds = async (_event: object | IEvent<unknown, unknown>) => {
          if (Objects.getLength() === currentClusters.length) {
            if (onObjectAdds) {
              Objects.events.remove("add", onObjectAdds);
            }
            await updateBounds();
          }
        };
        Objects.events.add("add", onObjectAdds);
      }
    } else {
      onFinally();
    }

    return () => {
      isDisposed = true;
      if (onObjectAdds) {
        Objects.events.remove("add", onObjectAdds);
        onObjectAdds = null;
      }
    };
  }, [ currentClusters, handleUpdate ]);

  // update state
  useEffect(() => {
    const newState = getDefaultState({ center, zoom, margin });
    const isChanged = prevStateRef.current.zoom !== newState.zoom
      || !isEqualNumberArray(prevStateRef.current.center, newState.center)
      || !isEqualNumberArray(prevStateRef.current.margin, newState.margin);
    if (isChanged) {
      setCurrentState((prevState) => {
        return {
          ...prevState,
          ...newState,
        };
      });
      prevStateRef.current = newState;
    }
  }, [ center, zoom, margin ]);

  const className = getCN("", {
    isLoading,
    isUpdating,
    isError,
    isDisabled: isMapDisabled,
    isVisible,
    isPanel: !!panelData,
    ...extraClasses,
  });

  const ctx = useMemo<TMapContext>(() => ({
    api, getCN, points, instanceRef, containerRef, balloonRef, center, zoom, modules, clusters, options, onAction, stateActions, panelSlot, panelData, maxActivePoints, currentClusters,
  }), [
    api,
    balloonRef,
    center,
    clusters,
    containerRef,
    currentClusters,
    getCN,
    instanceRef,
    maxActivePoints,
    modules,
    onAction,
    options,
    panelData,
    panelSlot,
    points,
    stateActions,
    zoom,
  ]);

  const SSGComponent = useSSGRender("data-js-yandex-map", props);

  if (SSGComponent) {
    return SSGComponent;
  }

  return (
    <MapContext value={ctx}>
      <div id={id} ref={containerRef} className={className} {...extraAttrs}>
        <div className={getCN("inner")}>
          {isVisible && (
            <Suspense fallback={placeholderSlot}>
              <YandexMap clusters={currentClusters} state={currentState} query={currentQuery} options={currentOptions} onReady={handleReady} onError={handleError}>
                {children}
              </YandexMap>
            </Suspense>
          )}
        </div>
      </div>
    </MapContext>
  );

};

export type { IMap, IMapRef };

export {
  Map,
};
