'use client';

/* eslint-disable import-x/no-named-as-default-member */
import mapboxgl from 'mapbox-gl';
import * as React from 'react';
import 'mapbox-gl/dist/mapbox-gl.css';
import { useMapboxStyle } from '@/lib/mapbox-style';

const VN_CENTER: [number, number] = [106.0, 16.0];
const DEFAULT_ZOOM = 5;

const SOURCE_ID = 'osm-parks';
const CLUSTER_LAYER_ID = 'osm-parks-clusters';
const CLUSTER_COUNT_LAYER_ID = 'osm-parks-cluster-count';
const POINT_LAYER_ID = 'osm-parks-points';
const BOUNDARY_FILL_LAYER_ID = 'osm-parks-boundaries-fill';
const BOUNDARY_LINE_LAYER_ID = 'osm-parks-boundaries-line';

interface OsmParkBboxMapProps {
  className?: string;
  /** Override the bbox API path. Default = `${NEXT_PUBLIC_API_URL}/industrial/parks/by-bbox`. */
  apiPath?: string;
  /** Show raw OSM-imported parks (admin tools). Default false. */
  includeOsmRaw?: boolean;
}

/**
 * Viewport-driven KCN map. Pulls parks from the bbox endpoint as the user
 * pans/zooms — clusters at low zoom (<12), shows polygon outlines at
 * high zoom. Designed for the public catalog where we have ~2000 OSM
 * imports + 50 curated rows; loading the entire dataset eagerly would
 * be wasteful.
 */
