import {
  type DietView,
  ErrorCode,
  type IngredientView,
  type Meal,
  type PlanningItemView,
  type RecipeSummaryView,
  type RecipeView,
  type WeekDay,
} from "@batch-cooking/shared";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { ApiError, apiClient } from "../../api/client";
import { CheckboxOption } from "../../components/ui/Checkbox";
import { Dialog } from "../../components/ui/Dialog";
import { errorMessageService } from "../../services/error-message.service";
import { DietTagSelect } from "../recipes/badges/DietTagSelect";
import { IngredientPicker } from "../recipes/ingredients/IngredientPicker";
import { ingredientLabel } from "../recipes/ingredients/ingredient-label";
import { RecipeDetailPanel, type RecipeDetailState } from "../recipes/RecipeDetailPanel";
import { RecipeTable } from "../recipes/RecipeTable";
import {
  isSourceTab,
  parseSourceTabValue,
  type RecipesPageTab,
  RecipeTabs,
} from "../recipes/RecipeTabs";
import { RecipeImportForm } from "../recipes/sources/RecipeImportForm";
import {
  RecipeSourcesPanel,
  type SourceItemSelection,
} from "../recipes/sources/RecipeSourcesPanel";
import { tryBuildCompleteImport } from "../recipes/sources/recipe-import-draft";
import { useEnabledSources } from "../recipes/sources/useEnabledSources";
import "./recipe-picker-dialog.scss";

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

/** Load state for the filtered catalog list, same discriminated-union shape as `RecipesPage`'s `RecipeListState`. */
type ListState =
  | { status: "loading" }
  | { status: "loaded"; recipes: RecipeSummaryView[] }
  | { status: "error" };

/**
 * The (day, meal) slot a `RecipePickerDialog` is adding a recipe to —
 * `date` is that day's `YYYY-MM-DD` (the specific date within the
 * displayed week, not just its weekday), needed by `POST /planning/items`
 * to resolve which week's `Planning` row to attach to.
 */
export interface PlanningSlot {
  date: string;
  weekDay: WeekDay;
  meal: Meal;
}

/**
 * Recipe-selection dialog opened from a planning grid cell's "+" button
 * (see `PlanningPage.tsx`'s `MealCell`) — the same catalog browsing
 * experience as `/recettes` (`RecipeTabs` + `RecipeTable`, reused as-is),
 * with three extra filters layered on top of the plain name search
 * (ingredients / regime / "convient à tout le foyer" toggle, all wired to
 * `GET /recipes`'s corresponding query params) since browsing here is
 * about finding something to cook, not just looking something up. Each
 * household-enabled source's own tab is included too.
 *
 * Mounted only while open (see `PlanningPage`, same conditional-mount
 * convention as its own `CalendarPopover`) — every piece of local state
 * below resets for free the next time it's reopened, no manual reset
 * needed.
 *
 * Clicking a row only ever *selects* it — same "preview before you commit"
 * shape for every kind of row: a regular tab's own master-detail pair
 * (`RecipeTable` + a `RecipeDetailPanel` fetched here, mirroring
 * `RecipesPage`'s own layout) fetches and previews a real recipe; a
 * source tab's `RecipeSourcesPanel` already previews either kind of row it
 * has (already-imported or not) inline, on its own. Nothing about a click
 * commits to anything by itself — the pinned footer's "Confirmer" button
 * (`handleFooterConfirm`) is what acts on whichever preview is currently
 * pending (`previewedRecipe`/`previewedDraft`, mutually exclusive):
 * - A real recipe (regular tab, or an already-imported source item) moves
 *   to the small "how many portions?" step (`selectedRecipe`), same as
 *   before this dialog grew a footer.
 * - A not-yet-imported source item is what actually imports one — nowhere
 *   else in the app does (see `confirmDraftSelection`) — since a source
 *   item only ever becomes a real, saved `Recipe` as a side effect of
 *   someone adding it to their planning. When the draft has everything a
 *   real recipe needs, it's imported and added to this slot transparently
 *   — no extra screen. Only when something's actually missing (an
 *   ingredient the automatic matcher couldn't resolve, say) does this
 *   switch to a third step instead, embedding the full review form
 *   (`RecipeImportForm`) right in this same dialog rather than navigating
 *   away to `ImportRecipePage` and losing the picker's own context (search
 *   term, filters, which slot this even was).
 */
