import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { Layer, PickingInfo } from '@deck.gl/core';
import { PathLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers';
import Supercluster from 'supercluster';

import type { ActualListing } from '../types';
import { trackEvent } from '../lib/analytics';

const PRICE_LABEL_MIN_ZOOM = 14;
const ADDRESS_LABEL_MIN_ZOOM = 16;
const LISTING_CLUSTER_RADIUS = 18;
const LISTING_CLUSTER_MAX_ZOOM = 24;
const LISTING_CLUSTER_POPUP_LIMIT = 30;
const LISTING_SPIDERFY_LIMIT = 12;
const TILE_SIZE = 512;
const PRICE_LABEL_CHARACTER_SET = '£0123456789.kM';

interface SingleListingPopupInfo {
  mode: 'single';
  x: number;
  y: number;
  listing: ActualListing;
  locked?: boolean;
}

interface ListingClusterPopupInfo {
  mode: 'cluster';
  x: number;
  y: number;
  count: number;
  listings: ActualListing[];
  locked?: boolean;
}

export type ListingPopupInfo = SingleListingPopupInfo | ListingClusterPopupInfo;

interface UseListingLayersProps {
  listings: ActualListing[];
  zoom: number;
  isDark: boolean;
}

interface ListingClusterPoint {
  lng: number;
  lat: number;
  count: number;
  clusterId: number;
}

interface ExpandedListingMarker {
  listing: ActualListing;
  lng: number;
  lat: number;
  anchorLng: number;
  anchorLat: number;
}

function formatShortPrice(price: number): string {
  if (price >= 1_000_000) return `£${(price / 1_000_000).toFixed(price >= 10_000_000 ? 0 : 1)}M`;
  if (price >= 1_000) return `£${Math.round(price / 1_000)}k`;
  return `£${price}`;
}

function formatClusterCount(count: number): string {
  if (count >= 1_000) return `${(count / 1_000).toFixed(count >= 10_000 ? 0 : 1)}k`;
  return String(count);
}

function compareListingsForDisplay(left: ActualListing, right: ActualListing): number {
  const dateCompare = (right.listing_date_iso ?? '').localeCompare(left.listing_date_iso ?? '');
  if (dateCompare !== 0) return dateCompare;
  return (right.asking_price ?? 0) - (left.asking_price ?? 0);
}

function getClusterListings(
  index: Supercluster<ActualListing>,
  clusterId: number,
  limit: number
): ActualListing[] {
  return index
    .getLeaves(clusterId, limit, 0)
    .map((feature) => feature.properties)
    .sort(compareListingsForDisplay);
}

function offsetLngLat(
  lng: number,
  lat: number,
  dxPixels: number,
  dyPixels: number,
  zoom: number
): [number, number] {
  const worldSize = TILE_SIZE * Math.pow(2, zoom);
  const lngPerPixel = 360 / worldSize;
  const cosLat = Math.max(0.25, Math.cos((lat * Math.PI) / 180));
  const latPerPixel = lngPerPixel / cosLat;
  return [lng + dxPixels * lngPerPixel, lat - dyPixels * latPerPixel];
}

function spiderfyPosition(
  lng: number,
  lat: number,
  index: number,
  total: number,
  zoom: number
): [number, number] {
  if (total <= 1) return [lng, lat];
  const radius = total <= 6 ? 24 : 32;
  const angle = -Math.PI / 2 + (index / total) * Math.PI * 2;
  return offsetLngLat(lng, lat, Math.cos(angle) * radius, Math.sin(angle) * radius, zoom);
}

export function useListingLayers({ listings, zoom, isDark }: UseListingLayersProps) {
  const [popupInfo, setPopupInfo] = useState<ListingPopupInfo | null>(null);
  const [selectedCluster, setSelectedCluster] = useState<ListingClusterPoint | null>(null);

  useEffect(() => {
    // Each refetch returns a fresh listings array and rebuilds the cluster index, so a
    // previously selected cluster's id is no longer valid (getLeaves would throw) —
    // always collapse the spiderfy. A locked popup, however, holds a self-contained
    // snapshot of the listings captured at click time, so preserve it across background
    // refetches instead of dismissing it (mirrors clearUnlockedPopup).
    setSelectedCluster(null);
    setPopupInfo((current) => (current?.locked ? current : null));
  }, [listings]);

  const clusterIndex = useMemo(() => {
    if (listings.length === 0) return null;
    const index = new Supercluster<ActualListing>({
      radius: LISTING_CLUSTER_RADIUS,
      maxZoom: LISTING_CLUSTER_MAX_ZOOM,
    });
    const features: Supercluster.PointFeature<ActualListing>[] = listings
      .filter((listing) => Number.isFinite(listing.lat) && Number.isFinite(listing.lon))
      .map((listing) => ({
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [listing.lon, listing.lat] },
        properties: listing,
      }));
    index.load(features);
    return index;
  }, [listings]);

  const clusterIndexRef = useRef(clusterIndex);
  clusterIndexRef.current = clusterIndex;

  const clusterZoom = Math.min(Math.floor(zoom), LISTING_CLUSTER_MAX_ZOOM);
  const { visibleListings, clusters } = useMemo(() => {
    if (!clusterIndex) {
      return {
        visibleListings: [] as ActualListing[],
        clusters: [] as ListingClusterPoint[],
      };
    }

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const features = clusterIndex.getClusters([-180, -85, 180, 85], clusterZoom) as any[];
    const individual: ActualListing[] = [];
    const clusterPoints: ListingClusterPoint[] = [];
    for (const feature of features) {
      if (feature.properties.cluster) {
        clusterPoints.push({
          lng: feature.geometry.coordinates[0],
          lat: feature.geometry.coordinates[1],
          count: feature.properties.point_count,
          clusterId: feature.properties.cluster_id,
        });
      } else {
        individual.push(feature.properties as ActualListing);
      }
    }
    return { visibleListings: individual, clusters: clusterPoints };
  }, [clusterIndex, clusterZoom]);

  const expandedListings = useMemo(() => {
    if (!selectedCluster || !clusterIndex) return [];
    const leaves = getClusterListings(
      clusterIndex,
      selectedCluster.clusterId,
      LISTING_SPIDERFY_LIMIT
    );
    return leaves.map((listing, index) => {
      const [lng, lat] = spiderfyPosition(
        selectedCluster.lng,
        selectedCluster.lat,
        index,
        leaves.length,
        zoom
      );
      return {
        listing,
        lng,
        lat,
        anchorLng: selectedCluster.lng,
        anchorLat: selectedCluster.lat,
      };
    });
  }, [clusterIndex, selectedCluster, zoom]);

  const clearUnlockedPopup = useCallback(() => {
    setPopupInfo((current) => (current?.locked ? current : null));
  }, []);

  const handleHover = useCallback(
    (info: PickingInfo<ActualListing>) => {
      if (info.object && info.x !== undefined && info.y !== undefined) {
        setPopupInfo({ mode: 'single', x: info.x, y: info.y, listing: info.object });
      } else {
        clearUnlockedPopup();
      }
    },
    [clearUnlockedPopup]
  );

  const handleClick = useCallback((info: PickingInfo<ActualListing>) => {
    const url = info.object?.listing_url;
    if (!url) return;
    trackEvent('Actual Listing Click', { url });
    window.open(url, '_blank', 'noopener,noreferrer');
  }, []);

  const handleHoverRef = useRef(handleHover);
  handleHoverRef.current = handleHover;
  const stableHover = useCallback(
    (info: PickingInfo<ActualListing>) => handleHoverRef.current(info),
    []
  );

  const handleClickRef = useRef(handleClick);
  handleClickRef.current = handleClick;
  const stableClick = useCallback(
    (info: PickingInfo<ActualListing>) => handleClickRef.current(info),
    []
  );

  const handleExpandedHover = useCallback(
    (info: PickingInfo<ExpandedListingMarker>) => {
      if (info.object && info.x !== undefined && info.y !== undefined) {
        setPopupInfo({ mode: 'single', x: info.x, y: info.y, listing: info.object.listing });
      } else {
        clearUnlockedPopup();
      }
    },
    [clearUnlockedPopup]
  );

  const handleExpandedClick = useCallback((info: PickingInfo<ExpandedListingMarker>) => {
    const url = info.object?.listing.listing_url;
    if (!url) return;
    trackEvent('Actual Listing Click', { url, source: 'cluster_expanded' });
    window.open(url, '_blank', 'noopener,noreferrer');
  }, []);

  const handleExpandedHoverRef = useRef(handleExpandedHover);
  handleExpandedHoverRef.current = handleExpandedHover;
  const stableExpandedHover = useCallback(
    (info: PickingInfo<ExpandedListingMarker>) => handleExpandedHoverRef.current(info),
    []
  );

  const handleExpandedClickRef = useRef(handleExpandedClick);
  handleExpandedClickRef.current = handleExpandedClick;
  const stableExpandedClick = useCallback(
    (info: PickingInfo<ExpandedListingMarker>) => handleExpandedClickRef.current(info),
    []
  );

  const handleClusterHover = useCallback(
    (info: PickingInfo<ListingClusterPoint>) => {
      if (info.object && info.x !== undefined && info.y !== undefined) {
        const cluster = info.object;
        setPopupInfo((current) =>
          current?.locked
            ? current
            : {
                mode: 'cluster',
                x: info.x,
                y: info.y,
                count: cluster.count,
                listings: [],
              }
        );
      } else {
        clearUnlockedPopup();
      }
    },
    [clearUnlockedPopup]
  );

  const handleClusterClick = useCallback((info: PickingInfo<ListingClusterPoint>) => {
    if (!info.object || info.x === undefined || info.y === undefined) return;
    const index = clusterIndexRef.current;
    if (!index) return;
    const cluster = info.object;
    const clusterListings = getClusterListings(
      index,
      cluster.clusterId,
      LISTING_CLUSTER_POPUP_LIMIT
    );
    setSelectedCluster(cluster);
    setPopupInfo({
      mode: 'cluster',
      x: info.x,
      y: info.y,
      count: cluster.count,
      listings: clusterListings,
      locked: true,
    });
    trackEvent('Actual Listing Cluster Click', { count: cluster.count });
  }, []);

  const handleClusterHoverRef = useRef(handleClusterHover);
  handleClusterHoverRef.current = handleClusterHover;
  const stableClusterHover = useCallback(
    (info: PickingInfo<ListingClusterPoint>) => handleClusterHoverRef.current(info),
    []
  );

  const handleClusterClickRef = useRef(handleClusterClick);
  handleClusterClickRef.current = handleClusterClick;
  const stableClusterClick = useCallback(
    (info: PickingInfo<ListingClusterPoint>) => handleClusterClickRef.current(info),
    []
  );

  const pinShadowLayer = useMemo(
    () =>
      new ScatterplotLayer<ActualListing>({
        id: 'actual-listing-shadow',
        data: visibleListings,
        getPosition: (d) => [d.lon, d.lat],
        getRadius: 8,
        radiusUnits: 'pixels',
        getFillColor: isDark ? [0, 0, 0, 80] : [0, 0, 0, 40],
        pickable: false,
      }),
    [visibleListings, isDark]
  );

  const pinLayer = useMemo(
    () =>
      new ScatterplotLayer<ActualListing>({
        id: 'actual-listing-pin',
        data: visibleListings,
        getPosition: (d) => [d.lon, d.lat],
        getRadius: 7,
        radiusUnits: 'pixels',
        getFillColor: [231, 76, 60, 240],
        getLineColor: [255, 255, 255, 255],
        getLineWidth: 1.5,
        lineWidthUnits: 'pixels',
        stroked: true,
        pickable: true,
        autoHighlight: true,
        highlightColor: [29, 228, 195, 220],
        onHover: stableHover,
        onClick: stableClick,
      }),
    [visibleListings, stableHover, stableClick]
  );

  const clusterShadowLayer = useMemo(
    () =>
      new ScatterplotLayer<ListingClusterPoint>({
        id: 'actual-listing-cluster-shadow',
        data: clusters,
        getPosition: (d) => [d.lng, d.lat],
        getRadius: (d) => Math.min(32, 13 + Math.sqrt(d.count) * 1.8),
        radiusUnits: 'pixels',
        getFillColor: isDark ? [0, 0, 0, 90] : [0, 0, 0, 45],
        pickable: false,
      }),
    [clusters, isDark]
  );

  const clusterLayer = useMemo(
    () =>
      new ScatterplotLayer<ListingClusterPoint>({
        id: 'actual-listing-cluster',
        data: clusters,
        getPosition: (d) => [d.lng, d.lat],
        getRadius: (d) => Math.min(30, 12 + Math.sqrt(d.count) * 1.8),
        radiusUnits: 'pixels',
        getFillColor: isDark ? [185, 28, 28, 230] : [220, 38, 38, 230],
        getLineColor: [255, 255, 255, isDark ? 90 : 180],
        getLineWidth: 2,
        lineWidthUnits: 'pixels',
        stroked: true,
        pickable: true,
        autoHighlight: true,
        highlightColor: [29, 228, 195, 220],
        onHover: stableClusterHover,
        onClick: stableClusterClick,
      }),
    [clusters, isDark, stableClusterHover, stableClusterClick]
  );

  const clusterTextLayer = useMemo(
    () =>
      new TextLayer<ListingClusterPoint>({
        id: 'actual-listing-cluster-text',
        data: clusters,
        getPosition: (d) => [d.lng, d.lat],
        getText: (d) => formatClusterCount(d.count),
        getSize: 12,
        getColor: [255, 255, 255, 255],
        fontFamily: 'Inter, system-ui, sans-serif',
        fontWeight: 800,
        getTextAnchor: 'middle',
        getAlignmentBaseline: 'center',
        sizeUnits: 'pixels',
        sizeMinPixels: 10,
        sizeMaxPixels: 13,
        pickable: false,
      }),
    [clusters]
  );

  const expandedConnectorLayer = useMemo(
    () =>
      new PathLayer<ExpandedListingMarker>({
        id: 'actual-listing-expanded-lines',
        data: expandedListings,
        getPath: (d) => [
          [d.anchorLng, d.anchorLat],
          [d.lng, d.lat],
        ],
        getColor: isDark ? [255, 255, 255, 80] : [80, 60, 50, 110],
        getWidth: 1,
        widthUnits: 'pixels',
        pickable: false,
      }),
    [expandedListings, isDark]
  );

  const expandedPinLayer = useMemo(
    () =>
      new ScatterplotLayer<ExpandedListingMarker>({
        id: 'actual-listing-expanded-pin',
        data: expandedListings,
        getPosition: (d) => [d.lng, d.lat],
        getRadius: 6,
        radiusUnits: 'pixels',
        getFillColor: [231, 76, 60, 245],
        getLineColor: [255, 255, 255, 255],
        getLineWidth: 1.5,
        lineWidthUnits: 'pixels',
        stroked: true,
        pickable: true,
        autoHighlight: true,
        highlightColor: [29, 228, 195, 220],
        onHover: stableExpandedHover,
        onClick: stableExpandedClick,
      }),
    [expandedListings, stableExpandedHover, stableExpandedClick]
  );

  const priceLabelLayer = useMemo(() => {
    if (zoom < PRICE_LABEL_MIN_ZOOM) return null;
    const labeled = visibleListings.filter((l) => l.asking_price && l.asking_price > 0);
    return new TextLayer<ActualListing>({
      id: 'actual-listing-price',
      data: labeled,
      getPosition: (d) => [d.lon, d.lat],
      getText: (d) => formatShortPrice(d.asking_price ?? 0),
      getSize: 12,
      getPixelOffset: [0, -16],
      getColor: isDark ? [255, 255, 255, 240] : [30, 30, 30, 240],
      fontFamily: 'Inter, system-ui, sans-serif',
      fontWeight: 700,
      getTextAnchor: 'middle',
      getAlignmentBaseline: 'bottom',
      outlineWidth: 3,
      outlineColor: isDark ? [10, 10, 10, 220] : [255, 255, 255, 230],
      fontSettings: { sdf: true },
      characterSet: PRICE_LABEL_CHARACTER_SET,
      sizeUnits: 'pixels',
      sizeMinPixels: 10,
      sizeMaxPixels: 14,
      pickable: false,
    });
  }, [visibleListings, zoom, isDark]);

  const detailLabelLayer = useMemo(() => {
    if (zoom < ADDRESS_LABEL_MIN_ZOOM) return null;
    const labeled = visibleListings.filter((l) => l.address || l.bedrooms != null);
    return new TextLayer<ActualListing>({
      id: 'actual-listing-detail',
      data: labeled,
      getPosition: (d) => [d.lon, d.lat],
      getText: (d) => {
        const parts: string[] = [];
        if (d.bedrooms != null) parts.push(`${d.bedrooms} bed`);
        if (d.property_sub_type) parts.push(d.property_sub_type);
        else if (d.property_type) parts.push(d.property_type);
        return parts.join(' · ');
      },
      getSize: 10,
      getPixelOffset: [0, 14],
      getColor: isDark ? [220, 220, 220, 230] : [60, 60, 60, 230],
      fontFamily: 'Inter, system-ui, sans-serif',
      fontWeight: 500,
      getTextAnchor: 'middle',
      getAlignmentBaseline: 'top',
      outlineWidth: 3,
      outlineColor: isDark ? [10, 10, 10, 220] : [255, 255, 255, 230],
      fontSettings: { sdf: true },
      sizeUnits: 'pixels',
      sizeMinPixels: 9,
      sizeMaxPixels: 12,
      pickable: false,
    });
  }, [visibleListings, zoom, isDark]);

  const listingLayers = useMemo(() => {
    const layers: Layer[] = [
      clusterShadowLayer,
      clusterLayer,
      clusterTextLayer,
      pinShadowLayer,
      pinLayer,
    ];
    if (expandedListings.length > 0) {
      layers.push(expandedConnectorLayer, expandedPinLayer);
    }
    if (priceLabelLayer) layers.push(priceLabelLayer);
    if (detailLabelLayer) layers.push(detailLabelLayer);
    return layers;
  }, [
    clusterShadowLayer,
    clusterLayer,
    clusterTextLayer,
    pinShadowLayer,
    pinLayer,
    expandedListings.length,
    expandedConnectorLayer,
    expandedPinLayer,
    priceLabelLayer,
    detailLabelLayer,
  ]);

  const clearListingPopup = useCallback(() => {
    setPopupInfo(null);
    setSelectedCluster(null);
  }, []);

  return { listingLayers, listingPopup: popupInfo, clearListingPopup };
}
