"use client";

import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useForm, type FieldErrors, type Resolver } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import { catalogApi } from "@/lib/api/catalog";
import { sellerApi } from "@/lib/api/seller";
import { ApiException } from "@/lib/api/client";
import { useToken } from "@/lib/auth/useToken";
import type {
  ApiBrand,
  ApiCatalogProduct,
  ApiCatalogSuggestion,
  ApiCategory,
  ApiPackagingType,
  ApiProductUnit,
  ApiSavedImage,
  ApiSubCategory,
} from "@/lib/api/types";
import { catalogToFormValues } from "./catalogPrefill";
import ProductStepper from "./ProductStepper";
import ProductNavigation from "./ProductNavigation";
import Step1BasicInfo from "./Step1BasicInfo";
import Step2Media from "./Step2Media";
import Step3QualityBrand from "./Step3QualityBrand";
import Step4Specs from "./Step4Specs";
import Step5Variants from "./Step5Variants";
import Step6LoadingMoq from "./Step6LoadingMoq";
import Step7ChargesStatus from "./Step7ChargesStatus";
import {
  defaultProductValues,
  productSchema,
  productStepDescriptions,
  productStepFieldMap,
  productSteps,
  type ProductFormValues,
} from "./product.schema";

const TOTAL_STEPS = productSteps.length;

type Props = {
  mode: "create" | "edit";
  productId?: number | null;
  initialValues?: Partial<ProductFormValues>;
  existingImages?: ApiSavedImage[];
  /**
   * Approval state of the product being edited. An approved product is live, so
   * finishing the wizard saves it in place instead of sending it back for review.
   */
  requestStatus?: string | null;
  /**
   * Option lists fetched during SSR on the edit screen.
   *
   * Without them the selects mount before their options exist, and a <select>
   * whose value matches no option silently falls back to blank — which is why a
   * saved Category and Sub Category came up empty.
   */
  initialCategories?: ApiCategory[];
  initialSubCategories?: ApiSubCategory[];
  initialBrands?: ApiBrand[];
  initialPackagingTypes?: ApiPackagingType[];
  initialUnits?: ApiProductUnit[];
};

