import type { BrowsableSourceItemView, Meal, SourceView, WeekDay } from "@batch-cooking/shared";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { apiClient } from "../../api/client";
import { RecipeDetailPanel, type RecipeDetailState } from "./RecipeDetailPanel";
import { SourceItemTable } from "./SourceItemTable";
import "./recipes.scss";

/** Debounce for the search field — same idea/value as `RecipesPage`'s own search. */
const SEARCH_DEBOUNCE_MS = 300;

/** One item's identity within a source's browsable catalog — `sourceKey` + `externalId` together, since `externalId` alone is only unique per source. */
export interface SourceItemSelection {
  sourceKey: string;
  externalId: string;
}

type EnabledSourcesState =
  | { status: "loading" }
  | { status: "loaded"; sources: SourceView[] }
  | { status: "error" };

type BrowseState =
  | { status: "loading" }
  | { status: "loaded"; items: BrowsableSourceItemView[]; nextCursor: string | null }
  | { status: "error" };

/**
 * "Sources" tab content of the recipe catalog (`RecipesPage`) — a
 * self-contained master-detail pair of its own (source selector + browsable
 * list on the left, a preview on the right), independent of `RecipeTable`'s
 * own `RecipeTab`-based fetching: it browses a source's *live* catalog
 * (`GET /sources/:sourceKey/browse`), not the saved `Recipe` table.
 *
 * The right-hand preview reuses `RecipeDetailPanel` itself (its
 * `"loaded-draft"` state) rather than a separate component — viewing a
 * not-yet-imported item is meant to look and feel exactly like viewing any
 * other recipe, differing only in which actions are offered (there's an
 * "Importer" button where Modifier/Supprimer would be).
 *
 * Selecting an already-imported item navigates straight to its real
 * recipe (`/recettes/:id`, leaving this tab) — `onSelectImportedRecipe`
 * hands back the id instead of this panel navigating anywhere itself, since
 * what "viewing" an already-imported item means depends on the caller:
 * `RecipesPage` switches its own active tab away from `"sources"` (its
 * `RecipeDetailPanel`/`RecipeTable` only render outside that tab, so
 * without switching first the URL would change but this panel would keep
 * rendering over it) and navigates to the recipe's detail page, while
 * `RecipePickerDialog` instead treats it exactly like picking that recipe
 * from one of the regular tabs — moving to its own confirm-portions step,
 * no navigation at all.
 *
 * `initialSelection`/`onItemSelected` are how `RecipesPage` keeps a
 * not-yet-imported item's preview addressable by URL
 * (`/recettes/sources/:sourceKey/:externalId`) without this panel needing
 * to know anything about routing itself — it reports selection changes
 * upward, and re-previews on mount/prop-change if handed one back.
 * `RecipePickerDialog` leaves both unset: previewing inside that modal has
 * no URL of its own to keep in sync.
 */
