/**
 * useNativeMapLayers — Mercator/Globe 공통 MapLibre 네이티브 레이어 관리 hook
 *
 * 반복되는 보일러플레이트를 자동화합니다:
 * - projectionBusy / isStyleLoaded 가드
 * - GeoJSON source 생성/업데이트
 * - Layer 생성 (ensureLayer)
 * - Visibility 토글
 * - Globe 레이어 순서 관리 (reorderGlobeFeatureLayers)
 * - kickRepaint
 * - Unmount 시 cleanupLayers
 *
 * 호버 하이라이트, 마우스 이벤트 등 레이어별 커스텀 로직은
 * 별도 useEffect에서 처리합니다.
 */
import { useEffect, useRef, type MutableRefObject } from 'react';
import maplibregl, { type GeoJSONSourceSpecification, type LayerSpecification } from 'maplibre-gl';
import { ensureGeoJsonSource, ensureLayer, setLayerVisibility, cleanupLayers } from '../lib/layerHelpers';
import { kickRepaint, onMapStyleReady } from '../lib/mapCore';

/* ── Public types ──────────────────────────────────────────────────── */

export interface NativeSourceConfig {
  id: string;
  data: GeoJSON.GeoJSON | null;
  /** GeoJSON source 옵션 (tolerance, buffer 등) */
  options?: Partial<Omit<GeoJSONSourceSpecification, 'type' | 'data'>>;
}

export interface NativeLayerSpec {
  id: string;
  type: 'line' | 'fill' | 'circle' | 'symbol';
  sourceId: string;
  paint: Record<string, unknown>;
  layout?: Record<string, unknown>;
  filter?: unknown[];
  minzoom?: number;
  maxzoom?: number;
}

export interface NativeMapLayersConfig {
  /** GeoJSON 데이터 소스 (다중 지원) */
  sources: NativeSourceConfig[];
  /** 레이어 스펙 배열 (생성 순서대로) */
  layers: NativeLayerSpec[];
  /** 전체 레이어 on/off */
  visible: boolean;
  /**
   * 이 레이어들을 삽입할 기준 레이어 ID.
   * 배열이면 첫 번째로 존재하는 레이어를 사용합니다.
   */
  beforeLayer?: string | string[];
  /**
   * 레이어 (재)생성 후 호출되는 콜백.
   * 호버 하이라이트 재적용 등에 사용합니다.
   */
  onAfterSetup?: (map: maplibregl.Map) => void;
}

/* ── Hook ──────────────────────────────────────────────────────────── */

/**
 * @param mapRef - Map 인스턴스 ref
 * @param projectionBusyRef - 프로젝션 전환 중 가드 ref
 * @param reorderGlobeFeatureLayers - Globe 레이어 순서 재정렬 함수
 * @param config - 소스/레이어/visibility 설정
 * @param deps - 이 값이 변경되면 레이어를 다시 셋업합니다.
 *              (subcableGeo, overlays.subcables, projection, mapSyncEpoch 등)
 */
export function useNativeMapLayers(
  mapRef: MutableRefObject<maplibregl.Map | null>,
  projectionBusyRef: MutableRefObject<boolean>,
  reorderGlobeFeatureLayers: () => void,
  config: NativeMapLayersConfig,
  deps: readonly unknown[],
) {
  // 최신 config를 항상 읽기 위한 ref (deps에 config 객체를 넣지 않기 위함)
  const configRef = useRef(config);
  useEffect(() => {
    configRef.current = config;
  });

  /* ── 레이어 생성/데이터 업데이트 ─────────────────────────────────── */
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    let cancelled = false;
    let retryRaf: number | null = null;

    const ensure = () => {
      if (cancelled) return;
      const cfg = configRef.current;

      // 1. Visibility 토글
      for (const spec of cfg.layers) {
        setLayerVisibility(map, spec.id, cfg.visible);
      }

      // projection transition 중에는 가시성 토글만 먼저 반영하고,
      // source/layer 업데이트는 transition 종료 후 재시도한다.
      if (projectionBusyRef.current) {
        if (cfg.visible) {
          retryRaf = requestAnimationFrame(ensure);
        }
        return;
      }

      // 2. 데이터가 있는 source가 하나도 없으면 종료
      const hasData = cfg.sources.some((s) => s.data != null);
      if (!hasData) return;
      // isStyleLoaded() 가드 제거 — AIS poll의 setData()로 인해
      // 일시적으로 false가 되어 데이터 업데이트가 스킵되는 문제 방지.
      // 실패 시 try/catch가 처리하고, 다음 deps 변경 시 자연 재시도.

      try {
        // 3. Source 생성/업데이트
        for (const src of cfg.sources) {
          if (src.data) {
            ensureGeoJsonSource(map, src.id, src.data, src.options);
          }
        }

        // 4. Before layer 해석
        let before: string | undefined;
        if (cfg.beforeLayer) {
          const candidates = Array.isArray(cfg.beforeLayer) ? cfg.beforeLayer : [cfg.beforeLayer];
          for (const candidate of candidates) {
            if (map.getLayer(candidate)) {
              before = candidate;
              break;
            }
          }
        }

        // 5. Layer 생성
        const vis = cfg.visible ? 'visible' : 'none';
        for (const spec of cfg.layers) {
          const layerDef: Record<string, unknown> = {
            id: spec.id,
            type: spec.type,
            source: spec.sourceId,
            paint: spec.paint,
            layout: { ...spec.layout, visibility: vis },
          };
          if (spec.filter) layerDef.filter = spec.filter;
          if (spec.minzoom != null) layerDef.minzoom = spec.minzoom;
          if (spec.maxzoom != null) layerDef.maxzoom = spec.maxzoom;

          ensureLayer(map, layerDef as unknown as LayerSpecification, { before });
        }

        // 6. Post-setup callback
        if (cfg.onAfterSetup) {
          cfg.onAfterSetup(map);
        }
      } catch (e) {
        console.warn('Native map layers setup failed:', e);
      } finally {
        reorderGlobeFeatureLayers();
        kickRepaint(map);
      }
    };

    const stop = onMapStyleReady(map, ensure);
    return () => {
      cancelled = true;
      if (retryRaf != null) {
        cancelAnimationFrame(retryRaf);
      }
      stop();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);

  /* ── Unmount cleanup ─────────────────────────────────────────────── */
  useEffect(() => {
    const mapInstance = mapRef.current;
    return () => {
      if (!mapInstance) return;
      const cfg = configRef.current;
      const layerIds = [...cfg.layers].reverse().map((l) => l.id);
      const sourceIds = [...cfg.sources].reverse().map((s) => s.id);
      cleanupLayers(mapInstance, layerIds, sourceIds);
    };
  }, []);
}