export default function ProductWizard({
  mode,
  productId: initialProductId = null,
  initialValues,
  existingImages: initialImages = [],
  requestStatus = null,
  initialCategories = [],
  initialSubCategories = [],
  initialBrands = [],
  initialPackagingTypes = [],
  initialUnits = [],
}: Props) {
  const router = useRouter();
  const { token } = useToken();
  const [categories, setCategories] = useState<ApiCategory[]>(initialCategories);
  const [brands, setBrands] = useState<ApiBrand[]>(initialBrands);
  const [packagingTypes, setPackagingTypes] = useState<ApiPackagingType[]>(initialPackagingTypes);
  const [units, setUnits] = useState<ApiProductUnit[]>(initialUnits);
  const [step, setStep] = useState(1);
  const [saving, setSaving] = useState(false);
  const [msg, setMsg] = useState<string | null>(null);
  const [apiErrors, setApiErrors] = useState<Record<string, string[]>>({});
  const [productId, setProductId] = useState<number | null>(initialProductId);
  const [existingImages, setExistingImages] = useState<ApiSavedImage[]>(initialImages);
  /**
   * The catalog product this listing is linked to, when there is one.
   *
   * Held here rather than in step 1 because the link reaches further than the
   * field that made it: step 3's brand list and step 6's loading locations are
   * both narrowed to what the catalog actually stocks.
   */
  const [catalog, setCatalog] = useState<ApiCatalogProduct | null>(null);
  const [linking, setLinking] = useState(false);

  /** Already in the catalog: edits save in place rather than queueing a review. */
  const isLive = requestStatus === "approved";

  const form = useForm<ProductFormValues>({
    resolver: zodResolver(productSchema) as unknown as Resolver<ProductFormValues>,
    defaultValues: { ...defaultProductValues, ...initialValues },
    mode: "onTouched",
  });

  const {
    handleSubmit,
    trigger,
    getValues,
    setValue,
    formState: { errors },
  } = form;

  useEffect(() => {
    // Already handed over by the server on the edit screen.
    if (initialCategories.length) return;

    catalogApi
      .categories({ per_page: 100 })
      .then((res) => setCategories(res.data ?? []))
      .catch(() => setCategories([]));
    // Mount-only: the server-provided list never changes for a given render.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (initialBrands.length) return;
    catalogApi
      .brands({ per_page: 100 })
      .then((res) => setBrands(res.data ?? []))
      .catch(() => setBrands([]));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (initialPackagingTypes.length) return;
    catalogApi
      .packagingTypes()
      .then((res) => setPackagingTypes(res.data ?? []))
      .catch(() => setPackagingTypes([]));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (initialUnits.length) return;
    catalogApi
      .units()
      .then((res) => setUnits(res.data ?? []))
      .catch(() => setUnits([]));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /*
    A product that already carries a catalog link keeps its narrowing.

    Only the brand and location lists are re-fetched — not the prefill: the
    seller's saved values are the ones that count on the edit screen, and
    overwriting them with the catalog's would undo their own pricing.
  */
  const linkedCatalogId = initialValues?.commodity_product_id ?? "";
  useEffect(() => {
    if (!token || !linkedCatalogId) return;
    let active = true;
    sellerApi
      .catalogProduct(token, Number(linkedCatalogId))
      .then((linked) => {
        if (active) setCatalog(linked);
      })
      .catch(() => {
        /* The listing still edits fine; only the narrowed lists are lost. */
      });
    return () => {
      active = false;
    };
  }, [token, linkedCatalogId]);

  /** Fill the wizard from a catalog product the seller picked by name. */
  const handleCatalogPick = async (suggestion: ApiCatalogSuggestion) => {
    if (!token) return;
    setLinking(true);
    try {
      const picked = await sellerApi.catalogProduct(token, suggestion.id);
      setCatalog(picked);
      /*
        reset(), not a run of setValue() calls: the prefill replaces whole field
        arrays — grades, packaging, variants, charges — and useFieldArray only
        rebuilds its rows from a reset. Current values go in first so anything
        the catalog has no opinion on (product type, the seller's short
        description, images already queued) survives.
      */
      form.reset({ ...getValues(), ...catalogToFormValues(picked) });
      toast.success(`Filled in from ${picked.name}. Change anything that differs for your offer.`);
    } catch {
      toast.error("Could not load that product from our catalog. Please try again.");
    } finally {
      setLinking(false);
    }
  };

  /**
   * Drop the link but keep the details.
   *
   * A seller who links by mistake is one click from a blank form otherwise, and
   * most of what was filled in is probably still right for their own product.
   */
  const handleCatalogClear = () => {
    setCatalog(null);
    setValue("commodity_product_id", "");
    toast("Unlinked from the catalog. The details already filled in are yours to edit.");
  };

  const reportError = (err: unknown, fallback: string) => {
    if (err instanceof ApiException) {
      setApiErrors(err.errors ?? {});
      const serverMessage = Object.values(err.errors ?? {}).flat().find(Boolean);
      const message = serverMessage || err.message || fallback;
      setMsg(message);
      toast.error(message);
      if (message.toLowerCase().includes("image")) setStep(2);
    } else {
      setMsg(fallback);
      toast.error(fallback);
    }
  };

  /** Saved images the seller has not marked for removal, plus anything pending. */
  const hasAnyImage = () => {
    const removed = getValues("removed_images") ?? [];
    const kept = existingImages.filter((image) => !removed.includes(String(image.id)));
    return kept.length > 0 || (getValues("product_images") as File[]).length > 0;
  };

  /** Persist one step to the server; returns the (possibly newly created) product id. */
  const persistStep = async (stepNo: number, currentId: number | null): Promise<number> => {
    const payload = buildStepPayload(stepNo, getValues());
    payload.append("step", String(stepNo));
    if (currentId) payload.append("id", String(currentId));

    const touchedImages =
      stepNo === 2 &&
      ((getValues("product_images") as File[]).length > 0 || (getValues("removed_images") ?? []).length > 0);
    // Saving step 1 with a catalog link copies that product's photographs onto
    // the draft, so the media step has something to show without the seller
    // re-uploading pictures the catalog already holds.
    const linkedCatalog = stepNo === 1 && Boolean(getValues("commodity_product_id"));
    const res = await sellerApi.saveProductStep(token!, payload);
    const id = res.data.id;
    setProductId(id);

    if (touchedImages || linkedCatalog) {
      // The server now owns the image list. Re-read it, and where the seller
      // touched it clear both queues so uploads are never repeated and removals
      // are never re-sent.
      try {
        const fresh = await sellerApi.productEditData(token!, id);
        setExistingImages(fresh.data.existing_images ?? []);
      } catch {
        /* keep existing preview */
      }
      if (touchedImages) {
        setValue("product_images", []);
        setValue("removed_images", []);
      }
    }
    return id;
  };

  const nextStep = async () => {
    if (!token) return;

    if (step === 2 && !hasAnyImage()) {
      toast.error("Please upload at least one product image.");
      return;
    }

    const fields = productStepFieldMap[step - 1] ?? [];
    const valid = fields.length ? await trigger(fields) : true;
    if (!valid) return;

    setSaving(true);
    setMsg(null);
    setApiErrors({});
    try {
      await persistStep(step, productId);
      setStep((prev) => Math.min(prev + 1, TOTAL_STEPS));
    } catch (err) {
      reportError(err, "Could not save this step. Please try again.");
    } finally {
      setSaving(false);
    }
  };

  const prevStep = () => {
    setMsg(null);
    setStep((prev) => Math.max(prev - 1, 1));
  };

  /**
   * Saves the current step and stays on it.
   *
   * Editing an existing product is usually one correction on one step, so the
   * seller should not have to walk the remaining steps to persist it. Only this
   * step's fields are validated — the API validates per step too, so a product
   * with an unrelated gap elsewhere does not block the field being fixed.
   */
  const saveCurrentStep = async () => {
    if (!token || !productId) return;

    if (step === 2 && !hasAnyImage()) {
      toast.error("Please upload at least one product image.");
      return;
    }

    const fields = productStepFieldMap[step - 1] ?? [];
    const valid = fields.length ? await trigger(fields) : true;
    if (!valid) return;

    setSaving(true);
    setMsg(null);
    setApiErrors({});
    try {
      await persistStep(step, productId);
      toast.success(`${productSteps[step - 1]} saved.`);
      setMsg(`${productSteps[step - 1]} saved.`);
    } catch (err) {
      reportError(err, "Could not save this step. Please try again.");
    } finally {
      setSaving(false);
    }
  };

  /**
   * Jumping between steps on an existing product — free in both directions.
   *
   * No validation and no save on the way out: a seller who only wants to swap a
   * photo should reach the Media step without first completing Basic Info,
   * which on a catalog-created product may have never been filled in at all.
   *
   * Nothing is lost by leaving a step. All seven steps share one form instance,
   * so typed values stay in the form; they are written to the server by "Save
   * this step" or by the final save, which persists every step.
   */
  const goToStep = (target: number) => {
    if (target === step || saving) return;
    setMsg(null);
    setApiErrors({});
    setStep(target);
  };

  const handleInvalidSubmit = (fieldErrors: FieldErrors<ProductFormValues>) => {
    const firstField = Object.keys(fieldErrors)[0] as keyof ProductFormValues | undefined;
    if (firstField) {
      const targetStep = productStepFieldMap.findIndex((fields) => fields.includes(firstField)) + 1;
      if (targetStep > 0) setStep(targetStep);
      const label = firstField.replace(/_/g, " ");
      toast.error(`Please fix ${label} before submitting.`);
      return;
    }
    toast.error("Please fix the highlighted fields before submitting.");
  };

  const submit = async () => {
    if (!token) return;
    if (!hasAnyImage()) {
      toast.error("Please upload at least one product image.");
      setStep(2);
      return;
    }
    setSaving(true);
    setMsg(null);
    setApiErrors({});
    try {
      // Persist every collect step (1-6) with current values, then finalise.
      let id = productId;
      for (let s = 1; s <= TOTAL_STEPS - 1; s++) {
        id = await persistStep(s, id);
      }
      if (!id) throw new Error("Product draft could not be created.");

      if (isLive) {
        // An approved product is already in the catalog. Submitting it would
        // send it back for review and pull it offline, so the last step is just
        // saved like the others.
        await persistStep(TOTAL_STEPS, id);
        toast.success("Product updated successfully.");
        router.push(`/seller/products/${id}`);
        router.refresh();
        return;
      }

      const payload = buildStepPayload(TOTAL_STEPS, getValues());
      const res = await sellerApi.submitProduct(token, id, payload);
      toast.success(res.message || "Product submitted successfully.");
      router.push("/seller/products");
    } catch (err) {
      reportError(err, isLive ? "Could not save the product. Please try again." : "Could not submit the product. Please try again.");
    } finally {
      setSaving(false);
    }
  };

  const hasError = Boolean(Object.keys(errors).length || Object.keys(apiErrors).length || (msg && msg.toLowerCase().includes("could not")));
  const heading = mode === "edit" ? "Edit Product" : "Add New Product";
  const crumb = mode === "edit" ? "Edit Product" : "Add Product";
  const submitLabel = isLive ? "Save changes" : mode === "edit" ? "Resubmit Product" : "Save Product";
  const intro = isLive
    ? "Open any step, change what you need and use “Save this step”. It goes straight to the live listing."
    : mode === "edit"
      ? "Open any step to edit it. Save a single step, or resubmit the whole product for review."
      : "Create a new product with all specifications and pricing details.";

  return (
    <div className="space-y-4">
      <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
        <div>
          <nav className="mb-1 text-xs text-gray-500">
            <Link href="/seller/products" className="hover:text-[#CC1414]">
              Products
            </Link>
            <span className="mx-1">›</span>
            <span className="text-gray-700">{crumb}</span>
          </nav>
          <h1 className="text-xl font-semibold text-gray-900">{heading}</h1>
          <p className="mt-1 text-sm text-gray-600">{intro}</p>
        </div>

        <div className="flex items-center gap-2 self-end sm:self-auto">
          <Link
            href={productId ? `/seller/products/${productId}` : "/seller/products"}
            className="rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
          >
            Cancel
          </Link>
          <button
            type="submit"
            form="product-wizard-form"
            disabled={saving}
            className="rounded-lg bg-[#CC1414] px-5 py-2 text-sm font-medium text-white hover:bg-[#b21010] disabled:opacity-60"
          >
            {saving ? "Saving..." : submitLabel}
          </button>
        </div>
      </div>

      <div className="rounded-2xl border border-slate-200 bg-white/95 p-4 shadow-[0_18px_40px_-28px_rgba(15,23,42,0.35)] backdrop-blur-sm sm:p-5 lg:p-6">
        {/* Free navigation only once the product exists — on a fresh draft there
            is nothing saved to jump to yet. */}
        <ProductStepper currentStep={step} onSelect={productId ? goToStep : undefined} />

        <div className="mt-5 rounded-xl border border-slate-200 bg-slate-50/90 px-4 py-3 sm:px-5 sm:py-4">
          <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-700">
            <span className="inline-flex items-center rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 ring-1 ring-slate-200">
              Step {step} of {TOTAL_STEPS}
            </span>
            <span className="text-slate-400">•</span>
            <span className="font-medium text-slate-600">{productStepDescriptions[step - 1]}</span>
          </div>
        </div>
      </div>

      <form id="product-wizard-form" onSubmit={handleSubmit(submit, handleInvalidSubmit)} className="space-y-4">
        {step === 1 && (
          <Step1BasicInfo
            form={form}
            categories={categories}
            initialSubCategories={initialSubCategories}
            token={token}
            linkedName={catalog?.name ?? null}
            linking={linking}
            onCatalogPick={handleCatalogPick}
            onCatalogClear={handleCatalogClear}
          />
        )}
        {step === 2 && <Step2Media form={form} existingImages={existingImages} />}
        {step === 3 && (
          <Step3QualityBrand
            form={form}
            brands={brands}
            packagingTypes={packagingTypes}
            locked={mode === "edit"}
            catalogBrands={catalog?.brands ?? null}
          />
        )}
        {step === 4 && <Step4Specs form={form} units={units} />}
        {step === 5 && <Step5Variants form={form} units={units} />}
        {step === 6 && (
          <Step6LoadingMoq form={form} units={units} catalogLocations={catalog?.locations ?? null} />
        )}
        {step === 7 && <Step7ChargesStatus form={form} />}

        <ProductNavigation
          step={step}
          totalSteps={TOTAL_STEPS}
          saving={saving}
          msg={msg}
          hasError={hasError}
          onPrev={prevStep}
          onNext={nextStep}
          onSaveStep={productId ? saveCurrentStep : undefined}
          submitLabel={submitLabel}
        />
      </form>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Per-step multipart payload builder                                  */
/* ------------------------------------------------------------------ */

function appendSpecs(fd: FormData, key: string, rows: ProductFormValues["physical_specs"]) {
  rows
    .filter((row) => row.parameter || row.value)
    .forEach((row, i) => {
      fd.append(`${key}[${i}][parameter]`, row.parameter ?? "");
      fd.append(`${key}[${i}][value]`, row.value ?? "");
      if (row.image instanceof File) fd.append(`${key}[${i}][image]`, row.image);
    });
}

function buildStepPayload(step: number, v: ProductFormValues): FormData {
  const fd = new FormData();

  switch (step) {
    case 1:
      fd.append("product_name", v.product_name);
      // Always sent, blank included: an empty value is how the API is told the
      // seller unlinked the listing from the catalog product.
      fd.append("commodity_product_id", v.commodity_product_id ?? "");
      fd.append("category_id", v.category);
      if (v.sub_category) fd.append("sub_category_id", v.sub_category);
      fd.append("product_type", v.product_type);
      fd.append("short_description", v.short_description);
      if (v.detailed_description) fd.append("detailed_description", v.detailed_description);
      fd.append("hsn_code", v.hsn_code);
      if (v.tax_rate) fd.append("tax_rate", v.tax_rate);
      break;

    case 2:
      v.product_images.forEach((file) => {
        if (file instanceof File) fd.append("product_images[]", file);
      });
      if (v.product_video) fd.append("product_video", v.product_video);
      v.removed_images.forEach((id) => fd.append("removed_images[]", id));
      break;

    case 3:
      // Only the ticked grades are the seller's offer; the rest are the catalog
      // list rendered for context and must not be saved.
      v.qualities
        .filter((row) => row.selected && (row.name ?? "").trim())
        .forEach((row, i) => {
          fd.append(`qualities[${i}][name]`, (row.name ?? "").trim());
          fd.append(`qualities[${i}][price]`, row.price ?? "");
        });
      fd.append("quality_charge", v.quality_charge ?? "");
      fd.append("quality_description", v.quality_description ?? "");
      // Multipart cannot carry an empty array, so an explicit flag tells the API
      // the step really did send a packaging selection — otherwise unticking the
      // last one would look like "packaging was not part of this step".
      fd.append("packaging_provided", "1");
      v.packaging
        .filter((row) => (row.type ?? "").trim())
        .forEach((row, i) => {
          fd.append(`packaging[${i}][type]`, row.type ?? "");
          fd.append(`packaging[${i}][charge]`, row.charge ?? "");
        });
      fd.append("brand_id", v.brand_id ?? "");
      fd.append("make", v.make ?? "");
      break;

    case 4:
      appendSpecs(fd, "physical_specs", v.physical_specs);
      appendSpecs(fd, "chemical_specs", v.chemical_specs);
      fd.append("weight_unit", v.weight_unit);
      fd.append("net_weight", v.net_weight);
      if (v.tolerance) fd.append("tolerance", v.tolerance);
      break;

    case 5:
      // Catalog rows keep their id and go back to the state-price row they came
      // from; free-form rows carry a size and unit instead. Unticked catalog
      // rows are still sent — the API needs them to clear the offer.
      v.variants.forEach((row, i) => {
        if ((row.id ?? "").trim()) {
          fd.append(`variants[${i}][id]`, row.id ?? "");
          fd.append(`variants[${i}][is_selected]`, row.selected ? "1" : "0");
        } else {
          fd.append(`variants[${i}][size]`, row.size ?? "");
          fd.append(`variants[${i}][unit]`, row.unit ?? "");
        }
        fd.append(`variants[${i}][charge]`, row.charge ?? "");
        fd.append(`variants[${i}][stock]`, row.stock ?? "");
      });
      break;

    case 6:
      fd.append("loading_city", v.loading_city);
      fd.append("loading_state", v.loading_state);
      fd.append("country", v.country);
      fd.append("moq", v.moq);
      fd.append("moq_unit", v.moq_unit);
      fd.append("available_quantity", v.available_quantity);
      fd.append("load_within", v.load_within);
      break;

    case 7:
      if (v.publish_on) fd.append("publish_on", v.publish_on);
      // Same flag trick as packaging: unticking every charge has to reach the
      // API as "no charges", not as a step that skipped charges.
      fd.append("charges_provided", "1");
      v.charges
        .filter((row) => row.enabled && row.type.trim())
        .forEach((row, i) => {
          fd.append(`charges[${i}][type]`, row.type.trim());
          fd.append(`charges[${i}][amount]`, row.amount ?? "");
          fd.append(`charges[${i}][operator]`, row.operator);
        });
      break;
  }

  return fd;
}
