import { useState, useCallback, useRef, useEffect } from 'react';
import type { AddressResult, PlaceResult } from '../types';
import { authHeaders, logNonAbortError } from '../lib/api';

const RECENT_SEARCHES_STORAGE_KEY = 'perfect-postcode.locationSearch.recent';
const RECENT_SEARCH_LIMIT = 10;

/** Matches a full UK postcode with complete inward code (e.g. "E14 2DG", "SW1A1AA").
 *  Outcodes like "E14" or "SW1A" intentionally do NOT match — they go through /api/places instead. */
const FULL_POSTCODE_RE = /^[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}$/i;

function looksLikePostcode(s: string) {
  return FULL_POSTCODE_RE.test(s.trim());
}

/** Normalize a UK postcode: uppercase, strip spaces, insert canonical space before inward code. */
function normalizePostcode(s: string): string {
  const stripped = s.replace(/\s+/g, '').toUpperCase();
  if (stripped.length >= 5) {
    return stripped.slice(0, -3) + ' ' + stripped.slice(-3);
  }
  return stripped;
}

function searchableTextForResult(result: SearchResult): string {
  if (result.type === 'postcode') {
    return result.label;
  }
  if (result.type === 'address') {
    return `${result.address} ${result.postcode}`;
  }
  return `${result.name} ${result.city ?? ''}`;
}

function searchTokens(value: string): string[] {
  return value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
}

function compactSearchText(value: string): string {
  return searchTokens(value).join('');
}

function resultMatchesQuery(result: SearchResult, query: string): boolean {
  const queryTokens = searchTokens(query);
  if (queryTokens.length === 0) {
    return false;
  }

  const searchable = searchableTextForResult(result);
  const resultText = searchTokens(searchable).join(' ');
  const resultCompact = compactSearchText(searchable);
  const queryCompact = queryTokens.join('');

  return (
    queryTokens.every((token) => resultText.includes(token)) ||
    (queryCompact.length >= 2 && resultCompact.includes(queryCompact))
  );
}

function filterResultsForQuery(results: SearchResult[], query: string): SearchResult[] {
  return results.filter((result) => resultMatchesQuery(result, query)).slice(0, 20);
}

export type SearchResult =
  | { type: 'postcode'; label: string }
  | {
      type: 'address';
      address: string;
      postcode: string;
      lat: number;
      lon: number;
    }
  | {
      type: 'place';
      name: string;
      slug: string;
      place_type: string;
      lat: number;
      lon: number;
      city?: string;
    };

function isFiniteNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function isSearchResult(value: unknown): value is SearchResult {
  if (!value || typeof value !== 'object') return false;

  const result = value as Record<string, unknown>;
  if (result.type === 'postcode') {
    return typeof result.label === 'string';
  }

  if (result.type === 'address') {
    return (
      typeof result.address === 'string' &&
      typeof result.postcode === 'string' &&
      isFiniteNumber(result.lat) &&
      isFiniteNumber(result.lon)
    );
  }

  if (result.type === 'place') {
    return (
      typeof result.name === 'string' &&
      typeof result.slug === 'string' &&
      typeof result.place_type === 'string' &&
      isFiniteNumber(result.lat) &&
      isFiniteNumber(result.lon) &&
      (result.city === undefined || typeof result.city === 'string')
    );
  }

  return false;
}

function readRecentSearches(): SearchResult[] {
  if (typeof window === 'undefined') return [];

  try {
    const raw = window.localStorage.getItem(RECENT_SEARCHES_STORAGE_KEY);
    if (!raw) return [];
    const parsed: unknown = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed.filter(isSearchResult).slice(0, RECENT_SEARCH_LIMIT);
  } catch {
    return [];
  }
}

function writeRecentSearches(searches: SearchResult[]) {
  if (typeof window === 'undefined') return;

  try {
    window.localStorage.setItem(
      RECENT_SEARCHES_STORAGE_KEY,
      JSON.stringify(searches.slice(0, RECENT_SEARCH_LIMIT))
    );
  } catch {
    // Recent searches are a convenience only; storage failures should not affect search.
  }
}

function searchResultKey(result: SearchResult): string {
  if (result.type === 'postcode') {
    return `postcode:${normalizePostcode(result.label)}`;
  }

  if (result.type === 'address') {
    return `address:${result.postcode.toUpperCase()}:${result.address.toLowerCase()}`;
  }

  return `place:${result.slug}`;
}

