import { ErrorCode, type RecipeSummaryView } from "@batch-cooking/shared";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { ApiError, apiClient } from "../../api/client";
import {
  RecipeDetailPanel,
  type RecipeDetailState,
} from "../../features/recipes/RecipeDetailPanel";
import { RecipeTable } from "../../features/recipes/RecipeTable";
import {
  isSourceTab,
  parseSourceTabValue,
  type RecipesPageTab,
  RecipeTabs,
  sourceTabValue,
} from "../../features/recipes/RecipeTabs";
import {
  RecipeSourcesPanel,
  type SourceItemSelection,
} from "../../features/recipes/sources/RecipeSourcesPanel";
import { useEnabledSources } from "../../features/recipes/sources/useEnabledSources";
import "../../features/recipes/recipes.scss";

/** Debounce for the search field — avoids firing a request on every keystroke, same idea as the household name's autosave (`HouseholdSettingsPage`). */
const SEARCH_DEBOUNCE_MS = 300;

/** Load state for the catalog table (`GET /recipes?tab=...`) — a discriminated union so a stale/impossible combination (e.g. "loading" with data) can't be represented, same pattern as `PlanningPage`'s `PlanningState`. */
type RecipeListState =
  | { status: "loading" }
  | { status: "loaded"; recipes: RecipeSummaryView[] }
  | { status: "error" };

/**
 * Recipe catalog — routed at `/recettes`, `/recettes/:id`, and
 * `/recettes/sources/:sourceKey/:externalId` (the same component either
 * way, see `App.tsx`): a tab bar + table on the left stay mounted at all
 * times, only the right-hand detail panel changes with the URL — a
 * master-detail layout, not a navigation to a separate page (see
 * `RecipeDetailPanel`, which replaces the earlier standalone
 * `RecipeDetailPage`).
 *
 * The `sources` route exists so a not-yet-imported item is just as
 * addressable/deep-linkable as a real recipe's `/recettes/:id` — without
 * it, selecting one inside its source's own tab only changed local
 * component state, with no URL of its own (see `RecipeSourcesPanel`'s
 * `initialSelection`/`onItemSelected`, which this page drives).
 */
