import { useCallback, useEffect, useRef, useState } from 'react';
import {
  fetchCatalog,
  fetchHarnesses,
  fetchPersistedSelection,
  persistSelection,
  type SelectionErrorCode,
} from './chat-api';
import type { HarnessCatalog, HarnessSelection, HarnessSummary } from '@/lib/types';

export interface HarnessPersistError {
  code: SelectionErrorCode;
  message: string;
  /** The exact tuple the user requested — preserved so the failed selection
   * stays visible rather than being silently dropped. */
  requested: HarnessSelection;
}

export interface HarnessSelectionValue {
  harnesses: HarnessSummary[];
  catalog: HarnessCatalog | null;
  /** True when the selected harness has no usable catalog (404/error). */
  catalogUnavailable: boolean;
  /** The working (displayed) selection, kept as three distinct ids. Empty
   * strings mean "not chosen yet" — there is deliberately no first-row default. */
  harnessId: string;
  providerId: string;
  modelId: string;
  /** The last tuple confirmed persisted by the server, or null. */
  persistedSelection: HarnessSelection | null;
  /** True when a persisted selection references a model no longer present as an
   * available catalog entry — it stays visibly displayed rather than dropped. */
  isStale: boolean;
  /** True ONLY once a full tuple has been confirmed persisted AND it is a
   * currently-available catalog entry. Send stays disabled otherwise, so a send
   * can never race ahead of successful persistence. */
  canSend: boolean;
  persistError: HarnessPersistError | null;
  selectHarness: (harnessId: string) => void;
  selectProvider: (providerId: string) => void;
  /** Persist the EXACT catalog row's `{providerId, modelId}` — the caller
   * resolves the composite option identity to the real entry and passes both
   * ids, so a bare model id is never combined with ambient provider state. */
  selectModel: (providerId: string, modelId: string) => void;
}

/** A tuple is a currently-usable catalog option only when the catalog holds a
 * matching, available entry — the single gate that keeps a stale/unavailable
 * model from ever counting as sendable. */
function isAvailableInCatalog(
  selection: HarnessSelection | null,
  catalog: HarnessCatalog | null,
): boolean {
  if (selection === null || catalog === null) return false;
  return catalog.models.some(
    (model) =>
      model.providerId === selection.providerId &&
      model.modelId === selection.modelId &&
      model.availability === 'available',
  );
}

function tuplesEqual(a: HarnessSelection | null, b: HarnessSelection | null): boolean {
  if (a === null || b === null) return a === b;
  return a.harnessId === b.harnessId && a.providerId === b.providerId && a.modelId === b.modelId;
}

/**
 * Owns the harness/catalog/selection state for the chat composer: loads the
 * harness list and any persisted tuple on mount, loads a harness's catalog when
 * chosen, and PUT-persists the full `{harnessId, providerId, modelId}` tuple
 * when a model is picked. It never auto-selects a catalog row, keeps a
 * stale/unavailable persisted tuple visible, and only reports `canSend` true
 * once a full tuple has actually persisted as an available catalog entry.
 */
export function useHarnessSelection(): HarnessSelectionValue {
  const [harnesses, setHarnesses] = useState<HarnessSummary[]>([]);
  const [catalog, setCatalog] = useState<HarnessCatalog | null>(null);
  const [catalogUnavailable, setCatalogUnavailable] = useState(false);
  const [harnessId, setHarnessId] = useState('');
  const [providerId, setProviderId] = useState('');
  const [modelId, setModelId] = useState('');
  const [persistedSelection, setPersistedSelection] = useState<HarnessSelection | null>(null);
  const [persistError, setPersistError] = useState<HarnessPersistError | null>(null);

  // Monotonic request ids so a slow in-flight catalog/persist response can never
  // overwrite the result of a newer request the user has since triggered.
  const catalogRequestRef = useRef(0);
  const persistRequestRef = useRef(0);

  const loadCatalog = useCallback(async (id: string): Promise<void> => {
    const requestId = catalogRequestRef.current + 1;
    catalogRequestRef.current = requestId;
    setCatalog(null);
    setCatalogUnavailable(false);
    const result = await fetchCatalog(id);
    if (catalogRequestRef.current !== requestId) return;
    if (result.ok) {
      setCatalog(result.catalog);
      setCatalogUnavailable(false);
    } else {
      setCatalog(null);
      setCatalogUnavailable(true);
    }
  }, []);

  useEffect(() => {
    let active = true;
    void (async (): Promise<void> => {
      const [list, persisted] = await Promise.all([fetchHarnesses(), fetchPersistedSelection()]);
      if (!active) return;
      setHarnesses(list);
      if (persisted !== null) {
        // Adopt the persisted tuple as the displayed selection and load its
        // catalog. If the model has since been retired, it still shows (stale).
        setHarnessId(persisted.harnessId);
        setProviderId(persisted.providerId);
        setModelId(persisted.modelId);
        setPersistedSelection(persisted);
        await loadCatalog(persisted.harnessId);
      }
      // No persisted selection → nothing is auto-selected; the user must choose.
    })();
    return () => {
      active = false;
    };
  }, [loadCatalog]);

  const selectHarness = useCallback(
    (id: string): void => {
      setHarnessId(id);
      // Changing harness invalidates the provider/model draft — never carry a
      // model across harnesses.
      setProviderId('');
      setModelId('');
      setPersistError(null);
      void loadCatalog(id);
    },
    [loadCatalog],
  );

  const selectProvider = useCallback((id: string): void => {
    setProviderId(id);
    // A new provider invalidates the chosen model — no cross-provider carryover.
    setModelId('');
    setPersistError(null);
  }, []);

  const selectModel = useCallback(
    (selectedProviderId: string, selectedModelId: string): void => {
      // Bind the model to the EXACT catalog row's provider — never to ambient
      // provider state — so two providers exposing the same modelId can never
      // collide or mis-resolve. Keep the displayed provider consistent with the
      // resolved row.
      setProviderId(selectedProviderId);
      setModelId(selectedModelId);
      setPersistError(null);
      const requested: HarnessSelection = {
        harnessId,
        providerId: selectedProviderId,
        modelId: selectedModelId,
      };
      const requestId = persistRequestRef.current + 1;
      persistRequestRef.current = requestId;
      void (async (): Promise<void> => {
        const result = await persistSelection(requested);
        if (persistRequestRef.current !== requestId) return;
        if (result.ok) {
          setPersistedSelection(result.selection);
          setPersistError(null);
        } else {
          // Leave persistedSelection unchanged (send stays disabled) and surface
          // the typed error carrying the exact requested tuple.
          setPersistError({
            code: result.code,
            message: result.message,
            requested: result.requested,
          });
        }
      })();
    },
    [harnessId],
  );

  const draft: HarnessSelection = { harnessId, providerId, modelId };
  const isStale = persistedSelection !== null && !isAvailableInCatalog(persistedSelection, catalog);
  const canSend =
    persistedSelection !== null &&
    !catalogUnavailable &&
    tuplesEqual(draft, persistedSelection) &&
    isAvailableInCatalog(persistedSelection, catalog);

  return {
    harnesses,
    catalog,
    catalogUnavailable,
    harnessId,
    providerId,
    modelId,
    persistedSelection,
    isStale,
    canSend,
    persistError,
    selectHarness,
    selectProvider,
    selectModel,
  };
}
