import {
  type CreateRecipeInput,
  createRecipeSchema,
  type DietView,
  ErrorCode,
  type IngredientView,
  type RecipeVisibility,
  type UnitView,
} from "@batch-cooking/shared";
import { type FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { ApiError, apiClient } from "../../api/client";
import { DietTagSelect } from "../../features/recipes/badges/DietTagSelect";
import { IngredientPicker } from "../../features/recipes/ingredients/IngredientPicker";
import { IngredientRow } from "../../features/recipes/ingredients/IngredientRow";
import {
  isUnsavedPlaceholder,
  makePlaceholderIngredientView,
} from "../../features/recipes/ingredients/placeholder-ingredient";
import { type StepDraft, StepListEditor } from "../../features/recipes/steps/StepListEditor";
import "../../features/recipes/recipes.scss";
import { makeClientKey } from "../../lib/client-key";
import { errorMessageService } from "../../services/error-message.service";

/** In display order — mirrors `RecipeVisibility` (schema.prisma/shared types). */
const VISIBILITY_OPTIONS: RecipeVisibility[] = ["PERSONAL", "HOUSE", "PUBLIC"];

/**
 * One selected ingredient line — `key` is a client-only stable identity,
 * same reasoning as `StepDraft`. `unitId` is `null` until the user picks
 * one (no default — unlike `portions`, there's no single "usually right"
 * unit across every ingredient); `canSubmit` gates on every line having one
 * set before allowing save.
 *
 * `ingredient` is a real `IngredientView` for a catalog pick or an
 * edit-loaded placeholder, or a synthetic one (see
 * `makePlaceholderIngredientView`) for a brand-new free-text placeholder
 * the user added because the catalog fell short — the latter submits as
 * `placeholderName`, not `ingredientId` (see {@link isUnsavedPlaceholder}).
 */
interface IngredientLine {
  key: string;
  ingredient: IngredientView;
  quantity: string;
  unitId: number | null;
}

/** Load state for the reference ingredient list (+ the existing recipe, when editing) this form needs before it can render. */
type LoadState = "loading" | "loaded" | "error";

/**
 * Create/edit form for one recipe — routed at `/recettes/nouvelle` and
 * `/recettes/:id/modifier`. Same component for both: edit mode is just
 * "there's an `:id` param", which also drives preloading the existing
 * recipe's fields. Saving always sends the recipe's *whole* content (name,
 * ingredients, steps) — there's no partial-field save here, matching the
 * API's `PATCH /recipes/:id` contract (see `recipe.service.ts`).
 */
export function RecipeFormPage() {
  const { t } = useTranslation();
  const navigate = useNavigate();
  const { id } = useParams<{ id: string }>();
  const recipeId = id !== undefined ? Number(id) : null;
  const isEditing = recipeId !== null;

  const [loadState, setLoadState] = useState<LoadState>("loading");
  const [ingredientsCatalog, setIngredientsCatalog] = useState<IngredientView[]>([]);
  const [dietsCatalog, setDietsCatalog] = useState<DietView[]>([]);
  const [unitsCatalog, setUnitsCatalog] = useState<UnitView[]>([]);

  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [picture, setPicture] = useState("");
  // Pre-filled with a sensible default (same posture as `visibility`
  // defaulting to `PERSONAL`) rather than starting empty — this is a
  // required field, but the user shouldn't have to type a value just to
  // get past the gate if 4 is already right for their recipe.
  const [portions, setPortions] = useState("4");
  const [visibility, setVisibility] = useState<RecipeVisibility>("PERSONAL");
  const [dietIds, setDietIds] = useState<number[]>([]);
  const [ingredientLines, setIngredientLines] = useState<IngredientLine[]>([]);
  const [steps, setSteps] = useState<StepDraft[]>([]);

  const [formError, setFormError] = useState<string | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);

  useEffect(() => {
    let cancelled = false;
    setLoadState("loading");

    Promise.all([
      apiClient.getIngredients(),
      apiClient.getDiets(),
      apiClient.getUnits(),
      recipeId !== null ? apiClient.getRecipe(recipeId) : Promise.resolve(null),
    ])
      .then(([ingredients, diets, units, recipe]) => {
        if (cancelled) return;
        setIngredientsCatalog(ingredients);
        setDietsCatalog(diets);
        setUnitsCatalog(units);
        if (recipe) {
          setName(recipe.name);
          setDescription(recipe.description ?? "");
          setPicture(recipe.picture ?? "");
          setPortions(String(recipe.portions));
          setVisibility(recipe.visibility);
          setDietIds(recipe.diets.map((diet) => diet.id));
          setIngredientLines(
            recipe.ingredients.map((line) => ({
              key: makeClientKey(),
              ingredient: line.ingredient,
              quantity: String(line.quantity),
              unitId: line.unit.id,
            })),
          );
          setSteps(
            recipe.steps.map((step) => ({
              key: makeClientKey(),
              description: step.description,
              picture: step.picture ?? "",
            })),
          );
        }
        setLoadState("loaded");
      })
      .catch(() => {
        if (!cancelled) setLoadState("error");
      });

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

  function addIngredient(ingredient: IngredientView) {
    setIngredientLines((lines) => [
      ...lines,
      { key: makeClientKey(), ingredient, quantity: "", unitId: null },
    ]);
  }

  /** Adds a free-text placeholder line — the escape hatch when nothing in the catalog matches (see `IngredientPicker`'s `onAddPlaceholder`). */
  function addPlaceholderIngredient(name: string) {
    setIngredientLines((lines) => [
      ...lines,
      {
        key: makeClientKey(),
        ingredient: makePlaceholderIngredientView(name),
        quantity: "",
        unitId: null,
      },
    ]);
  }

  function updateIngredientLine(
    key: string,
    patch: Partial<Pick<IngredientLine, "quantity" | "unitId">>,
  ) {
    setIngredientLines((lines) =>
      lines.map((line) => (line.key === key ? { ...line, ...patch } : line)),
    );
  }

  function removeIngredientLine(key: string) {
    setIngredientLines((lines) => lines.filter((line) => line.key !== key));
  }

  // Surfaced next to the submit button when it's the reason `canSubmit` is
  // false — same "don't leave the button silently disabled" reasoning as
  // `RecipeImportForm` (issue #53), just less likely to bite here since a
  // manually-added line starts with no unit by design, right where the
  // person is already looking.
  const hasIngredientMissingUnit = ingredientLines.some((line) => line.unitId === null);

  // Gates the submit button — the schema (checked again on submit, see
  // `handleSubmit`) is the source of truth, this is just instant feedback
  // that doesn't need a round trip through zod on every keystroke.
  const canSubmit =
    name.trim().length > 0 &&
    Number.isInteger(Number(portions)) &&
    Number(portions) > 0 &&
    ingredientLines.length > 0 &&
    ingredientLines.every((line) => Number(line.quantity) > 0 && line.unitId !== null) &&
    steps.length > 0 &&
    steps.every((step) => step.description.trim().length > 0);

  async function handleSubmit(e: FormEvent) {
    e.preventDefault();
    setFormError(null);

    const payload: CreateRecipeInput = {
      name: name.trim(),
      description: description.trim() || null,
      picture: picture.trim() || null,
      portions: Number(portions),
      visibility,
      dietIds,
      ingredients: ingredientLines.map((line) => ({
        // A just-added free-text line has no catalog id yet — ask the API to
        // create the placeholder row via `placeholderName`. An edit-loaded
        // placeholder already has a real id and goes through `ingredientId`
        // like any other line (so re-saving never duplicates it).
        ...(isUnsavedPlaceholder(line.ingredient)
          ? { placeholderName: line.ingredient.displayName ?? "" }
          : { ingredientId: line.ingredient.id }),
        quantity: Number(line.quantity),
        // `canSubmit` already requires every line to have a unit picked
        // before the button is enabled — `?? 0` is just to satisfy the
        // type here; if it's ever reached with no unit set, the schema's
        // `positive()` check rejects it the same way an invalid quantity
        // already does.
        unitId: line.unitId ?? 0,
      })),
      steps: steps.map((step) => ({
        description: step.description.trim(),
        picture: step.picture.trim() || null,
      })),
    };

    const result = createRecipeSchema.safeParse(payload);
    if (!result.success) {
      setFormError(result.error.issues[0]?.message ?? t("recipes.form.genericError"));
      return;
    }

    setIsSubmitting(true);
    try {
      const saved =
        recipeId !== null
          ? await apiClient.updateRecipe(recipeId, result.data)
          : await apiClient.createRecipe(result.data);
      void navigate(`/recettes/${saved.id}`);
    } catch (err) {
      const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
      setFormError(errorMessageService.getLabel(code));
    } finally {
      setIsSubmitting(false);
    }
  }

  if (loadState === "loading") {
    return (
      <div className="recipe-form">
        <p className="recipes-page__status">{t("recipes.loading")}</p>
      </div>
    );
  }

  if (loadState === "error") {
    return (
      <div className="recipe-form">
        <p className="recipes-page__status recipes-page__status--error">{t("common.loadError")}</p>
      </div>
    );
  }

  // Placeholder lines carry no real catalog id (a brand-new one is id 0, an
  // edit-loaded one isn't in the browsable catalog anyway), so they never
  // belong in the picker's "already picked, hide it" set.
  const selectedIds = ingredientLines
    .filter((line) => !line.ingredient.isPlaceholder)
    .map((line) => line.ingredient.id);

  return (
    <form className="recipe-form" onSubmit={handleSubmit} noValidate>
      <h1>{isEditing ? t("recipes.form.editTitle") : t("recipes.form.newTitle")}</h1>

      <label htmlFor="recipe-name">{t("recipes.form.nameLabel")}</label>
      <input id="recipe-name" value={name} onChange={(e) => setName(e.target.value)} />

      <label htmlFor="recipe-description">{t("recipes.form.descriptionLabel")}</label>
      <textarea
        id="recipe-description"
        value={description}
        onChange={(e) => setDescription(e.target.value)}
        rows={3}
      />

      <label htmlFor="recipe-picture">{t("recipes.form.pictureLabel")}</label>
      <input
        id="recipe-picture"
        type="url"
        value={picture}
        onChange={(e) => setPicture(e.target.value)}
        placeholder="https://…"
      />

      <label htmlFor="recipe-portions">{t("recipes.form.portionsLabel")}</label>
      <input
        id="recipe-portions"
        type="number"
        min="1"
        step="1"
        value={portions}
        onChange={(e) => setPortions(e.target.value)}
      />

      <label htmlFor="recipe-visibility">{t("recipes.form.visibilityLabel")}</label>
      <select
        id="recipe-visibility"
        value={visibility}
        onChange={(e) => setVisibility(e.target.value as RecipeVisibility)}
      >
        {VISIBILITY_OPTIONS.map((option) => (
          <option key={option} value={option}>
            {t(`recipes.form.visibility.${option}`)}
          </option>
        ))}
      </select>

      <DietTagSelect diets={dietsCatalog} value={dietIds} onChange={setDietIds} />

      <section className="recipe-form__section">
        <h2>{t("recipes.ingredientsTitle")}</h2>
        <ul className="recipe-form__ingredient-list">
          {ingredientLines.map((line) => (
            <IngredientRow
              key={line.key}
              ingredient={line.ingredient}
              quantity={line.quantity}
              unitId={line.unitId}
              unitsCatalog={unitsCatalog}
              onQuantityChange={(quantity) => updateIngredientLine(line.key, { quantity })}
              onUnitChange={(unitId) => updateIngredientLine(line.key, { unitId })}
              onRemove={() => removeIngredientLine(line.key)}
            />
          ))}
        </ul>
        <IngredientPicker
          ingredients={ingredientsCatalog}
          excludeIds={selectedIds}
          onSelect={addIngredient}
          onAddPlaceholder={addPlaceholderIngredient}
        />
      </section>

      <section className="recipe-form__section">
        <h2>{t("recipes.stepsTitle")}</h2>
        <StepListEditor steps={steps} onChange={setSteps} />
      </section>

      {formError && <p className="form-error">{formError}</p>}
      {!canSubmit && hasIngredientMissingUnit && (
        <p className="form-error">{t("recipes.form.incompleteIngredientsHint")}</p>
      )}

      <div className="recipe-form__actions">
        <button type="submit" disabled={isSubmitting || !canSubmit}>
          {isSubmitting ? t("recipes.form.submitting") : t("recipes.form.submit")}
        </button>
      </div>
    </form>
  );
}