export function RecipeSourcesPanel({
  onSelectImportedRecipe,
  planningSlot,
  initialSelection,
  onItemSelected,
}: {
  onSelectImportedRecipe: (recipeId: number) => void;
  /** Forwarded as-is to `RecipeDetailPanel` — see its own doc comment. Only ever set by `RecipePickerDialog`. */
  planningSlot?: { date: string; weekDay: WeekDay; meal: Meal };
  initialSelection?: SourceItemSelection;
  onItemSelected?: (item: SourceItemSelection | null) => void;
}) {
  const { t } = useTranslation();

  const [enabledSources, setEnabledSources] = useState<EnabledSourcesState>({ status: "loading" });
  const [selectedSourceKey, setSelectedSourceKey] = useState<string | null>(
    initialSelection?.sourceKey ?? null,
  );
  const [search, setSearch] = useState("");
  const [debouncedSearch, setDebouncedSearch] = useState("");
  const [browseState, setBrowseState] = useState<BrowseState>({ status: "loading" });
  const [selectedExternalId, setSelectedExternalId] = useState<string | null>(
    initialSelection?.externalId ?? null,
  );
  const [previewState, setPreviewState] = useState<RecipeDetailState>({ status: "empty" });
  // Which item `previewState` actually reflects (or is in flight for) —
  // lets the `initialSelection` effect below tell "the URL just changed to
  // match a selection this component already made itself" (a row click
  // already fetched/is fetching this exact item; `onItemSelected` only
  // round-trips that same pair back in as a new `initialSelection` prop)
  // apart from "the URL changed to point somewhere new" (a deep link, or
  // the browser's back/forward button) — only the latter needs a fetch.
  // Always starts at `null`, even when `initialSelection` is already set on
  // mount — nothing has been fetched yet at that point, that's exactly the
  // "needs a fetch" case the effect below must still run for.
  const [previewedItem, setPreviewedItem] = useState<SourceItemSelection | null>(null);

  // Loaded once — which sources exist, crossed with which the household
  // has enabled (`/parametres/foyer`). Defaults the selector to the first
  // enabled one, if any — but never overrides a source `initialSelection`
  // already picked (the `current ??` below), so a deep link always wins.
  useEffect(() => {
    let cancelled = false;
    Promise.all([apiClient.getSources(), apiClient.getHouseSourceIds()])
      .then(([sources, enabledIds]) => {
        if (cancelled) return;
        const enabled = sources.filter((source) => enabledIds.includes(source.id));
        setEnabledSources({ status: "loaded", sources: enabled });
        setSelectedSourceKey((current) => current ?? enabled[0]?.key ?? null);
      })
      .catch(() => {
        if (!cancelled) setEnabledSources({ status: "error" });
      });
    return () => {
      cancelled = true;
    };
  }, []);

  // Re-previews whenever `initialSelection` itself changes (a fresh deep
  // link, or the browser's back/forward button landing on a different
  // item) — not just once on mount. Deliberately doesn't touch
  // `browseState`/the source dropdown beyond `selectedSourceKey` above: the
  // item list for whichever source this belongs to loads independently
  // (see the effect below), on its own schedule. Keyed on the primitive
  // fields below, not `initialSelection` itself — a fresh object literal
  // from the caller on every render (see `RecipesPage`) would otherwise
  // re-run this on every render too.
  // biome-ignore lint/correctness/useExhaustiveDependencies: see above.
  useEffect(() => {
    if (!initialSelection) return;
    if (
      previewedItem?.sourceKey === initialSelection.sourceKey &&
      previewedItem?.externalId === initialSelection.externalId
    ) {
      return;
    }
    let cancelled = false;
    setPreviewedItem(initialSelection);
    setSelectedSourceKey(initialSelection.sourceKey);
    setSelectedExternalId(initialSelection.externalId);
    setPreviewState({ status: "loading" });
    apiClient
      .previewSourceItem(initialSelection.sourceKey, initialSelection.externalId)
      .then((draft) => {
        if (!cancelled) setPreviewState({ status: "loaded-draft", draft });
      })
      .catch(() => {
        if (!cancelled) setPreviewState({ status: "error" });
      });
    return () => {
      cancelled = true;
    };
  }, [initialSelection?.sourceKey, initialSelection?.externalId]);

  useEffect(() => {
    const timeout = window.setTimeout(() => setDebouncedSearch(search), SEARCH_DEBOUNCE_MS);
    return () => window.clearTimeout(timeout);
  }, [search]);

  // Only manages `browseState` — deliberately doesn't reset the current
  // item selection/preview when `selectedSourceKey` changes, so that the
  // `initialSelection` effect above (which also sets `selectedSourceKey`,
  // to reflect a deep link) isn't immediately undone by this one running
  // straight after it in the same commit. Switching source via the
  // dropdown clears the selection explicitly, in its own `onChange` below,
  // where that reset is actually wanted.
  useEffect(() => {
    if (selectedSourceKey === null) return;
    let cancelled = false;
    setBrowseState({ status: "loading" });

    apiClient
      .browseSource(selectedSourceKey, { query: debouncedSearch.trim() || undefined })
      .then(({ items, nextCursor }) => {
        if (!cancelled) setBrowseState({ status: "loaded", items, nextCursor });
      })
      .catch(() => {
        if (!cancelled) setBrowseState({ status: "error" });
      });

    return () => {
      cancelled = true;
    };
  }, [selectedSourceKey, debouncedSearch]);

  function handleLoadMore() {
    if (selectedSourceKey === null || browseState.status !== "loaded" || !browseState.nextCursor) {
      return;
    }
    const cursor = browseState.nextCursor;
    apiClient
      .browseSource(selectedSourceKey, { query: debouncedSearch.trim() || undefined, cursor })
      .then(({ items, nextCursor }) => {
        setBrowseState((prev) =>
          prev.status === "loaded"
            ? { status: "loaded", items: [...prev.items, ...items], nextCursor }
            : prev,
        );
      })
      .catch(() => setBrowseState({ status: "error" }));
  }

  function handleSelectItem(item: BrowsableSourceItemView) {
    if (item.alreadyImported && item.recipeId !== null) {
      onSelectImportedRecipe(item.recipeId);
      return;
    }
    if (selectedSourceKey === null) return;
    const selection = { sourceKey: selectedSourceKey, externalId: item.externalId };
    setSelectedExternalId(item.externalId);
    setPreviewState({ status: "loading" });
    // Set before `onItemSelected` so the `initialSelection` effect above
    // recognizes the URL change it triggers as reflecting this same fetch,
    // not a fresh one to make (see that effect's own doc comment).
    setPreviewedItem(selection);
    onItemSelected?.(selection);
    apiClient
      .previewSourceItem(selectedSourceKey, item.externalId)
      .then((draft) => setPreviewState({ status: "loaded-draft", draft }))
      .catch(() => setPreviewState({ status: "error" }));
  }

  if (enabledSources.status === "loading") {
    return <p className="recipes-page__status">{t("recipes.loading")}</p>;
  }
  if (enabledSources.status === "error") {
    return (
      <p className="recipes-page__status recipes-page__status--error">{t("common.loadError")}</p>
    );
  }
  if (enabledSources.sources.length === 0) {
    return (
      <p className="recipes-page__status">
        {t("recipes.sources.noneEnabled")}{" "}
        <Link to="/parametres/foyer">{t("recipes.sources.noneEnabledLink")}</Link>
      </p>
    );
  }

  return (
    <>
      <div className="recipes-page__header recipes-page__header--sources">
        {enabledSources.sources.length > 1 && (
          <select
            className="source-sources-select"
            aria-label={t("recipes.sources.sourceLabel")}
            value={selectedSourceKey ?? ""}
            onChange={(e) => {
              setSelectedSourceKey(e.target.value);
              setSelectedExternalId(null);
              setPreviewState({ status: "empty" });
              setPreviewedItem(null);
              onItemSelected?.(null);
            }}
          >
            {enabledSources.sources.map((source) => (
              <option key={source.key} value={source.key}>
                {source.name}
              </option>
            ))}
          </select>
        )}
        <input
          type="search"
          className="recipes-page__search"
          placeholder={t("recipes.sources.searchPlaceholder")}
          value={search}
          onChange={(e) => setSearch(e.target.value)}
        />
      </div>

      <div className="recipes-page__catalog">
        {browseState.status === "loading" && (
          <p className="recipes-page__status">{t("recipes.sources.loading")}</p>
        )}
        {browseState.status === "error" && (
          <p className="recipes-page__status recipes-page__status--error">
            {t("recipes.sources.loadError")}
          </p>
        )}
        {browseState.status === "loaded" && browseState.items.length === 0 && (
          <p className="recipes-page__status">{t("recipes.sources.empty")}</p>
        )}
        {browseState.status === "loaded" && browseState.items.length > 0 && (
          <div className="source-items-column">
            <SourceItemTable
              items={browseState.items}
              selectedExternalId={selectedExternalId}
              onSelect={handleSelectItem}
            />
            {browseState.nextCursor && (
              <button type="button" className="source-items-load-more" onClick={handleLoadMore}>
                {t("recipes.sources.loadMore")}
              </button>
            )}
          </div>
        )}

        <RecipeDetailPanel state={previewState} planningSlot={planningSlot} />
      </div>
    </>
  );
}
