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

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

/** A resolved ingredient line — identical shape to `RecipeFormPage`'s own `IngredientLine` (`ingredient` may be a synthetic placeholder view, see there). */
interface IngredientLine {
  key: string;
  ingredient: IngredientView;
  quantity: string;
  unitId: number | null;
}

/** One draft line that didn't resolve to a real `Ingredient` on preview (`DraftRecipeIngredientView.ingredient === null`) — still needs a person to pick the right one, keep it as a free-text placeholder, or discard it, before this recipe can be saved. */
interface UnresolvedIngredientLine {
  key: string;
  rawText: string;
  quantity: string;
}

type LoadState = "loading" | "loaded" | "error";

/**
 * The review/creation form for finalizing a source item's import — the
 * form itself, extracted out of `ImportRecipePage` so `RecipePickerDialog`
 * can embed it directly as a step of its own (the normal way this is
 * reached now: `handleSelectDraftItem`'s fallback when a draft has
 * something `tryBuildCompleteImport` couldn't resolve on its own) instead
 * of navigating to a separate page and losing the picker's context.
 * `ImportRecipePage` still wraps this as a standalone, directly-linkable
 * route — a safety net (a stale bookmark, a reload mid-flow), not the
 * primary path any more.
 *
 * Pre-filled from `GET /sources/:sourceKey/preview/:externalId`,
 * structurally the same form as `RecipeFormPage` — same sub-components
 * (`IngredientRow`, `IngredientPicker`, `StepListEditor`, `DietTagSelect`),
 * same `CreateRecipeInput` submit shape — plus one thing a manual creation
 * never has to handle: ingredient lines the automatic matching
 * (`ingredient-matcher.ts`) couldn't resolve. Those render as their own "à
 * compléter" list, each needing a real ingredient picked (or the line
 * discarded) before the form can submit — never silently drops/guesses
 * one, per the product decision this stage was built against (no invalid
 * recipe is ever persisted).
 *
 * Submits to `POST /sources/:sourceKey/import/:externalId`
 * (`apiClient.importSourceItem`) instead of `POST /recipes` — the only
 * other difference from `RecipeFormPage`'s own submit. When `planningSlot`
 * is given, a successful import also adds the freshly created recipe
 * straight to that slot (`POST /planning/items`, using this form's own
 * `portions` field) before calling `onImported` — `planningItem` on that
 * result is `null` either when there's no slot to add to, or when the
 * recipe saved fine but that add itself failed (the caller decides what to
 * do about that rather than this component guessing — see
 * `ImportRecipePage`/`RecipePickerDialog`'s own handling).
 */