/** A category-tagged, scored result from the unified `/api/places` ranking. */
type UnifiedResultDTO =
  | { type: 'postcode'; label: string; score: number }
  | { type: 'address'; address: string; postcode: string; lat: number; lon: number; score: number }
  | {
      type: 'place';
      name: string;
      slug: string;
      place_type: string;
      lat: number;
      lon: number;
      city?: string;
      score: number;
    };

interface PlacesApiResponse {
  places: PlaceResult[];
  postcodes?: string[];
  addresses?: AddressResult[];
  /** Preferred: a single relevance-ordered list across all categories. */
  results?: UnifiedResultDTO[];
}

function isNonNull<T>(value: T | null): value is T {
  return value !== null;
}

function unifiedToSearchResult(result: UnifiedResultDTO): SearchResult | null {
  if (result.type === 'postcode') {
    return { type: 'postcode', label: result.label };
  }
  if (result.type === 'address') {
    return {
      type: 'address',
      address: result.address,
      postcode: result.postcode,
      lat: result.lat,
      lon: result.lon,
    };
  }
  if (result.type === 'place') {
    return {
      type: 'place',
      name: result.name,
      slug: result.slug,
      place_type: result.place_type,
      lat: result.lat,
      lon: result.lon,
      city: result.city === 'City of London' ? 'London' : result.city,
    };
  }
  return null;
}

/** Legacy ordering for servers that predate the unified `results` list: positional buckets,
 *  re-filtered locally. Retained only as a fallback. */
function legacyCombineResults(json: PlacesApiResponse, trimmed: string): SearchResult[] {
  const placeResults = json.places.map((p) => ({
    type: 'place' as const,
    name: p.name,
    slug: p.slug,
    place_type: p.place_type,
    lat: p.lat,
    lon: p.lon,
    city: p.city === 'City of London' ? 'London' : p.city,
  }));
  const outcodeResults = placeResults.filter((result) => result.place_type === 'outcode');
  const otherPlaceResults = placeResults.filter((result) => result.place_type !== 'outcode');
  const postcodeResults: SearchResult[] = (json.postcodes ?? []).map((postcode) => ({
    type: 'postcode' as const,
    label: postcode,
  }));
  const addressResults: SearchResult[] = (json.addresses ?? []).map((address) => ({
    type: 'address' as const,
    address: address.address,
    postcode: address.postcode,
    lat: address.lat,
    lon: address.lon,
  }));
  const containsHouseNumber = /\d/.test(trimmed);
  return filterResultsForQuery(
    containsHouseNumber
      ? [...outcodeResults, ...postcodeResults, ...addressResults, ...otherPlaceResults]
      : [...outcodeResults, ...postcodeResults, ...otherPlaceResults, ...addressResults],
    trimmed
  );
}

export type ViewportCenter = { lat: number; lng: number };

