import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { cellToParent, latLngToCell } from 'h3-js';
import { trackEvent } from '../lib/analytics';
import type {
  FeatureMeta,
  FeatureFilters,
  HexagonData,
  Property,
  PostcodeGeometry,
  HexagonPropertiesResponse,
  HexagonStatsResponse,
} from '../types';
import { buildFilterString, apiUrl, assertOk, logNonAbortError, authHeaders } from '../lib/api';
import { findOverlappingSelectableHexagon } from '../lib/h3-selection';
import { SMALLEST_VISIBLE_HEXAGON_RESOLUTION } from '../lib/consts';
import type { TravelTimeEntry } from './useTravelTime';
import { buildTravelParam } from '../lib/travel-params';

interface SelectedHexagon {
  id: string;
  type: 'hexagon' | 'postcode';
  resolution: number;
  lockedResolution?: boolean;
  /**
   * The postcode the user is really tracking. Set when a postcode is selected
   * (or first derived from a hexagon) and carried through zoom-driven hexagon
   * conversions, so that zooming out and back in restores the same postcode
   * instead of drifting to a neighbour whose centre happens to fall in the same
   * cell. Cleared implicitly on every explicit (re)selection.
   */
  anchorPostcode?: { id: string; geometry: PostcodeGeometry | null };
}

interface JourneyDest {
  mode: string;
  slug: string;
}

interface PostcodeLookupResponse {
  postcode: string;
  latitude: number;
  longitude: number;
  geometry: PostcodeGeometry;
}

interface UseHexagonSelectionOptions {
  filters: FeatureFilters;
  features: FeatureMeta[];
  hexagonData: HexagonData[];
  resolution: number;
  usePostcodeView: boolean;
  travelTimeEntries: TravelTimeEntry[];
  areaStatsFields?: string[];
  shareCode?: string;
  /** First transit destination — used to pick the best central_postcode for journey display. */
  journeyDest?: JourneyDest | null;
}

const PROPERTY_PANE_FIELDS = [
  'Last known price',
  'Estimated current price',
  'Price per sqm',
  'Est. price per sqm',
  'Total floor area (sqm)',
  'Number of bedrooms & living rooms',
  'Construction year',
  'Date of last transaction',
];