export function OsmParkBboxMap({
  className,
  apiPath,
  includeOsmRaw = false,
}: OsmParkBboxMapProps) {
  const containerRef = React.useRef<HTMLDivElement>(null);
  const mapRef = React.useRef<mapboxgl.Map | null>(null);
  const fetchAbortRef = React.useRef<AbortController | null>(null);
  const mapStyle = useMapboxStyle();

  const apiBase = React.useMemo(() => {
    if (apiPath) return apiPath;
    const apiUrl = process.env['NEXT_PUBLIC_API_URL'] ?? 'http://localhost:3201/api/v1';
    return `${apiUrl}/industrial/parks/by-bbox`;
  }, [apiPath]);

  // Capture the current includeOsmRaw value via a ref so the moveend
  // handler always sees the latest without re-binding the listener.
  const includeOsmRawRef = React.useRef(includeOsmRaw);
  // Bumping this triggers a manual refetch when the toggle changes —
  // the moveend handler alone doesn't fire on prop changes.
  const refetchTokenRef = React.useRef<(() => void) | null>(null);
  React.useEffect(() => {
    includeOsmRawRef.current = includeOsmRaw;
    refetchTokenRef.current?.();
  }, [includeOsmRaw]);

  React.useEffect(() => {
    if (!containerRef.current) return;
    const token = process.env['NEXT_PUBLIC_MAPBOX_TOKEN'];
    if (!token) return;
    mapboxgl.accessToken = token;

    const map = new mapboxgl.Map({
      container: containerRef.current,
      style: mapStyle,
      center: VN_CENTER,
      zoom: DEFAULT_ZOOM,
      attributionControl: false,
    });
    map.addControl(new mapboxgl.NavigationControl(), 'top-right');
    map.addControl(
      new mapboxgl.AttributionControl({ compact: true, customAttribution: 'Data © OSM' }),
      'bottom-right',
    );
    mapRef.current = map;

    const fetchParks = async () => {
      try {
        // Cancel any in-flight request — only the latest viewport matters.
        fetchAbortRef.current?.abort();
        const controller = new AbortController();
        fetchAbortRef.current = controller;
        const bounds = map.getBounds();
        if (!bounds) return;
        const sw = bounds.getSouthWest();
        const ne = bounds.getNorthEast();
        const zoom = Math.round(map.getZoom());
        const params = new URLSearchParams({
          south: sw.lat.toString(),
          west: sw.lng.toString(),
          north: ne.lat.toString(),
          east: ne.lng.toString(),
          zoom: zoom.toString(),
          ...(includeOsmRawRef.current ? { includeOsmRaw: 'true' } : {}),
        });
        const res = await fetch(`${apiBase}?${params}`, {
          credentials: 'include',
          signal: controller.signal,
        });
        if (!res.ok) return;
        const fc = (await res.json()) as GeoJSON.FeatureCollection;
        const src = map.getSource(SOURCE_ID) as mapboxgl.GeoJSONSource | undefined;
        if (src) src.setData(fc);
      } catch (err) {
        if (err instanceof Error && err.name === 'AbortError') return;
        console.warn('[osm-park-bbox-map] fetch failed:', err);
      }
    };

    map.on('load', () => {
      // Empty source — populated by the first fetchParks() call below.
      map.addSource(SOURCE_ID, {
        type: 'geojson',
        data: { type: 'FeatureCollection', features: [] },
        cluster: true,
        clusterRadius: 50,
        clusterMaxZoom: 11,
        clusterProperties: {
          // No extra metrics yet — total count is built-in.
        },
      });

      // Cluster bubbles. Mapbox color parser only accepts literal colors,
      // so we use hex constants matching our design-system primary token.
      map.addLayer({
        id: CLUSTER_LAYER_ID,
        type: 'circle',
        source: SOURCE_ID,
        filter: ['has', 'point_count'],
        paint: {
          'circle-color': [
            'step',
            ['get', 'point_count'],
            '#22c55e', // primary
            10,
            '#f59e0b',
            50,
            '#ef4444',
          ],
          'circle-radius': [
            'step',
            ['get', 'point_count'],
            16,
            10,
            22,
            50,
            30,
          ],
          'circle-stroke-width': 2,
          'circle-stroke-color': '#ffffff',
          'circle-opacity': 0.9,
        },
      });

      map.addLayer({
        id: CLUSTER_COUNT_LAYER_ID,
        type: 'symbol',
        source: SOURCE_ID,
        filter: ['has', 'point_count'],
        layout: {
          'text-field': ['get', 'point_count_abbreviated'],
          'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
          'text-size': 12,
        },
        paint: { 'text-color': '#ffffff' },
      });

      // Individual park markers (centroid Points) when not clustered.
      // Color is data-driven: green = curated (MANUAL / OSM_PROMOTED),
      // amber = raw OSM imports awaiting admin review.
      map.addLayer({
        id: POINT_LAYER_ID,
        type: 'circle',
        source: SOURCE_ID,
        filter: [
          'all',
          ['!', ['has', 'point_count']],
          ['==', ['get', '_kind'], 'point'],
        ],
        paint: {
          'circle-color': [
            'case',
            ['==', ['get', 'dataSource'], 'OSM'],
            '#f59e0b', // amber for raw OSM
            '#22c55e', // green for curated
          ],
          'circle-radius': [
            'case',
            ['==', ['get', 'dataSource'], 'OSM'],
            5,
            6,
          ],
          'circle-stroke-color': '#ffffff',
          'circle-stroke-width': 1.5,
          'circle-opacity': [
            'case',
            ['==', ['get', 'dataSource'], 'OSM'],
            0.7,
            1,
          ],
        },
      });

      // Polygon outlines — only present when zoom >= 12 (server omits them
      // at lower zoom). Fill layer for hit-test, line layer for stroke.
      map.addLayer({
        id: BOUNDARY_FILL_LAYER_ID,
        type: 'fill',
        source: SOURCE_ID,
        filter: ['==', ['get', '_kind'], 'polygon'],
        paint: {
          'fill-color': [
            'case',
            ['==', ['get', 'dataSource'], 'OSM'],
            '#f59e0b',
            '#22c55e',
          ],
          'fill-opacity': [
            'case',
            ['==', ['get', 'dataSource'], 'OSM'],
            0.1,
            0.18,
          ],
        },
      });
      map.addLayer({
        id: BOUNDARY_LINE_LAYER_ID,
        type: 'line',
        source: SOURCE_ID,
        filter: ['==', ['get', '_kind'], 'polygon'],
        paint: {
          'line-color': [
            'case',
            ['==', ['get', 'dataSource'], 'OSM'],
            '#f59e0b',
            '#22c55e',
          ],
          'line-width': 2,
          'line-opacity': [
            'case',
            ['==', ['get', 'dataSource'], 'OSM'],
            0.4,
            0.6,
          ],
        },
      });

      // Click handler on point/polygon → navigate to detail.
      const onClick = (e: mapboxgl.MapLayerMouseEvent) => {
        const f = e.features?.[0];
        if (!f) return;
        const slug = (f.properties as Record<string, unknown> | null)?.['slug'];
        if (typeof slug === 'string' && slug.length > 0) {
          window.location.href = `/vi/khu-cong-nghiep/${slug}`;
        }
      };
      map.on('click', POINT_LAYER_ID, onClick);
      map.on('click', BOUNDARY_FILL_LAYER_ID, onClick);
      // Cursor feedback
      for (const layerId of [POINT_LAYER_ID, BOUNDARY_FILL_LAYER_ID, CLUSTER_LAYER_ID]) {
        map.on('mouseenter', layerId, () => {
          map.getCanvas().style.cursor = 'pointer';
        });
        map.on('mouseleave', layerId, () => {
          map.getCanvas().style.cursor = '';
        });
      }
      // Cluster click — zoom in
      map.on('click', CLUSTER_LAYER_ID, (e) => {
        const features = map.queryRenderedFeatures(e.point, { layers: [CLUSTER_LAYER_ID] });
        const clusterFeature = features[0];
        if (!clusterFeature) return;
        const clusterId = clusterFeature.properties?.['cluster_id'];
        const src = map.getSource(SOURCE_ID) as mapboxgl.GeoJSONSource;
        if (typeof clusterId === 'number') {
          src.getClusterExpansionZoom(
            clusterId,
            (err: Error | null | undefined, zoom: number | null | undefined) => {
              if (err || zoom == null) return;
              const geom = clusterFeature.geometry;
              if (geom.type === 'Point') {
                map.easeTo({ center: geom.coordinates as [number, number], zoom });
              }
            },
          );
        }
      });

      // Initial fetch + listen to viewport changes.
      void fetchParks();
      // Wire up the prop-change refetch (used when `includeOsmRaw` flips
      // — the moveend listener alone doesn't fire on parent re-renders).
      refetchTokenRef.current = () => {
        void fetchParks();
      };
    });

    map.on('moveend', () => {
      void fetchParks();
    });

    return () => {
      fetchAbortRef.current?.abort();
      map.remove();
      mapRef.current = null;
    };
    // We intentionally do NOT depend on includeOsmRaw — the ref-based
    // approach avoids tearing down the map on every prop tick.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [apiBase]);

  // Sync mapStyle (theme switch) without rebuilding the map.
  React.useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    map.setStyle(mapStyle);
  }, [mapStyle]);

  const hasToken = typeof process !== 'undefined' && process.env['NEXT_PUBLIC_MAPBOX_TOKEN'];

  return (
    <div className={`relative overflow-hidden rounded-lg border ${className || 'h-[600px]'}`}>
      <div ref={containerRef} className="h-full w-full" />
      {!hasToken && (
        <div className="absolute inset-0 flex items-center justify-center bg-muted text-sm text-muted-foreground">
          Thiết lập NEXT_PUBLIC_MAPBOX_TOKEN để hiển thị bản đồ
        </div>
      )}
    </div>
  );
}
