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 { RecipeSourcesPanel } from "../features/recipes/RecipeSourcesPanel";
import { RecipeTable } from "../features/recipes/RecipeTable";
import { RecipeTabs, type RecipesPageTab } from "../features/recipes/RecipeTabs";
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 both `/recettes` and `/recettes/:id` (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
 * `:id` param — a master-detail layout, not a navigation to a separate
 * page (see `RecipeDetailPanel`, which replaces the earlier standalone
 * `RecipeDetailPage`).
 */
export function RecipesPage() {
  const { t } = useTranslation();
  const navigate = useNavigate();
  const { id } = useParams<{ id: string }>();
  const selectedId = id !== undefined ? Number(id) : null;
  // `?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();

  const [activeTab, setActiveTab] = useState<RecipesPageTab>("favoris");
  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(() => {
    // The "sources" tab doesn't query the recipe table at all — it browses
    // a source's own live catalog instead (see `RecipeSourcesPanel`, which
    // owns its own fetching entirely).
    if (activeTab === "sources") 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) {
    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>
        {activeTab !== "sources" && (
          <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} />

      {activeTab === "sources" ? (
        <RecipeSourcesPanel
          onSelectImportedRecipe={(recipeId) => {
            setActiveTab("favoris");
            navigate(`/recettes/${recipeId}`);
          }}
        />
      ) : (
        <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>
  );
}