export function useHexagonSelection({
  filters,
  features,
  hexagonData,
  resolution,
  usePostcodeView,
  travelTimeEntries,
  areaStatsFields,
  shareCode,
  journeyDest,
}: UseHexagonSelectionOptions) {
  const [selectedHexagon, setSelectedHexagon] = useState<SelectedHexagon | null>(null);
  const [properties, setProperties] = useState<Property[]>([]);
  const [propertiesTotal, setPropertiesTotal] = useState(0);
  const [propertiesOffset, setPropertiesOffset] = useState(0);
  const [loadingProperties, setLoadingProperties] = useState(false);
  const [areaStats, setAreaStats] = useState<HexagonStatsResponse | null>(null);
  const [unfilteredAreaCount, setUnfilteredAreaCount] = useState<number | null>(null);
  const [loadingAreaStats, setLoadingAreaStats] = useState(false);
  const [hoveredHexagon, setHoveredHexagon] = useState<string | null>(null);
  const [rightPaneTab, setRightPaneTab] = useState<'properties' | 'area'>('area');
  const [areaStatsUseFilters, setAreaStatsUseFilters] = useState(true);
  const [selectedPostcodeGeometry, setSelectedPostcodeGeometry] = useState<PostcodeGeometry | null>(
    null
  );
  const areaRequestIdRef = useRef(0);
  const propertiesRequestIdRef = useRef(0);

  const invalidateAreaRequests = useCallback(() => {
    areaRequestIdRef.current += 1;
    return areaRequestIdRef.current;
  }, []);

  const invalidatePropertyRequests = useCallback(() => {
    propertiesRequestIdRef.current += 1;
    return propertiesRequestIdRef.current;
  }, []);

  const isCurrentAreaRequest = useCallback((requestId: number) => {
    return areaRequestIdRef.current === requestId;
  }, []);

  const isCurrentPropertyRequest = useCallback((requestId: number) => {
    return propertiesRequestIdRef.current === requestId;
  }, []);

  const travelParam = useMemo(() => buildTravelParam(travelTimeEntries), [travelTimeEntries]);
  const areaStatsFieldsKey = useMemo(() => areaStatsFields?.join(';;') ?? '', [areaStatsFields]);
  const propertyPaneFieldsParam = useMemo(() => {
    const availableFields = new Set(features.map((feature) => feature.name));
    return PROPERTY_PANE_FIELDS.filter((field) => availableFields.has(field)).join(';;');
  }, [features]);

  const fetchHexagonStats = useCallback(
    async (
      h3: string,
      res: number,
      signal?: AbortSignal,
      fields?: string[],
      includeFilters = true
    ) => {
      const params = new URLSearchParams({
        h3,
        resolution: res.toString(),
      });
      const filterStr = includeFilters ? buildFilterString(filters, features) : '';
      if (filterStr) params.append('filters', filterStr);
      if (includeFilters && travelParam) params.set('travel', travelParam);
      if (shareCode) params.set('share', shareCode);
      const requestedFields = fields ?? areaStatsFields;
      if (requestedFields) {
        params.set('fields', requestedFields.join(';;'));
      }
      if (journeyDest) {
        params.set('journey_mode', journeyDest.mode);
        params.set('journey_slug', journeyDest.slug);
      }
      const response = await fetch(apiUrl('hexagon-stats', params), authHeaders({ signal }));
      assertOk(response, 'hexagon-stats');
      return (await response.json()) as HexagonStatsResponse;
    },
    [areaStatsFields, filters, features, journeyDest, shareCode, travelParam]
  );

  const fetchPostcodeStats = useCallback(
    async (postcode: string, signal?: AbortSignal, includeFilters = true, fields?: string[]) => {
      const params = new URLSearchParams({ postcode });
      const filterStr = includeFilters ? buildFilterString(filters, features) : '';
      if (filterStr) params.append('filters', filterStr);
      if (includeFilters && travelParam) params.set('travel', travelParam);
      if (shareCode) params.set('share', shareCode);
      const requestedFields = fields ?? areaStatsFields;
      if (requestedFields) params.set('fields', requestedFields.join(';;'));
      const response = await fetch(apiUrl('postcode-stats', params), authHeaders({ signal }));
      assertOk(response, 'postcode-stats');
      return (await response.json()) as HexagonStatsResponse;
    },
    [areaStatsFields, filters, features, shareCode, travelParam]
  );

  const filterStr = useMemo(() => buildFilterString(filters, features), [filters, features]);
  const hasStatsFilters = filterStr.length > 0 || travelParam.length > 0;
  const journeyKey = journeyDest ? `${journeyDest.mode}:${journeyDest.slug}` : '';
  const areaStatsDataKey = useMemo(
    () =>
      [
        areaStatsUseFilters ? 'filtered' : 'all',
        areaStatsUseFilters ? filterStr : '',
        areaStatsUseFilters ? travelParam : '',
        journeyKey,
        shareCode ?? '',
      ].join('|'),
    [areaStatsUseFilters, filterStr, journeyKey, shareCode, travelParam]
  );
  const areaStatsQueryKey = useMemo(
    () => [areaStatsDataKey, areaStatsFieldsKey].join('|'),
    [areaStatsDataKey, areaStatsFieldsKey]
  );

  const fetchUnfilteredAreaCount = useCallback(
    async (selection: SelectedHexagon, requestId: number, signal?: AbortSignal) => {
      if (!hasStatsFilters) {
        if (isCurrentAreaRequest(requestId)) setUnfilteredAreaCount(null);
        return;
      }

      const stats =
        selection.type === 'postcode'
          ? await fetchPostcodeStats(selection.id, signal, false, [])
          : await fetchHexagonStats(selection.id, selection.resolution, signal, [], false);
      if (isCurrentAreaRequest(requestId)) setUnfilteredAreaCount(stats.count);
    },
    [fetchHexagonStats, fetchPostcodeStats, hasStatsFilters, isCurrentAreaRequest]
  );

  const refreshUnfilteredAreaCount = useCallback(
    (
      selection: SelectedHexagon,
      statsCount: number,
      includeFilters: boolean,
      requestId: number,
      signal?: AbortSignal
    ) => {
      if (!includeFilters || !hasStatsFilters || statsCount > 0) {
        if (isCurrentAreaRequest(requestId)) setUnfilteredAreaCount(null);
        return;
      }

      fetchUnfilteredAreaCount(selection, requestId, signal).catch((error) =>
        logNonAbortError('Failed to fetch unfiltered area count', error)
      );
    },
    [fetchUnfilteredAreaCount, hasStatsFilters, isCurrentAreaRequest]
  );

  const fetchPostcodeLookup = useCallback(async (postcode: string, signal?: AbortSignal) => {
    const response = await fetch(
      `/api/postcode/${encodeURIComponent(postcode)}`,
      authHeaders({ signal })
    );
    assertOk(response, 'postcode lookup');
    return (await response.json()) as PostcodeLookupResponse;
  }, []);

  const fetchHexagonProperties = useCallback(
    async (h3: string, res: number, offset = 0) => {
      const requestId = invalidatePropertyRequests();
      setLoadingProperties(true);
      try {
        const params = new URLSearchParams({
          h3,
          resolution: res.toString(),
          offset: offset.toString(),
        });

        const filterStr = areaStatsUseFilters ? buildFilterString(filters, features) : '';
        if (filterStr) params.append('filters', filterStr);
        if (areaStatsUseFilters && travelParam) params.set('travel', travelParam);
        params.set('fields', propertyPaneFieldsParam);
        if (shareCode) params.set('share', shareCode);

        const response = await fetch(apiUrl('hexagon-properties', params), authHeaders());
        assertOk(response, 'hexagon-properties');
        const data: HexagonPropertiesResponse = await response.json();
        if (!isCurrentPropertyRequest(requestId)) return;

        if (offset === 0) {
          setProperties(data.properties);
        } else {
          setProperties((prev) => [...prev, ...data.properties]);
        }
        setPropertiesTotal(data.total);
        setPropertiesOffset(offset + data.properties.length);
      } catch (err) {
        logNonAbortError('Failed to fetch properties', err);
      } finally {
        if (isCurrentPropertyRequest(requestId)) setLoadingProperties(false);
      }
    },
    [
      filters,
      features,
      areaStatsUseFilters,
      invalidatePropertyRequests,
      isCurrentPropertyRequest,
      propertyPaneFieldsParam,
      shareCode,
      travelParam,
    ]
  );

  const fetchPostcodeProperties = useCallback(
    async (postcode: string, offset = 0, focusAddress?: string) => {
      const requestId = invalidatePropertyRequests();
      setLoadingProperties(true);
      try {
        const params = new URLSearchParams({
          postcode,
          offset: offset.toString(),
        });
        if (focusAddress && offset === 0) {
          params.set('focus_address', focusAddress);
        }

        const filterStr = areaStatsUseFilters ? buildFilterString(filters, features) : '';
        if (filterStr) params.append('filters', filterStr);
        if (areaStatsUseFilters && travelParam) params.set('travel', travelParam);
        params.set('fields', propertyPaneFieldsParam);
        if (shareCode) params.set('share', shareCode);

        const response = await fetch(apiUrl('postcode-properties', params), authHeaders());
        assertOk(response, 'postcode-properties');
        const data: HexagonPropertiesResponse = await response.json();
        if (!isCurrentPropertyRequest(requestId)) return;

        if (offset === 0) {
          setProperties(data.properties);
        } else {
          setProperties((prev) => [...prev, ...data.properties]);
        }
        setPropertiesTotal(data.total);
        setPropertiesOffset(offset + data.properties.length);
      } catch (err) {
        logNonAbortError('Failed to fetch postcode properties', err);
      } finally {
        if (isCurrentPropertyRequest(requestId)) setLoadingProperties(false);
      }
    },
    [
      filters,
      features,
      areaStatsUseFilters,
      invalidatePropertyRequests,
      isCurrentPropertyRequest,
      propertyPaneFieldsParam,
      shareCode,
      travelParam,
    ]
  );

  const handleHexagonClick = useCallback(
    (id: string, isPostcode = false, geometry?: PostcodeGeometry) => {
      if (selectedHexagon?.id === id) {
        invalidateAreaRequests();
        invalidatePropertyRequests();
        setSelectedHexagon(null);
        setProperties([]);
        setAreaStats(null);
        setUnfilteredAreaCount(null);
        setSelectedPostcodeGeometry(null);
      } else {
        const type: SelectedHexagon['type'] = isPostcode ? 'postcode' : 'hexagon';
        const selection: SelectedHexagon = {
          id,
          type,
          resolution,
          ...(isPostcode ? { anchorPostcode: { id, geometry: geometry ?? null } } : {}),
        };
        const requestId = invalidateAreaRequests();
        invalidatePropertyRequests();
        trackEvent('Hexagon Click', { type });
        setSelectedHexagon(selection);
        setSelectedPostcodeGeometry(isPostcode && geometry ? geometry : null);
        setProperties([]);
        setPropertiesTotal(0);
        setPropertiesOffset(0);
        setAreaStats(null);
        setUnfilteredAreaCount(null);
        setRightPaneTab('area');

        if (isPostcode) {
          setLoadingAreaStats(true);
          fetchPostcodeStats(id, undefined, areaStatsUseFilters)
            .then((stats) => {
              if (!isCurrentAreaRequest(requestId)) return;
              setAreaStats(stats);
              refreshUnfilteredAreaCount(selection, stats.count, areaStatsUseFilters, requestId);
            })
            .catch((error) => logNonAbortError('Failed to fetch postcode stats', error))
            .finally(() => {
              if (isCurrentAreaRequest(requestId)) setLoadingAreaStats(false);
            });
        } else {
          setLoadingAreaStats(true);
          fetchHexagonStats(id, resolution, undefined, undefined, areaStatsUseFilters)
            .then((stats) => {
              if (!isCurrentAreaRequest(requestId)) return;
              setAreaStats(stats);
              refreshUnfilteredAreaCount(selection, stats.count, areaStatsUseFilters, requestId);
            })
            .catch((error) => logNonAbortError('Failed to fetch area stats', error))
            .finally(() => {
              if (isCurrentAreaRequest(requestId)) setLoadingAreaStats(false);
            });
        }
      }
    },
    [
      selectedHexagon,
      resolution,
      areaStatsUseFilters,
      fetchHexagonStats,
      fetchPostcodeStats,
      invalidateAreaRequests,
      invalidatePropertyRequests,
      isCurrentAreaRequest,
      refreshUnfilteredAreaCount,
    ]
  );

  const handleHexagonHover = useCallback((h3: string | null) => {
    setHoveredHexagon(h3);
  }, []);

  const handleViewPropertiesFromArea = useCallback(() => {
    if (!selectedHexagon) return;
    trackEvent('View Properties');
    setRightPaneTab('properties');
    setPropertiesOffset(0);
    if (selectedHexagon.type === 'postcode') {
      fetchPostcodeProperties(selectedHexagon.id, 0);
    } else {
      fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, 0);
    }
  }, [selectedHexagon, fetchHexagonProperties, fetchPostcodeProperties]);

  const handlePropertiesTabClick = useCallback(() => {
    setRightPaneTab('properties');
    if (selectedHexagon && properties.length === 0 && !loadingProperties) {
      setPropertiesOffset(0);
      if (selectedHexagon.type === 'postcode') {
        fetchPostcodeProperties(selectedHexagon.id, 0);
      } else {
        fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, 0);
      }
    }
  }, [
    selectedHexagon,
    properties.length,
    loadingProperties,
    fetchHexagonProperties,
    fetchPostcodeProperties,
  ]);

  const handleLoadMoreProperties = useCallback(() => {
    if (!selectedHexagon) return;
    if (selectedHexagon.type === 'postcode') {
      fetchPostcodeProperties(selectedHexagon.id, propertiesOffset);
    } else {
      fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, propertiesOffset);
    }
  }, [selectedHexagon, propertiesOffset, fetchHexagonProperties, fetchPostcodeProperties]);

  const handleCloseSelection = useCallback(() => {
    invalidateAreaRequests();
    invalidatePropertyRequests();
    setSelectedHexagon(null);
    setProperties([]);
    setAreaStats(null);
    setUnfilteredAreaCount(null);
    setSelectedPostcodeGeometry(null);
  }, [invalidateAreaRequests, invalidatePropertyRequests]);

  // Keep the selected area aligned with the active map view as zoom changes.
  useEffect(() => {
    if (!selectedHexagon) return;
    const selection = selectedHexagon;
    const shouldSync =
      (usePostcodeView &&
        selection.type === 'hexagon' &&
        !selection.lockedResolution &&
        (selection.anchorPostcode != null || areaStats?.central_postcode != null)) ||
      (!usePostcodeView && selection.type === 'postcode' && !selection.lockedResolution) ||
      (!usePostcodeView &&
        selection.type === 'hexagon' &&
        !selection.lockedResolution &&
        selection.resolution !== resolution);
    if (!shouldSync) return;

    const zoomingIntoHexagon =
      !usePostcodeView &&
      selection.type === 'hexagon' &&
      !selection.lockedResolution &&
      selection.resolution < resolution;
    const overlappingHexagon = zoomingIntoHexagon
      ? findOverlappingSelectableHexagon(selection.id, hexagonData, resolution)
      : null;
    if (zoomingIntoHexagon && !overlappingHexagon) return;

    const requestId = invalidateAreaRequests();
    invalidatePropertyRequests();
    let cancelled = false;
    const controller = new AbortController();

    const refreshProperties = (selection: SelectedHexagon) => {
      if (rightPaneTab !== 'properties') return;
      if (selection.type === 'postcode') {
        fetchPostcodeProperties(selection.id, 0);
      } else {
        fetchHexagonProperties(selection.id, selection.resolution, 0);
      }
    };

    async function syncSelection() {
      let nextSelection: SelectedHexagon | null = null;
      let nextGeometry: PostcodeGeometry | null = null;
      let nextStats: HexagonStatsResponse | null = null;

      if (usePostcodeView && selection.type === 'hexagon' && !selection.lockedResolution) {
        // Prefer the anchored postcode (the one the user originally tracked) so
        // a zoom-out/zoom-in round-trip restores it exactly, rather than the
        // hexagon's current central_postcode, which may be a neighbour.
        const anchor = selection.anchorPostcode;
        const postcode = anchor?.id ?? areaStats?.central_postcode;
        if (!postcode) return;
        let geometry = anchor?.geometry ?? null;
        if (!geometry) {
          geometry = (await fetchPostcodeLookup(postcode, controller.signal)).geometry;
        }
        nextSelection = {
          id: postcode,
          type: 'postcode',
          resolution,
          anchorPostcode: { id: postcode, geometry },
        };
        nextGeometry = geometry;
        nextStats = await fetchPostcodeStats(postcode, controller.signal, areaStatsUseFilters);
      } else if (!usePostcodeView && selection.type === 'postcode') {
        const lookup = await fetchPostcodeLookup(selection.id, controller.signal);
        const nextId = latLngToCell(lookup.latitude, lookup.longitude, resolution);
        const anchor = selection.anchorPostcode ?? { id: selection.id, geometry: lookup.geometry };
        nextSelection = { id: nextId, type: 'hexagon', resolution, anchorPostcode: anchor };
        nextStats = await fetchHexagonStats(
          nextId,
          resolution,
          controller.signal,
          undefined,
          areaStatsUseFilters
        );
      } else if (
        !usePostcodeView &&
        selection.type === 'hexagon' &&
        !selection.lockedResolution &&
        selection.resolution !== resolution
      ) {
        const nextId =
          resolution < selection.resolution
            ? cellToParent(selection.id, resolution)
            : overlappingHexagon?.h3;
        if (!nextId) return;
        nextSelection = {
          id: nextId,
          type: 'hexagon',
          resolution,
          anchorPostcode: selection.anchorPostcode,
        };
        nextStats = await fetchHexagonStats(
          nextId,
          resolution,
          controller.signal,
          undefined,
          areaStatsUseFilters
        );
      } else {
        return;
      }

      if (cancelled || !isCurrentAreaRequest(requestId) || !nextSelection || !nextStats) return;
      setSelectedHexagon(nextSelection);
      setSelectedPostcodeGeometry(nextGeometry);
      setAreaStats(nextStats);
      refreshUnfilteredAreaCount(
        nextSelection,
        nextStats.count,
        areaStatsUseFilters,
        requestId,
        controller.signal
      );
      refreshProperties(nextSelection);
    }

    setProperties([]);
    setPropertiesTotal(0);
    setPropertiesOffset(0);
    setUnfilteredAreaCount(null);
    setLoadingAreaStats(true);

    syncSelection()
      .catch((error) => {
        if (!cancelled) logNonAbortError('Failed to sync selected area with map view', error);
      })
      .finally(() => {
        if (!cancelled && isCurrentAreaRequest(requestId)) setLoadingAreaStats(false);
      });

    return () => {
      cancelled = true;
      controller.abort();
    };
  }, [
    selectedHexagon,
    hexagonData,
    resolution,
    usePostcodeView,
    areaStatsUseFilters,
    areaStats?.central_postcode,
    fetchHexagonStats,
    fetchPostcodeStats,
    fetchPostcodeLookup,
    fetchHexagonProperties,
    fetchPostcodeProperties,
    invalidateAreaRequests,
    invalidatePropertyRequests,
    isCurrentAreaRequest,
    refreshUnfilteredAreaCount,
    rightPaneTab,
  ]);

  // Re-fetch stats when the selected stats basis or requested field projection changes.
  const prevAreaStatsQueryRef = useRef({
    dataKey: areaStatsDataKey,
    queryKey: areaStatsQueryKey,
  });

  useEffect(() => {
    const previousQuery = prevAreaStatsQueryRef.current;
    if (previousQuery.queryKey === areaStatsQueryKey) return;
    prevAreaStatsQueryRef.current = {
      dataKey: areaStatsDataKey,
      queryKey: areaStatsQueryKey,
    };

    if (!selectedHexagon) return;
    const fieldProjectionOnlyChanged = previousQuery.dataKey === areaStatsDataKey;

    if (!fieldProjectionOnlyChanged) {
      // Clear stale properties
      setProperties([]);
      setPropertiesTotal(0);
      setPropertiesOffset(0);
      invalidatePropertyRequests();
      setAreaStats(null);
      setUnfilteredAreaCount(null);
    }

    setLoadingAreaStats(true);
    const requestId = invalidateAreaRequests();

    const fetchStats =
      selectedHexagon.type === 'postcode'
        ? fetchPostcodeStats(selectedHexagon.id, undefined, areaStatsUseFilters)
        : fetchHexagonStats(
            selectedHexagon.id,
            selectedHexagon.resolution,
            undefined,
            undefined,
            areaStatsUseFilters
          );

    fetchStats
      .then((stats) => {
        if (!isCurrentAreaRequest(requestId)) return;
        setAreaStats(stats);
        refreshUnfilteredAreaCount(selectedHexagon, stats.count, areaStatsUseFilters, requestId);
        // Re-fetch properties if the properties tab is active and the selected basis has matches.
        if (!fieldProjectionOnlyChanged && rightPaneTab === 'properties' && stats.count > 0) {
          if (selectedHexagon.type === 'postcode') {
            fetchPostcodeProperties(selectedHexagon.id, 0);
          } else {
            fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, 0);
          }
        }
      })
      .catch((error) => {
        if (!isCurrentAreaRequest(requestId)) return;
        logNonAbortError('Failed to refresh stats', error);
      })
      .finally(() => {
        if (isCurrentAreaRequest(requestId)) setLoadingAreaStats(false);
      });
  }, [
    areaStatsDataKey,
    areaStatsQueryKey,
    selectedHexagon,
    fetchHexagonStats,
    fetchPostcodeStats,
    areaStatsUseFilters,
    rightPaneTab,
    fetchHexagonProperties,
    fetchPostcodeProperties,
    invalidateAreaRequests,
    invalidatePropertyRequests,
    isCurrentAreaRequest,
    refreshUnfilteredAreaCount,
  ]);

  const handleLocationSearch = useCallback(
    (
      postcode: string,
      geometry: PostcodeGeometry,
      _lat?: number,
      _lng?: number,
      openProperties = false,
      focusAddress?: string
    ) => {
      const requestId = invalidateAreaRequests();
      invalidatePropertyRequests();
      const selection = {
        id: postcode,
        type: 'postcode' as const,
        resolution,
        lockedResolution: true,
      };
      trackEvent(openProperties ? 'Address Search' : 'Postcode Search');
      setSelectedHexagon(selection);
      setSelectedPostcodeGeometry(geometry);
      setProperties([]);
      setPropertiesTotal(0);
      setPropertiesOffset(0);
      setAreaStats(null);
      setUnfilteredAreaCount(null);
      setRightPaneTab(openProperties ? 'properties' : 'area');
      setLoadingAreaStats(true);

      fetchPostcodeStats(postcode, undefined, areaStatsUseFilters)
        .then((stats) => {
          if (!isCurrentAreaRequest(requestId)) return;
          setAreaStats(stats);
          refreshUnfilteredAreaCount(selection, stats.count, areaStatsUseFilters, requestId);
          if (openProperties && stats.count > 0) {
            fetchPostcodeProperties(postcode, 0, focusAddress);
          }
        })
        .catch((error) => logNonAbortError('Failed to fetch postcode stats', error))
        .finally(() => {
          if (isCurrentAreaRequest(requestId)) setLoadingAreaStats(false);
        });
    },
    [
      resolution,
      areaStatsUseFilters,
      fetchPostcodeStats,
      fetchPostcodeProperties,
      invalidateAreaRequests,
      invalidatePropertyRequests,
      isCurrentAreaRequest,
      refreshUnfilteredAreaCount,
    ]
  );

  const handleCurrentLocationSearch = useCallback(
    (lat: number, lng: number) => {
      const requestId = invalidateAreaRequests();
      invalidatePropertyRequests();
      const h3 = latLngToCell(lat, lng, SMALLEST_VISIBLE_HEXAGON_RESOLUTION);
      const selection = {
        id: h3,
        type: 'hexagon' as const,
        resolution: SMALLEST_VISIBLE_HEXAGON_RESOLUTION,
        lockedResolution: true,
      };

      trackEvent('Current Location Search');
      setSelectedHexagon(selection);
      setSelectedPostcodeGeometry(null);
      setProperties([]);
      setPropertiesTotal(0);
      setPropertiesOffset(0);
      setAreaStats(null);
      setUnfilteredAreaCount(null);
      setRightPaneTab('area');
      setLoadingAreaStats(true);

      fetchHexagonStats(
        h3,
        SMALLEST_VISIBLE_HEXAGON_RESOLUTION,
        undefined,
        undefined,
        areaStatsUseFilters
      )
        .then((stats) => {
          if (!isCurrentAreaRequest(requestId)) return;
          setAreaStats(stats);
          refreshUnfilteredAreaCount(selection, stats.count, areaStatsUseFilters, requestId);
        })
        .catch((error) => logNonAbortError('Failed to fetch current location hex stats', error))
        .finally(() => {
          if (isCurrentAreaRequest(requestId)) setLoadingAreaStats(false);
        });
    },
    [
      areaStatsUseFilters,
      fetchHexagonStats,
      invalidateAreaRequests,
      invalidatePropertyRequests,
      isCurrentAreaRequest,
      refreshUnfilteredAreaCount,
    ]
  );

  return {
    selectedHexagon,
    properties,
    propertiesTotal,
    loadingProperties,
    areaStats,
    loadingAreaStats,
    unfilteredAreaCount,
    areaStatsUseFilters,
    setAreaStatsUseFilters,
    hoveredHexagon,
    rightPaneTab,
    setRightPaneTab,
    handleHexagonClick,
    handleHexagonHover,
    handleViewPropertiesFromArea,
    handlePropertiesTabClick,
    handleLoadMoreProperties,
    handleCloseSelection,
    selectedPostcodeGeometry,
    handleLocationSearch,
    handleCurrentLocationSearch,
  };
}
