import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import type { MapFlyToOptions, PostcodeGeometry } from '../../types';
import { authHeaders, isAbortError } from '../../lib/api';
import { DEV_LOCATION, POSTCODE_SEARCH_ZOOM } from '../../lib/consts';
import { useIsMobile } from '../../hooks/useIsMobile';
import {
  useLocationSearch,
  type SearchResult,
  type ViewportCenter,
} from '../../hooks/useLocationSearch';
import { PlaceSearchInput } from '../ui/PlaceSearchInput';
import { LocateIcon } from '../ui/icons/LocateIcon';
import { SearchIcon } from '../ui/icons/SearchIcon';

declare const __DEV__: boolean;

export interface SearchedLocation {
  postcode: string;
  geometry: PostcodeGeometry;
  latitude: number;
  longitude: number;
  zoom: number;
  markerLatitude?: number;
  markerLongitude?: number;
  openProperties?: boolean;
  focusAddress?: string;
}

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

const ZOOM_FOR_TYPE: Record<string, number> = {
  city: 10,
  borough: 12,
  outcode: 14,
  town: 13,
  suburb: 14,
  quarter: 14,
  neighbourhood: 14,
  village: 14,
  station: 15,
  island: 12,
  locality: 14,
  hamlet: 15,
  isolated_dwelling: 16,
  street: 16,
  university: 15,
  park: 15,
  attraction: 16,
  hospital: 16,
  retail: 15,
};