export function useLocationSearch(mode?: string, getViewportCenter?: () => ViewportCenter | null) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<SearchResult[]>([]);
  const [recentSearches, setRecentSearches] = useState<SearchResult[]>(readRecentSearches);
  const [activeIndex, setActiveIndex] = useState(-1);
  const [open, setOpen] = useState(false);
  const [searching, setSearching] = useState(false);
  const abortRef = useRef<AbortController | null>(null);
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const latestQueryRef = useRef('');
  const lastResultsRef = useRef<SearchResult[]>([]);
  // Held in a ref so a non-memoized callback from the parent doesn't churn handleInputChange.
  const getViewportCenterRef = useRef(getViewportCenter);
  getViewportCenterRef.current = getViewportCenter;

  const handleInputChange = useCallback(
    (value: string) => {
      setQuery(value);
      latestQueryRef.current = value;
      setActiveIndex(-1);

      abortRef.current?.abort();
      if (debounceRef.current) clearTimeout(debounceRef.current);

      const trimmed = value.trim();
      if (!trimmed) {
        setSearching(false);
        setResults(recentSearches);
        lastResultsRef.current = [];
        setOpen(recentSearches.length > 0);
        return;
      }

      if (!mode && looksLikePostcode(trimmed)) {
        setSearching(false);
        const postcodeResults: SearchResult[] = [
          { type: 'postcode', label: normalizePostcode(trimmed) },
        ];
        setResults(postcodeResults);
        setOpen(true);
        return;
      }

      if (trimmed.length < 2) {
        setSearching(false);
        setResults([]);
        setOpen(false);
        return;
      }

      const locallyFilteredResults = filterResultsForQuery(lastResultsRef.current, trimmed);
      setResults(locallyFilteredResults);
      setOpen(locallyFilteredResults.length > 0);
      setSearching(true);

      debounceRef.current = setTimeout(async () => {
        const controller = new AbortController();
        abortRef.current = controller;
        try {
          const params = new URLSearchParams({ q: trimmed });
          if (mode) params.set('mode', mode);
          const center = getViewportCenterRef.current?.();
          if (center) {
            params.set('lat', String(center.lat));
            params.set('lng', String(center.lng));
          }
          const res = await fetch(
            `/api/places?${params}`,
            authHeaders({ signal: controller.signal })
          );
          if (!res.ok) {
            if (!controller.signal.aborted && latestQueryRef.current.trim() === trimmed) {
              setResults([]);
              setOpen(true);
            }
            return;
          }
          const json: PlacesApiResponse = await res.json();
          const combinedResults = (
            Array.isArray(json.results)
              ? json.results.map(unifiedToSearchResult).filter(isNonNull)
              : legacyCombineResults(json, trimmed)
          ).slice(0, 20);
          if (controller.signal.aborted || latestQueryRef.current.trim() !== trimmed) {
            return;
          }
          lastResultsRef.current = combinedResults;
          // Trust the server's unified ranking — re-filtering here previously dropped valid
          // alias and partial-postcode matches. The optimistic pre-fetch path still filters.
          setResults(combinedResults);
          setOpen(true);
        } catch (err) {
          logNonAbortError('places search', err);
          if (!controller.signal.aborted && latestQueryRef.current.trim() === trimmed) {
            setResults([]);
            setOpen(true);
          }
        } finally {
          if (!controller.signal.aborted && latestQueryRef.current.trim() === trimmed) {
            setSearching(false);
          }
        }
      }, 200);
    },
    [mode, recentSearches]
  );

  const showEmptySearches = useCallback(() => {
    if (latestQueryRef.current.trim()) {
      setOpen(results.length > 0 || latestQueryRef.current.trim().length >= 2);
      return;
    }

    setResults(recentSearches);
    setActiveIndex(-1);
    setOpen(recentSearches.length > 0);
  }, [recentSearches, results.length]);

  const close = useCallback(() => setOpen(false), []);

  const clear = useCallback(() => {
    setQuery('');
    latestQueryRef.current = '';
    setSearching(false);
    setResults([]);
    lastResultsRef.current = [];
    setOpen(false);
    setActiveIndex(-1);
  }, []);

  const saveRecentSearch = useCallback((result: SearchResult) => {
    setRecentSearches((prev) => {
      const key = searchResultKey(result);
      const next = [result, ...prev.filter((recent) => searchResultKey(recent) !== key)].slice(
        0,
        RECENT_SEARCH_LIMIT
      );
      writeRecentSearches(next);
      return next;
    });
  }, []);

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent, onSelect: (result: SearchResult) => void) => {
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        setActiveIndex((prev) => (prev < results.length - 1 ? prev + 1 : prev));
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        setActiveIndex((prev) => (prev > 0 ? prev - 1 : -1));
      } else if (e.key === 'Enter') {
        e.preventDefault();
        if (activeIndex >= 0 && activeIndex < results.length) {
          onSelect(results[activeIndex]);
        } else if (results.length > 0) {
          onSelect(results[0]);
        } else if (looksLikePostcode(query)) {
          onSelect({ type: 'postcode', label: normalizePostcode(query) });
        }
      } else if (e.key === 'Escape') {
        setOpen(false);
      }
    },
    [results, activeIndex, query]
  );

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      abortRef.current?.abort();
      if (debounceRef.current) clearTimeout(debounceRef.current);
    };
  }, []);

  return {
    query,
    results,
    activeIndex,
    setActiveIndex,
    open,
    searching,
    setOpen,
    handleInputChange,
    handleKeyDown,
    showEmptySearches,
    saveRecentSearch,
    close,
    clear,
  };
}