export function RecipesPage() {
  const { t } = useTranslation();
  const navigate = useNavigate();
  const { id, sourceKey, externalId } = useParams<{
    id: string;
    sourceKey: string;
    externalId: string;
  }>();
  const selectedId = id !== undefined ? Number(id) : null;
  const selectedSourceItem: SourceItemSelection | undefined =
    sourceKey !== undefined && externalId !== undefined ? { sourceKey, externalId } : undefined;
  const enabledSources = useEnabledSources();
  // `?search=` lets another page (the recipe form's "faisable maison"
  // badge, see `ReproducibleBadge`) deep-link straight into a pre-filled
  // search — read once on mount, not kept in sync on every keystroke
  // afterwards (this page doesn't own the URL the way e.g. a shareable
  // filter view would).
  const [searchParams] = useSearchParams();

  // Deep-linking straight into `/recettes/sources/:sourceKey/:externalId`
  // must land on that source's own tab — otherwise `RecipeSourcesPanel`
  // (which reads this URL via `selectedSourceItem` below) wouldn't even be
  // mounted to show it. Lazy initializer: only matters for this page's
  // very first render, same reasoning as `search`'s below.
  const [activeTab, setActiveTab] = useState<RecipesPageTab>(() =>
    selectedSourceItem ? sourceTabValue(selectedSourceItem.sourceKey) : "favoris",
  );
  const activeSourceKey = parseSourceTabValue(activeTab);
  const [search, setSearch] = useState(() => searchParams.get("search") ?? "");
  // Seeded from the same initial value as `search` — otherwise the first
  // fetch below would fire with an empty term (the debounce effect hasn't
  // run yet), then a second one 300ms later once it catches up.
  const [debouncedSearch, setDebouncedSearch] = useState(() => searchParams.get("search") ?? "");
  const [listState, setListState] = useState<RecipeListState>({ status: "loading" });
  const [detailState, setDetailState] = useState<RecipeDetailState>({ status: "empty" });
  const [dislikedIngredientIds, setDislikedIngredientIds] = useState<number[]>([]);

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

  useEffect(() => {
    // A source's own tab doesn't query the recipe table at all — it
    // browses that source's live catalog instead (see `RecipeSourcesPanel`,
    // which owns its own fetching entirely). The type-guard (not just
    // `activeSourceKey !== null`) is what lets `activeTab` narrow to
    // `RecipeTab` below, for `apiClient.listRecipes`.
    if (isSourceTab(activeTab)) return;
    let cancelled = false;
    setListState({ status: "loading" });

    apiClient
      .listRecipes(activeTab, { search: debouncedSearch.trim() || undefined })
      .then((recipes) => {
        if (!cancelled) setListState({ status: "loaded", recipes });
      })
      .catch(() => {
        if (!cancelled) setListState({ status: "error" });
      });

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

  useEffect(() => {
    if (selectedId === null) {
      setDetailState({ status: "empty" });
      return;
    }
    let cancelled = false;
    setDetailState({ status: "loading" });

    apiClient
      .getRecipe(selectedId)
      .then((recipe) => {
        if (!cancelled) setDetailState({ status: "loaded", recipe });
      })
      .catch((err) => {
        if (cancelled) return;
        if (err instanceof ApiError && err.code === ErrorCode.RECIPE_NOT_FOUND) {
          setDetailState({ status: "not-found" });
        } else {
          setDetailState({ status: "error" });
        }
      });

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

  // The viewer's personal "disliked" list only changes from the
  // preferences page, never from here — loaded once, not re-fetched on
  // every tab/selection change.
  useEffect(() => {
    apiClient
      .getDislikedIngredientIds()
      .then(setDislikedIngredientIds)
      .catch(() => setDislikedIngredientIds([]));
  }, []);

  /** Keeps the table row's fav-mark and the `favoris` tab's membership in sync with a toggle made from the detail panel, without a full reload. */
  function handleFavoriteToggled(recipeId: number, isFavorite: boolean) {
    setDetailState((prev) =>
      prev.status === "loaded" && prev.recipe.id === recipeId
        ? { status: "loaded", recipe: { ...prev.recipe, isFavorite } }
        : prev,
    );
    setListState((prev) => {
      if (prev.status !== "loaded") return prev;
      const recipes = prev.recipes
        .map((recipe) => (recipe.id === recipeId ? { ...recipe, isFavorite } : recipe))
        .filter((recipe) => activeTab !== "favoris" || recipe.isFavorite);
      return { status: "loaded", recipes };
    });
  }

  /** After a delete, the removed recipe can no longer be selected, and the table must drop it too. */
  function handleDeleted(recipeId: number) {
    void navigate("/recettes");
    setListState((prev) =>
      prev.status === "loaded"
        ? { status: "loaded", recipes: prev.recipes.filter((r) => r.id !== recipeId) }
        : prev,
    );
  }

  return (
    <div className="recipes-page">
      <div className="recipes-page__header">
        <h1>{t("recipes.title")}</h1>
        {activeSourceKey === null && (
          <input
            type="search"
            className="recipes-page__search"
            placeholder={t("recipes.searchPlaceholder")}
            value={search}
            onChange={(e) => setSearch(e.target.value)}
          />
        )}
        <Link to="/recettes/nouvelle" className="recipes-page__new-button">
          {t("recipes.newButton")}
        </Link>
      </div>

      <RecipeTabs
        active={activeTab}
        onChange={setActiveTab}
        sources={enabledSources.status === "loaded" ? enabledSources.sources : []}
      />

      {activeSourceKey !== null ? (
        <RecipeSourcesPanel
          key={activeSourceKey}
          sourceKey={activeSourceKey}
          // Only meaningful when it actually belongs to this source — e.g.
          // clicking straight from TheMealDB's item "9999" to Marmiton's tab
          // changes `activeSourceKey` before `selectedSourceItem` (URL-driven)
          // catches up, since only picking a *row* navigates, not switching
          // tabs. Passing it through unguarded would have the freshly
          // (`key`-forced) remounted panel try to preview "9999" against the
          // wrong source.
          initialSelection={
            selectedSourceItem?.sourceKey === activeSourceKey ? selectedSourceItem : undefined
          }
          onItemSelected={(item) =>
            navigate(
              item
                ? `/recettes/sources/${item.sourceKey}/${encodeURIComponent(item.externalId)}`
                : "/recettes",
            )
          }
          onSelectImportedRecipe={(recipe) => {
            setActiveTab("favoris");
            void navigate(`/recettes/${recipe.id}`);
          }}
        />
      ) : (
        <div className="recipes-page__catalog">
          {listState.status === "loading" && (
            <p className="recipes-page__status">{t("recipes.loading")}</p>
          )}
          {listState.status === "error" && (
            <p className="recipes-page__status recipes-page__status--error">
              {t("common.loadError")}
            </p>
          )}
          {listState.status === "loaded" && listState.recipes.length === 0 && (
            <p className="recipes-page__status">{t("recipes.empty")}</p>
          )}
          {listState.status === "loaded" && listState.recipes.length > 0 && (
            <RecipeTable
              recipes={listState.recipes}
              selectedId={selectedId}
              onSelect={(recipeId) => navigate(`/recettes/${recipeId}`)}
            />
          )}

          <RecipeDetailPanel
            state={detailState}
            dislikedIngredientIds={dislikedIngredientIds}
            onFavoriteToggled={handleFavoriteToggled}
            onDeleted={handleDeleted}
          />
        </div>
      )}
    </div>
  );
}
