"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { useDebouncedValue } from "@/hooks/useDebouncedValue";
import type { ProductFilters, ProductSort } from "@/types/search";
import type { SearchApiError, SearchApiResponse, SearchMetaResult } from "@/types/result";

/**
 * Client-side driver for the hybrid search API.
 *
 * Owns the request lifecycle for the products page: debouncing, out-of-order
 * response rejection, and filter/sort state. Keeping it out of the component
 * means the UI only has to render whatever this returns.
 */

export const DEBOUNCE_MS = 300;

export interface UseProductSearchOptions {
  initialQuery?: string;
  initialFilters?: ProductFilters;
  initialSort?: ProductSort;
  limit?: number;
}

export interface UseProductSearchResult {
  /** The value in the input, updated immediately. */
  input: string;
  setInput: (value: string) => void;
  clear: () => void;
  filters: ProductFilters;
  setFilters: (updater: (current: ProductFilters) => ProductFilters) => void;
  resetFilters: () => void;
  sort: ProductSort;
  setSort: (sort: ProductSort) => void;
  data: SearchApiResponse | null;
  meta: SearchMetaResult | null;
  isLoading: boolean;
  /** True while fetching, but results from the previous query are still shown. */
  isRefreshing: boolean;
  error: string | null;
  retry: () => void;
}

export function useProductSearch({
  initialQuery = "",
  initialFilters = {},
  initialSort = "relevance",
  limit = 24,
}: UseProductSearchOptions = {}): UseProductSearchResult {
  const [input, setInput] = useState(initialQuery);
  const [filters, setFiltersState] = useState<ProductFilters>(initialFilters);
  const [sort, setSort] = useState<ProductSort>(initialSort);
  const [data, setData] = useState<SearchApiResponse | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [settledKey, setSettledKey] = useState<string | null>(null);
  const [nonce, setNonce] = useState(0);

  const debouncedQuery = useDebouncedValue(input, DEBOUNCE_MS);

  // Only the latest request may write to state, so a slow early response cannot
  // overwrite the results of a later one.
  const latestRequest = useRef(0);
  const abortController = useRef<AbortController | null>(null);

  const params = useMemo(() => {
    const search = new URLSearchParams();
    if (debouncedQuery.trim()) search.set("q", debouncedQuery.trim());
    if (filters.category) search.set("category", filters.category);
    if (filters.brand) search.set("brand", filters.brand);
    if (filters.minPrice !== undefined) search.set("minPrice", String(filters.minPrice));
    if (filters.maxPrice !== undefined) search.set("maxPrice", String(filters.maxPrice));
    if (filters.minRating !== undefined) search.set("minRating", String(filters.minRating));
    if (filters.inStockOnly) search.set("inStock", "true");
    if (sort !== "relevance") search.set("sort", sort);
    search.set("limit", String(limit));
    return search;
  }, [debouncedQuery, filters, sort, limit]);

  /** The same state minus the page size, which does not belong in a shared URL. */
  const shareableParams = useMemo(() => {
    const shareable = new URLSearchParams(params);
    shareable.delete("limit");
    return shareable;
  }, [params]);

  const requestKey = params.toString();

  // Derived rather than stored: the request for `requestKey` is in flight until
  // it reports back as `settledKey`.
  const isLoading = settledKey !== `${requestKey}|${nonce}`;

  useEffect(() => {
    const request = ++latestRequest.current;
    abortController.current?.abort();
    const controller = new AbortController();
    abortController.current = controller;

    const settle = () => {
      if (request === latestRequest.current) setSettledKey(`${requestKey}|${nonce}`);
    };

    void (async () => {
      try {
        const response = await fetch(`/api/search?${requestKey}`, {
          signal: controller.signal,
          headers: { accept: "application/json" },
        });

        const payload = (await response.json()) as SearchApiResponse | SearchApiError;

        if (request !== latestRequest.current) return;

        if (!response.ok || "error" in payload) {
          setError(
            "error" in payload
              ? Object.values(payload.error.details ?? {})
                  .flat()
                  .join(" ") || payload.error.message
              : `Search failed with status ${response.status}`,
          );
          setData(null);
          return;
        }

        setData(payload);
        setError(null);
      } catch (cause) {
        if (request !== latestRequest.current) return;
        if (cause instanceof DOMException && cause.name === "AbortError") return;
        setError("Could not reach the search service. Check your connection and try again.");
        setData(null);
      } finally {
        settle();
      }
    })();

    return () => controller.abort();
  }, [requestKey, nonce]);

  // Keep the address bar in step with the current search without triggering a
  // server navigation: the results are client state, not a server round trip.
  useEffect(() => {
    const queryString = shareableParams.toString();
    const nextUrl = queryString ? `/products?${queryString}` : "/products";
    if (window.location.pathname + window.location.search !== nextUrl) {
      window.history.replaceState(null, "", nextUrl);
    }
  }, [shareableParams]);

  const setFilters = useCallback(
    (updater: (current: ProductFilters) => ProductFilters) => {
      setFiltersState((current) => {
        const next = updater(current);
        // Drop keys that were cleared so they do not linger in the query string.
        return Object.fromEntries(
          Object.entries(next).filter(([, value]) => value !== undefined && value !== false && value !== ""),
        ) as ProductFilters;
      });
    },
    [],
  );

  const resetFilters = useCallback(() => setFiltersState({}), []);

  const clear = useCallback(() => setInput(""), []);

  const retry = useCallback(() => setNonce((value) => value + 1), []);

  return {
    input,
    setInput,
    clear,
    filters,
    setFilters,
    resetFilters,
    sort,
    setSort,
    data,
    meta: data?.meta ?? null,
    isLoading,
    isRefreshing: isLoading && data !== null,
    error,
    retry,
  };
}