export function RecipeImportForm({
  sourceKey,
  externalId,
  planningSlot,
  onImported,
}: {
  sourceKey: string;
  externalId: string;
  planningSlot?: { date: string; weekDay: WeekDay; meal: Meal };
  onImported: (result: { recipe: RecipeView; planningItem: PlanningItemView | null }) => void;
}) {
  const { t } = useTranslation();

  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("");
  const [portions, setPortions] = useState("4");
  const [visibility, setVisibility] = useState<RecipeVisibility>("PERSONAL");
  const [dietIds, setDietIds] = useState<number[]>([]);
  const [ingredientLines, setIngredientLines] = useState<IngredientLine[]>([]);
  const [unresolvedIngredients, setUnresolvedIngredients] = useState<UnresolvedIngredientLine[]>(
    [],
  );
  // Which unresolved line's picker is currently open — at most one at a
  // time (IngredientPicker is a whole browsable grid, not a compact
  // popover; showing one per unresolved line at once would be unwieldy).
  const [resolvingKey, setResolvingKey] = useState<string | null>(null);
  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(),
      apiClient.previewSourceItem(sourceKey, externalId),
    ])
      .then(([ingredients, diets, units, draft]) => {
        if (cancelled) return;
        setIngredientsCatalog(ingredients);
        setDietsCatalog(diets);
        setUnitsCatalog(units);

        setName(draft.name);
        setDescription(draft.description ?? "");
        setPicture(draft.picture ?? "");
        setPortions(draft.portions !== null ? String(draft.portions) : "4");

        const resolved: IngredientLine[] = [];
        const unresolved: UnresolvedIngredientLine[] = [];
        for (const line of draft.ingredients) {
          if (line.ingredient !== null) {
            resolved.push({
              key: makeClientKey(),
              ingredient: line.ingredient,
              quantity: line.quantity !== null ? String(line.quantity) : "",
              unitId: line.unit?.id ?? null,
            });
          } else {
            unresolved.push({
              key: makeClientKey(),
              rawText: line.rawText,
              quantity: line.quantity !== null ? String(line.quantity) : "",
            });
          }
        }
        setIngredientLines(resolved);
        setUnresolvedIngredients(unresolved);

        setSteps(
          draft.steps.map((step) => ({
            key: makeClientKey(),
            description: step.description,
            picture: step.picture ?? "",
          })),
        );
        setLoadState("loaded");
      })
      .catch(() => {
        if (!cancelled) setLoadState("error");
      });

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

  function addIngredient(ingredient: IngredientView) {
    setIngredientLines((lines) => [
      ...lines,
      { key: makeClientKey(), ingredient, 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));
  }

  /** Promotes an unresolved line into a real ingredient line, carrying its quantity over — its unit still needs picking, same as a freshly-added ingredient. */
  function resolveIngredient(unresolvedKey: string, ingredient: IngredientView) {
    setUnresolvedIngredients((lines) => {
      const line = lines.find((l) => l.key === unresolvedKey);
      if (line) {
        setIngredientLines((resolved) => [
          ...resolved,
          { key: makeClientKey(), ingredient, quantity: line.quantity, unitId: null },
        ]);
      }
      return lines.filter((l) => l.key !== unresolvedKey);
    });
    setResolvingKey(null);
  }

  /** Keeps an unresolved line as a free-text placeholder — the catalog had nothing matching, but the ingredient shouldn't be silently dropped (see `Ingredient.isPlaceholder` in schema.prisma). Its unit still needs picking, same as a resolved line. */
  function keepUnresolvedAsPlaceholder(unresolvedKey: string) {
    setUnresolvedIngredients((lines) => {
      const line = lines.find((l) => l.key === unresolvedKey);
      if (line) {
        setIngredientLines((resolved) => [
          ...resolved,
          {
            key: makeClientKey(),
            ingredient: makePlaceholderIngredientView(line.rawText.trim()),
            quantity: line.quantity,
            unitId: null,
          },
        ]);
      }
      return lines.filter((l) => l.key !== unresolvedKey);
    });
    setResolvingKey((current) => (current === unresolvedKey ? null : current));
  }

  function discardUnresolvedIngredient(key: string) {
    setUnresolvedIngredients((lines) => lines.filter((line) => line.key !== key));
    setResolvingKey((current) => (current === key ? null : current));
  }

  // Surfaced next to the submit button (see the `unitMissingHint` per-row
  // hint in `IngredientRow` for the same thing at the line level) — without
  // this, a pre-filled import whose auto-matched ingredients are otherwise
  // complete could leave `canSubmit` false with nothing on the page saying
  // why (see issue #53).
  const hasIngredientMissingUnit = ingredientLines.some((line) => line.unitId === null);

  // Two raw source lines can independently resolve to the same catalog
  // ingredient (e.g. "Egg Yolks" and "Eggs" both matching "Egg") —
  // `RecipeIngredient`'s primary key is `(recipeId, ingredientId)`, one row
  // per ingredient (schema.prisma), so submitting both would otherwise fail
  // (`createRecipeSchema` now rejects it, see its own doc comment). Blocked
  // here too, with each duplicate row highlighted, rather than letting the
  // user find out only after clicking "Importer".
  const duplicateIngredientIds = (() => {
    const seen = new Set<number>();
    const duplicates = new Set<number>();
    for (const line of ingredientLines) {
      // Placeholder lines have no catalog id (all share the sentinel 0) and
      // each becomes its own fresh row server-side — two of them never
      // collide on `RecipeIngredient`'s `(recipeId, ingredientId)` key.
      if (line.ingredient.isPlaceholder) continue;
      if (seen.has(line.ingredient.id)) duplicates.add(line.ingredient.id);
      seen.add(line.ingredient.id);
    }
    return duplicates;
  })();
  const hasDuplicateIngredient = duplicateIngredientIds.size > 0;

  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) &&
    !hasDuplicateIngredient &&
    unresolvedIngredients.length === 0 &&
    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) => ({
        // Free-text lines (nothing in the catalog matched) submit as
        // `placeholderName` so the API creates the placeholder row — same
        // branch as RecipeFormPage's identical submit.
        ...(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 —
        // same "?? 0, the schema rejects it if ever reached" reasoning as
        // RecipeFormPage's identical submit.
        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.sources.import.genericError"));
      return;
    }

    setIsSubmitting(true);
    try {
      const saved = await apiClient.importSourceItem(sourceKey, externalId, result.data);
      if (planningSlot) {
        try {
          const planningItem = await apiClient.addPlanningItem({
            date: planningSlot.date,
            weekDay: planningSlot.weekDay,
            meal: planningSlot.meal,
            recipeId: saved.id,
            portions: Number(portions),
          });
          onImported({ recipe: saved, planningItem });
          return;
        } catch {
          // The recipe itself was already imported successfully — only the
          // planning add failed. The caller decides what to do with a
          // `null` planningItem (land on the recipe's own page rather than
          // stranding the user on a form that already submitted; it can
          // still be added to that slot afterwards via the normal
          // "déjà importée" picker path).
          onImported({ recipe: saved, planningItem: null });
          return;
        }
      }
      onImported({ recipe: saved, planningItem: null });
    } 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("recipes.sources.import.loadError")}
        </p>
      </div>
    );
  }

  const selectedIds = ingredientLines
    .filter((line) => !line.ingredient.isPlaceholder)
    .map((line) => line.ingredient.id);

  return (
    <form className="recipe-form" onSubmit={handleSubmit} noValidate>
      {planningSlot && (
        <p className="source-item-preview__hint">{t("recipes.sources.import.planningHint")}</p>
      )}

      <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}
              duplicate={duplicateIngredientIds.has(line.ingredient.id)}
              onQuantityChange={(quantity) => updateIngredientLine(line.key, { quantity })}
              onUnitChange={(unitId) => updateIngredientLine(line.key, { unitId })}
              onRemove={() => removeIngredientLine(line.key)}
            />
          ))}
        </ul>

        {unresolvedIngredients.length > 0 && (
          <section className="import-recipe__unresolved">
            <h3>{t("recipes.sources.import.unresolvedTitle")}</h3>
            <p className="source-item-preview__hint">
              {t("recipes.sources.import.unresolvedHint")}
            </p>
            <ul className="import-recipe__unresolved-list">
              {unresolvedIngredients.map((line) => (
                <li key={line.key}>
                  <div className="import-recipe__unresolved-row">
                    <span>{line.rawText}</span>
                    <button
                      type="button"
                      onClick={() =>
                        setResolvingKey((current) => (current === line.key ? null : line.key))
                      }
                    >
                      {t("recipes.sources.import.resolveButton")}
                    </button>
                    <button type="button" onClick={() => keepUnresolvedAsPlaceholder(line.key)}>
                      {t("recipes.sources.import.keepAsPlaceholderButton")}
                    </button>
                    <button type="button" onClick={() => discardUnresolvedIngredient(line.key)}>
                      {t("recipes.sources.import.discardButton")}
                    </button>
                  </div>
                  {resolvingKey === line.key && (
                    <IngredientPicker
                      ingredients={ingredientsCatalog}
                      excludeIds={selectedIds}
                      onSelect={(ingredient) => resolveIngredient(line.key, ingredient)}
                    />
                  )}
                </li>
              ))}
            </ul>
          </section>
        )}

        <IngredientPicker
          ingredients={ingredientsCatalog}
          excludeIds={selectedIds}
          onSelect={addIngredient}
        />
      </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>
      )}
      {!canSubmit && hasDuplicateIngredient && (
        <p className="form-error">{t("recipes.form.duplicateIngredientsHint")}</p>
      )}

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