export default function LocationSearch({
  onFlyTo,
  onLocationSearched,
  onCurrentLocationFound,
  onMouseEnter,
  getViewportCenter,
  className = '',
  inputClassName,
}: {
  onFlyTo: (lat: number, lng: number, zoom: number, options?: MapFlyToOptions) => void;
  onLocationSearched?: (postcode: SearchedLocation | null) => void;
  onCurrentLocationFound?: (lat: number, lng: number) => void;
  onMouseEnter?: () => void;
  /** Returns the current map centre so search ranking can bias toward the visible area. */
  getViewportCenter?: () => ViewportCenter | null;
  className?: string;
  inputClassName?: string;
}) {
  const { t } = useTranslation();
  const search = useLocationSearch(undefined, getViewportCenter);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [expanded, setExpanded] = useState(false);
  const isMobile = useIsMobile();
  const containerRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const lookupAbortRef = useRef<AbortController | null>(null);
  const lookupRequestIdRef = useRef(0);

  const cancelLookup = useCallback((updateLoading = true) => {
    lookupRequestIdRef.current += 1;
    lookupAbortRef.current?.abort();
    lookupAbortRef.current = null;
    if (updateLoading) setLoading(false);
  }, []);

  const beginLookup = useCallback(() => {
    lookupAbortRef.current?.abort();
    const controller = new AbortController();
    lookupAbortRef.current = controller;
    lookupRequestIdRef.current += 1;
    setError(null);
    setLoading(true);
    search.close();
    return { controller, requestId: lookupRequestIdRef.current };
  }, [search]);

  const isCurrentLookup = useCallback((requestId: number, controller: AbortController) => {
    return (
      lookupRequestIdRef.current === requestId &&
      lookupAbortRef.current === controller &&
      !controller.signal.aborted
    );
  }, []);

  // Close on outside click
  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
        search.close();
        if (isMobile) setExpanded(false);
      }
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [isMobile, search]);

  // Focus input when expanding on mobile
  useEffect(() => {
    if (isMobile && expanded) {
      inputRef.current?.focus();
    }
  }, [isMobile, expanded]);

  useEffect(() => {
    return () => cancelLookup(false);
  }, [cancelLookup]);

  const selectResult = useCallback(
    async (result: SearchResult) => {
      const { controller, requestId } = beginLookup();

      if (result.type === 'place') {
        const zoom = ZOOM_FOR_TYPE[result.place_type] ?? 14;
        const flyZoom = result.place_type === 'outcode' ? POSTCODE_SEARCH_ZOOM : zoom;
        // Move the camera straight away using the coordinates already in the
        // search result. The nearest-postcode lookup below only feeds the side
        // panel, so gating the fly on it (and on isCurrentLookup) made the jump
        // intermittent whenever that request was slow, failed, or was superseded
        // by another keystroke/selection.
        if (!isMobile) onFlyTo(result.lat, result.lon, flyZoom);
        try {
          const params = new URLSearchParams({
            lat: String(result.lat),
            lng: String(result.lon),
            log: 'false',
          });
          const res = await fetch(
            `/api/nearest-postcode?${params}`,
            authHeaders({ signal: controller.signal })
          );
          if (!isCurrentLookup(requestId, controller)) return;
          if (!res.ok) {
            setError(t('locationSearch.lookupFailed'));
            return;
          }
          const json: PostcodeLookupResponse = await res.json();
          if (!isCurrentLookup(requestId, controller)) return;
          onLocationSearched?.({
            postcode: json.postcode,
            geometry: json.geometry,
            latitude: json.latitude,
            longitude: json.longitude,
            zoom: flyZoom,
            markerLatitude: result.lat,
            markerLongitude: result.lon,
          });
          search.saveRecentSearch(result);
          search.clear();
          if (isMobile) setExpanded(false);
        } catch (error) {
          if (!isCurrentLookup(requestId, controller) || isAbortError(error)) return;
          setError(t('locationSearch.lookupFailed'));
        } finally {
          if (isCurrentLookup(requestId, controller)) {
            lookupAbortRef.current = null;
            setLoading(false);
          }
        }
        return;
      }

      if (result.type === 'address') {
        // Fly from the result's own coordinates immediately; the postcode fetch
        // below only resolves the geometry for the side panel. See the note in
        // the place branch above.
        if (!isMobile) onFlyTo(result.lat, result.lon, 17);
        try {
          const res = await fetch(
            `/api/postcode/${encodeURIComponent(result.postcode)}`,
            authHeaders({ signal: controller.signal })
          );
          if (!isCurrentLookup(requestId, controller)) return;
          if (!res.ok) {
            setError(t('locationSearch.postcodeNotFound'));
            return;
          }
          const json: PostcodeLookupResponse = await res.json();
          if (!isCurrentLookup(requestId, controller)) return;
          onLocationSearched?.({
            postcode: json.postcode,
            geometry: json.geometry,
            latitude: result.lat,
            longitude: result.lon,
            zoom: 17,
            markerLatitude: result.lat,
            markerLongitude: result.lon,
            openProperties: true,
            focusAddress: result.address,
          });
          search.saveRecentSearch(result);
          search.clear();
          if (isMobile) setExpanded(false);
        } catch (error) {
          if (!isCurrentLookup(requestId, controller) || isAbortError(error)) return;
          setError(t('locationSearch.lookupFailed'));
        } finally {
          if (isCurrentLookup(requestId, controller)) {
            lookupAbortRef.current = null;
            setLoading(false);
          }
        }
        return;
      }

      // Postcode — fetch geometry. Unlike place/address results, a postcode
      // result carries no coordinates, so the camera move genuinely depends on
      // this response and stays gated by isCurrentLookup.
      try {
        const res = await fetch(
          `/api/postcode/${encodeURIComponent(result.label)}`,
          authHeaders({ signal: controller.signal })
        );
        if (!isCurrentLookup(requestId, controller)) return;
        if (!res.ok) {
          setError(t('locationSearch.postcodeNotFound'));
          return;
        }
        const json: PostcodeLookupResponse = await res.json();
        if (!isCurrentLookup(requestId, controller)) return;
        if (!isMobile) onFlyTo(json.latitude, json.longitude, POSTCODE_SEARCH_ZOOM);
        onLocationSearched?.({
          postcode: json.postcode,
          geometry: json.geometry,
          latitude: json.latitude,
          longitude: json.longitude,
          zoom: POSTCODE_SEARCH_ZOOM,
        });
        search.saveRecentSearch(result);
        search.clear();
        if (isMobile) setExpanded(false);
      } catch (error) {
        if (!isCurrentLookup(requestId, controller) || isAbortError(error)) return;
        setError(t('locationSearch.lookupFailed'));
      } finally {
        if (isCurrentLookup(requestId, controller)) {
          lookupAbortRef.current = null;
          setLoading(false);
        }
      }
    },
    [beginLookup, isCurrentLookup, onFlyTo, onLocationSearched, isMobile, search, t]
  );

  const [locating, setLocating] = useState(false);

  const locateUser = useCallback(async () => {
    if (!__DEV__ && !navigator.geolocation) {
      setError(t('locationSearch.geolocationUnsupported'));
      return;
    }
    setError(null);
    cancelLookup();
    setLocating(true);
    search.close();
    try {
      const { latitude, longitude } = __DEV__
        ? DEV_LOCATION
        : await new Promise<GeolocationCoordinates>((resolve, reject) => {
            if (!navigator.geolocation) {
              reject(new Error('Geolocation unsupported'));
              return;
            }
            navigator.geolocation.getCurrentPosition(
              (position) => resolve(position.coords),
              reject,
              {
                enableHighAccuracy: true,
                timeout: 10000,
              }
            );
          });
      if (onCurrentLocationFound) {
        onCurrentLocationFound(latitude, longitude);
      } else {
        onFlyTo(latitude, longitude, 17);
      }
      search.clear();
      if (isMobile) setExpanded(false);
    } catch {
      setError(t('locationSearch.geolocationFailed'));
    } finally {
      setLocating(false);
    }
  }, [cancelLookup, onFlyTo, onCurrentLocationFound, isMobile, search, t]);

  // Mobile collapsed state: search icon + locate button
  if (isMobile && !expanded) {
    return (
      <div className="flex gap-2 pointer-events-auto">
        <button
          type="button"
          onClick={() => setExpanded(true)}
          className="p-2 bg-white dark:bg-warm-800 rounded shadow-lg"
          aria-label={t('locationSearch.searchLabel')}
        >
          <SearchIcon className="w-5 h-5 text-warm-600 dark:text-warm-300" />
        </button>
        <button
          type="button"
          onClick={locateUser}
          disabled={locating}
          className="p-2 bg-white dark:bg-warm-800 rounded shadow-lg text-warm-600 dark:text-warm-300 hover:text-teal-600 dark:hover:text-teal-400 disabled:opacity-50"
          aria-label={t('locationSearch.locateMe')}
        >
          <LocateIcon className={`w-5 h-5 ${locating ? 'animate-pulse' : ''}`} />
        </button>
      </div>
    );
  }

  return (
    <div
      ref={containerRef}
      data-tutorial="search"
      className={`flex flex-col pointer-events-auto ${className}`}
      onMouseEnter={onMouseEnter}
    >
      <div className="flex items-center shadow-lg rounded bg-white dark:bg-warm-800">
        <SearchIcon className="w-4 h-4 text-warm-400 dark:text-warm-500 ml-3 shrink-0" />
        <PlaceSearchInput
          search={search}
          onSelect={selectResult}
          loading={loading}
          placeholder={t('locationSearch.placeholder')}
          ariaLabel={t('locationSearch.searchLabel')}
          name="location-search"
          size="sm"
          inputClassName={
            inputClassName ??
            'px-2 py-2 text-sm w-56 border-none outline-none bg-transparent text-warm-700 dark:text-warm-200 placeholder-warm-400 dark:placeholder-warm-500'
          }
          inputRef={inputRef}
          onInputChange={() => {
            setError(null);
            cancelLookup();
          }}
        />
        <button
          type="button"
          onClick={locateUser}
          disabled={locating}
          className="p-2 mr-0.5 rounded hover:bg-warm-100 dark:hover:bg-warm-700 text-warm-400 dark:text-warm-500 hover:text-teal-600 dark:hover:text-teal-400 disabled:opacity-50"
          aria-label={t('locationSearch.locateMe')}
          title={t('locationSearch.locateMe')}
        >
          <LocateIcon className={`w-4 h-4 ${locating ? 'animate-pulse' : ''}`} />
        </button>
      </div>

      {error && (
        <span className="text-xs text-red-600 dark:text-red-300 bg-white/90 dark:bg-warm-800/90 rounded px-2 py-0.5 shadow mt-1">
          {error}
        </span>
      )}
    </div>
  );
}