export function RecipePickerDialog({
  slot,
  onClose,
  onAdded,
}: {
  slot: PlanningSlot;
  onClose: () => void;
  onAdded: (item: PlanningItemView) => void;
}) {
  const { t } = useTranslation();
  const navigate = useNavigate();

  const [activeTab, setActiveTab] = useState<RecipesPageTab>("favoris");
  const activeSourceKey = parseSourceTabValue(activeTab);

  /** Switching tabs drops whatever was previewed/pending on the one just left — a stale "Confirmer" target from a different tab would be confusing at best. */
  function handleTabChange(tab: RecipesPageTab) {
    setActiveTab(tab);
    setPreviewedRecipe(null);
    setPreviewedDraft(null);
    setRegularPreviewState({ status: "empty" });
  }
  const enabledSources = useEnabledSources();
  const [search, setSearch] = useState("");
  const [debouncedSearch, setDebouncedSearch] = useState("");
  const [selectedIngredientIds, setSelectedIngredientIds] = useState<number[]>([]);
  const [selectedDietIds, setSelectedDietIds] = useState<number[]>([]);
  const [suitableForHousehold, setSuitableForHousehold] = useState(false);
  const [hasHousehold, setHasHousehold] = useState(false);
  const [isIngredientPickerOpen, setIsIngredientPickerOpen] = useState(false);

  const [ingredientsCatalog, setIngredientsCatalog] = useState<IngredientView[]>([]);
  const [dietsCatalog, setDietsCatalog] = useState<DietView[]>([]);
  const [listState, setListState] = useState<ListState>({ status: "loading" });
  // The regular tabs' own master-detail pair — `RecipeTable` on the left,
  // this on the right, fetched on row click (mirrors `RecipesPage`'s
  // identical layout). Source tabs don't use this at all: `RecipeSourcesPanel`
  // previews its own rows internally.
  const [regularPreviewState, setRegularPreviewState] = useState<RecipeDetailState>({
    status: "empty",
  });
  // Which real recipe is the pending selection — from either the regular
  // tabs' own preview above, or a source tab's already-imported row
  // (`RecipeSourcesPanel`'s `onSelectImportedRecipe`, which already
  // previewed it internally). Mutually exclusive with `previewedDraft`
  // below; the footer's "Confirmer" (`handleFooterConfirm`) acts on
  // whichever one is set.
  const [previewedRecipe, setPreviewedRecipe] = useState<RecipeView | null>(null);
  // Which not-yet-imported source item is the pending selection —
  // `RecipeSourcesPanel`'s `onDraftSelected`, fired the moment such a row
  // is clicked (it previews itself internally; this is just "which one").
  const [previewedDraft, setPreviewedDraft] = useState<SourceItemSelection | null>(null);
  // True while `confirmDraftSelection` below is resolving the footer's
  // "Confirmer" for a pending draft (fetch it, maybe import it, maybe add
  // it to the slot) — disables the footer for that brief window rather
  // than allowing a second click mid-flight.
  const [isConfirmingDraft, setIsConfirmingDraft] = useState(false);
  // Set by `confirmDraftSelection`'s fallback when the pending draft needs
  // a person's input before it can be imported — switches this whole
  // dialog to its third step (see the top-level `if` below), embedding
  // `RecipeImportForm` instead of showing it inline here.
  const [reviewDraftItem, setReviewDraftItem] = useState<SourceItemSelection | null>(null);

  // The recipe the footer's "Confirmer" moved to this small step for —
  // `null` while still browsing.
  const [selectedRecipe, setSelectedRecipe] = useState<RecipeSummaryView | null>(null);
  const [portions, setPortions] = useState("1");
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitError, setSubmitError] = useState<string | null>(null);

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

  // Reference lists + "does the viewer have a household" — loaded once,
  // they don't change while the dialog is open.
  useEffect(() => {
    apiClient
      .getIngredients()
      .then(setIngredientsCatalog)
      .catch(() => setIngredientsCatalog([]));
    apiClient
      .getDiets()
      .then(setDietsCatalog)
      .catch(() => setDietsCatalog([]));
    apiClient
      .getCurrentHouse()
      .then((house) => setHasHousehold(house !== null))
      .catch(() => setHasHousehold(false));
  }, []);

  useEffect(() => {
    // A source's own tab doesn't query the recipe table at all — same
    // guard as `RecipesPage`'s own identical effect (the type-guard, not
    // just `activeSourceKey !== null`, is what narrows `activeTab` to
    // `RecipeTab` below).
    if (isSourceTab(activeTab)) return;
    let cancelled = false;
    setListState({ status: "loading" });

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

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

  const selectedIngredients = ingredientsCatalog.filter((ingredient) =>
    selectedIngredientIds.includes(ingredient.id),
  );

  /** A regular tab's own row click — fetches the full recipe and previews it in this dialog's own master-detail pair, exactly like `RecipesPage` does. */
  function handleSelectRegularRecipe(id: number) {
    setPreviewedDraft(null);
    setRegularPreviewState({ status: "loading" });
    apiClient
      .getRecipe(id)
      .then((recipe) => {
        setRegularPreviewState({ status: "loaded", recipe });
        setPreviewedRecipe(recipe);
      })
      .catch(() => setRegularPreviewState({ status: "error" }));
  }

  /** A source tab's already-imported row — `RecipeSourcesPanel` already fetched and is previewing it itself; this just records it as the pending selection. */
  function handleSelectImportedRecipe(recipe: RecipeView) {
    setPreviewedDraft(null);
    setPreviewedRecipe(recipe);
  }

  /** A source tab's not-yet-imported row — `RecipeSourcesPanel` previews it itself; this just records it as the pending selection. */
  function handleDraftSelected(selection: SourceItemSelection) {
    setPreviewedRecipe(null);
    setPreviewedDraft(selection);
  }

  /**
   * The footer's "Confirmer" — acts on whichever preview is currently
   * pending. A real recipe moves to the small portions step below; a
   * not-yet-imported draft runs {@link confirmDraftSelection}.
   */
  function handleFooterConfirm() {
    if (previewedRecipe) {
      setSelectedRecipe(previewedRecipe);
      setPortions(String(previewedRecipe.portions));
      return;
    }
    if (previewedDraft) {
      void confirmDraftSelection(previewedDraft);
    }
  }

  /**
   * Confirming a not-yet-imported source item — the one action in the
   * whole app that actually imports one (see this component's own doc
   * comment). Fetches its full draft, and when {@link tryBuildCompleteImport}
   * finds nothing missing, imports it and adds it to `slot` transparently:
   * no extra screen, same end result as picking any other recipe. Anything
   * short of that — an unresolved ingredient, a network hiccup on any of
   * these three calls — switches to the embedded review-form step instead
   * (`setReviewDraftItem`), since only a person can supply what's actually
   * missing.
   */
  async function confirmDraftSelection(selection: SourceItemSelection) {
    const { sourceKey, externalId } = selection;
    setIsConfirmingDraft(true);

    function needsReview() {
      setIsConfirmingDraft(false);
      setReviewDraftItem({ sourceKey, externalId });
    }

    let payload: ReturnType<typeof tryBuildCompleteImport>;
    try {
      payload = tryBuildCompleteImport(await apiClient.previewSourceItem(sourceKey, externalId));
    } catch {
      needsReview();
      return;
    }
    if (!payload) {
      needsReview();
      return;
    }

    let saved: RecipeView;
    try {
      saved = await apiClient.importSourceItem(sourceKey, externalId, payload);
    } catch {
      needsReview();
      return;
    }

    try {
      const planningItem = await apiClient.addPlanningItem({
        date: slot.date,
        weekDay: slot.weekDay,
        meal: slot.meal,
        recipeId: saved.id,
        portions: payload.portions,
      });
      onAdded(planningItem);
      onClose();
    } catch {
      // The recipe itself is already saved at this point — only adding it
      // to this slot failed. Land on its own page rather than retrying the
      // whole import (same fallback `RecipeImportForm`'s own submit takes
      // for the identical failure — see this dialog's `onImported` handler
      // below).
      setIsConfirmingDraft(false);
      void navigate(`/recettes/${saved.id}`);
    }
  }

  async function handleConfirm() {
    if (!selectedRecipe) return;
    const parsedPortions = Number(portions);
    if (!Number.isInteger(parsedPortions) || parsedPortions < 1) return;

    setIsSubmitting(true);
    setSubmitError(null);
    try {
      const item = await apiClient.addPlanningItem({
        date: slot.date,
        weekDay: slot.weekDay,
        meal: slot.meal,
        recipeId: selectedRecipe.id,
        portions: parsedPortions,
      });
      onAdded(item);
      onClose();
    } catch (err) {
      const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
      setSubmitError(errorMessageService.getLabel(code));
      setIsSubmitting(false);
    }
  }

  if (reviewDraftItem) {
    return (
      <Dialog
        onClose={onClose}
        title={t("recipes.sources.import.title")}
        className="recipe-picker-dialog"
      >
        <RecipeImportForm
          sourceKey={reviewDraftItem.sourceKey}
          externalId={reviewDraftItem.externalId}
          planningSlot={slot}
          onImported={({ recipe, planningItem }) => {
            if (planningItem) {
              onAdded(planningItem);
              onClose();
            } else {
              // The recipe itself is saved at this point — only adding it
              // to this slot failed. Same fallback as the transparent-
              // import path above: land on its own page instead of
              // retrying.
              void navigate(`/recettes/${recipe.id}`);
            }
          }}
        />
      </Dialog>
    );
  }

  if (selectedRecipe) {
    return (
      <Dialog
        onClose={onClose}
        title={t("planning.picker.confirmTitle", { recipe: selectedRecipe.name })}
      >
        <div className="recipe-picker-confirm">
          <label htmlFor="planning-picker-portions">{t("planning.picker.portionsLabel")}</label>
          <input
            id="planning-picker-portions"
            type="number"
            min="1"
            step="1"
            value={portions}
            onChange={(e) => setPortions(e.target.value)}
          />
          {submitError && <p className="field-error">{submitError}</p>}
          <div className="recipe-picker-confirm__actions">
            <button type="button" onClick={() => setSelectedRecipe(null)} disabled={isSubmitting}>
              {t("planning.picker.backButton")}
            </button>
            <button
              type="button"
              className="recipe-picker-confirm__confirm"
              onClick={handleConfirm}
              disabled={isSubmitting}
            >
              {isSubmitting ? t("planning.picker.adding") : t("planning.picker.confirmButton")}
            </button>
          </div>
        </div>
      </Dialog>
    );
  }

  const canConfirm = previewedRecipe !== null || previewedDraft !== null;

  return (
    <Dialog
      onClose={onClose}
      title={t("planning.picker.title")}
      className="recipe-picker-dialog"
      footer={
        <>
          <button type="button" onClick={onClose}>
            {t("planning.picker.footerClose")}
          </button>
          <button
            type="button"
            className="recipe-picker-confirm__confirm"
            onClick={handleFooterConfirm}
            disabled={!canConfirm || isConfirmingDraft}
          >
            {isConfirmingDraft ? t("planning.picker.adding") : t("planning.picker.footerConfirm")}
          </button>
        </>
      }
    >
      {activeSourceKey === null && (
        <div className="recipe-picker__filters">
          <input
            type="search"
            className="recipe-picker__search"
            placeholder={t("planning.picker.searchPlaceholder")}
            value={search}
            onChange={(e) => setSearch(e.target.value)}
          />

          <div className="recipe-picker__ingredient-filter">
            <span className="recipe-picker__filter-label">
              {t("planning.picker.ingredientsFilterLabel")}
            </span>
            <div className="recipe-picker__chips">
              {selectedIngredients.map((ingredient) => (
                <span key={ingredient.id} className="filter-chip">
                  {ingredientLabel(ingredient, t)}
                  <button
                    type="button"
                    onClick={() =>
                      setSelectedIngredientIds((ids) => ids.filter((id) => id !== ingredient.id))
                    }
                  >
                    ✕
                  </button>
                </span>
              ))}
              <button
                type="button"
                className="recipe-picker__toggle-ingredient-picker"
                onClick={() => setIsIngredientPickerOpen((open) => !open)}
                aria-expanded={isIngredientPickerOpen}
              >
                +{" "}
                {isIngredientPickerOpen
                  ? t("planning.picker.hideIngredientPicker")
                  : t("planning.picker.addIngredientFilter")}
              </button>
            </div>
            {isIngredientPickerOpen && (
              <IngredientPicker
                ingredients={ingredientsCatalog}
                excludeIds={selectedIngredientIds}
                onSelect={(ingredient) =>
                  setSelectedIngredientIds((ids) => [...ids, ingredient.id])
                }
              />
            )}
          </div>

          <DietTagSelect
            diets={dietsCatalog}
            value={selectedDietIds}
            onChange={setSelectedDietIds}
          />

          {hasHousehold && (
            <CheckboxOption checked={suitableForHousehold} onChange={setSuitableForHousehold}>
              {t("planning.picker.suitableForHouseholdLabel")}
            </CheckboxOption>
          )}
        </div>
      )}

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

      {activeSourceKey !== null ? (
        <RecipeSourcesPanel
          key={activeSourceKey}
          sourceKey={activeSourceKey}
          onSelectImportedRecipe={handleSelectImportedRecipe}
          onDraftSelected={handleDraftSelected}
        />
      ) : (
        <>
          {listState.status === "loading" && (
            <p className="recipes-page__status">{t("planning.picker.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("planning.picker.empty")}</p>
          )}
          {listState.status === "loaded" && listState.recipes.length > 0 && (
            <div className="recipes-page__catalog">
              <RecipeTable
                recipes={listState.recipes}
                selectedId={previewedRecipe?.id ?? null}
                onSelect={handleSelectRegularRecipe}
              />
              <RecipeDetailPanel state={regularPreviewState} showActions={false} />
            </div>
          )}
        </>
      )}
    </Dialog>
  );
}
