"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useFieldArray, useForm, type Resolver } from "react-hook-form";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { z } from "zod";
import { Alert } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { FormField } from "@/components/ui/form-field";
import { Input, Textarea } from "@/components/ui/input";
import { PhoneField } from "@/components/shared/phone-field";
import { useT } from "@/i18n";
import { applyFieldErrors, friendlyErrorMessage } from "@/lib/errors";
import { queryKeys } from "@/lib/query-keys";
import { updateOrder } from "@/services/orders";
import type { Order, OrderStatus, UpdateOrderInput, UpdateOrderItemInput } from "@/types/order";
import { LETTERS_ONLY_RE } from "@/lib/field-rules";

/** The server's own limit, said here so the refusal lands before the wire. */
const REASON_MAX = 255;

const itemSchema = z.object({
  name: z.string().trim().min(1, "errors.required").max(255),
  quantity: z.coerce.number().int().min(1).max(999),
  unit_price: z.union([z.literal(""), z.coerce.number().min(0)]),
  sku: z.string().trim().max(255),
});

const editSchema = z.object({
  customer_name: z.string().trim().min(1, "errors.required").regex(LETTERS_ONLY_RE, "errors.lettersOnly").max(255),
  customer_phone: z.string().trim(),
  customer_note: z.string(),
  note: z.string(),
  pickup_name: z.string().trim().max(255),
  pickup_address: z.string().trim().min(1, "errors.required").max(255),
  pickup_lat: z.union([z.literal(""), z.coerce.number().min(-90).max(90)]),
  pickup_lng: z.union([z.literal(""), z.coerce.number().min(-180).max(180)]),
  dropoff_address: z.string().trim().min(1, "errors.required").max(255),
  dropoff_lat: z.union([z.literal(""), z.coerce.number().min(-90).max(90)]),
  dropoff_lng: z.union([z.literal(""), z.coerce.number().min(-180).max(180)]),
  amount_to_collect: z.union([z.literal(""), z.coerce.number().min(0)]),
  items: z.array(itemSchema),
  reason: z.string().trim().max(REASON_MAX),
});

type EditValues = z.infer<typeof editSchema>;
type ItemValues = EditValues["items"][number];

// `z.coerce.number()` widens schema input types to `unknown`; the resolver is handed a
// `Resolver` whose input matches the form's already-coerced values. Same as the create form.
const resolver = zodResolver(editSchema) as unknown as Resolver<EditValues>;

/**
 * How far an order has got, for the two parts of it that stop being editable.
 *
 * The server decides and refuses by name — this only avoids offering a field that is certain to
 * be refused. The rules are the ones the API documents: the pickup locks once a captain has been
 * assigned, because they were chosen by their distance to it; the basket locks once the captain
 * is holding the goods, because changing the list does not change what is in the car.
 */
function locksFor(status: OrderStatus) {
  const inTransit = status === "picked_up" || status === "on_the_way";

  return {
    pickup: status !== "pending",
    items: inTransit,
    /** Once the goods are moving, anything beyond the customer's own details has to say why. */
    reasonRequired: inTransit,
  };
}

interface EditOrderDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  order: Order;
}

/**
 * Change an order that is already open.
 *
 * Until this existed the office could only cancel and raise a new order, which loses the
 * history, the captain and the assignment — for something as ordinary as a customer who has
 * moved, a supplier branch that was wrong, or a quantity typed twice.
 *
 * **It sends only what changed.** The server reads an absent field as "leave it" and a null as
 * "clear it", so posting the whole form back would turn every edit into a rewrite of the order
 * — and would reprice a delivery because somebody fixed a spelling. What is sent is diffed
 * against the order as it was opened.
 *
 * **The basket is the exception, and it is sent whole.** A partial basket cannot say whether a
 * missing line was removed or simply not mentioned, so the server replaces every line with the
 * ones it is given. That is also why it is only sent when something in it actually moved.
 */
export function EditOrderDialog(props: EditOrderDialogProps) {
  return (
    <Dialog open={props.open} onOpenChange={props.onOpenChange}>
      <DialogContent className="max-h-[90vh] max-w-3xl overflow-y-auto">
        {/* Its own component, so every open starts from the order as it now stands rather
            than from the values the last open was given. */}
        <EditForm {...props} />
      </DialogContent>
    </Dialog>
  );
}

function EditForm({ onOpenChange, order }: EditOrderDialogProps) {
  const t = useT();
  const queryClient = useQueryClient();
  const locks = locksFor(order.status);

  /*
   * A line the office called off stays listed, counts for nothing, and says so. Replacing the
   * basket writes plain lines, which would bring it back as an ordinary one — so an order
   * carrying a cancelled line is not offered basket editing at all. Losing the record of a
   * cancellation to a quantity correction is not a trade worth making silently.
   */
  const hasCancelledLine = (order.items ?? []).some((item) => item.cancelled === true);
  const itemsEditable = !locks.items && !hasCancelledLine;

  const initialItems: ItemValues[] = (order.items ?? []).map((item) => ({
    name: item.name,
    quantity: item.quantity,
    unit_price: item.unit_price ?? "",
    sku: item.sku ?? "",
  }));

  const initial: EditValues = {
    customer_name: order.customer_name ?? "",
    customer_phone: order.customer_phone ?? "",
    customer_note: order.customer_note ?? "",
    note: order.note ?? "",
    pickup_name: order.pickups?.[0]?.store_name ?? "",
    pickup_address: order.pickup_address ?? "",
    pickup_lat: order.pickup_lat ?? "",
    pickup_lng: order.pickup_lng ?? "",
    dropoff_address: order.dropoff_address ?? "",
    dropoff_lat: order.dropoff_lat ?? "",
    dropoff_lng: order.dropoff_lng ?? "",
    amount_to_collect: order.amount_to_collect ?? "",
    items: initialItems,
    reason: "",
  };

  const form = useForm<EditValues>({ resolver, defaultValues: initial });
  const items = useFieldArray({ control: form.control, name: "items" });

  /**
   * Only the fields that actually moved.
   *
   * An empty box means "clear it" for the optional fields and is sent as null; for the ones that
   * cannot be empty the schema has already refused it. Comparing against `initial` rather than
   * React Hook Form's own dirty tracking because a field typed and then typed back is not a
   * change, and the server would reprice as though it were.
   */
  function changedOnly(values: EditValues): UpdateOrderInput {
    const input: UpdateOrderInput = {};

    for (const field of ["customer_name", "dropoff_address", "pickup_address"] as const) {
      if (values[field] !== initial[field]) input[field] = values[field];
    }

    for (const field of ["customer_phone", "customer_note", "note", "pickup_name"] as const) {
      if (values[field] !== initial[field]) {
        input[field] = values[field] === "" ? null : values[field];
      }
    }

    for (const field of ["dropoff_lat", "dropoff_lng", "amount_to_collect"] as const) {
      if (values[field] !== initial[field]) {
        input[field] = values[field] === "" ? null : Number(values[field]);
      }
    }

    /*
     * The pickup coordinates are not nullable the way the drop-off's are: an order with no
     * collection point is one nobody can route, and clearing it is not an edit anybody means.
     */
    for (const field of ["pickup_lat", "pickup_lng"] as const) {
      if (values[field] !== initial[field] && values[field] !== "") {
        input[field] = Number(values[field]);
      }
    }

    if (itemsEditable) {
      const basket = values.items.map(
        (line): UpdateOrderItemInput => ({
          name: line.name,
          quantity: Number(line.quantity),
          unit_price: line.unit_price === "" ? null : Number(line.unit_price),
          sku: line.sku === "" ? null : line.sku,
        }),
      );

      // Compared as a whole, because the basket travels as a whole: a reordered list is a
      // different basket to the server, and an identical one must not trigger a re-price.
      if (JSON.stringify(basket) !== JSON.stringify(asPayloadItems(initialItems))) {
        input.items = basket;
      }
    }

    if (values.reason !== "") input.reason = values.reason;

    return input;
  }

  const save = useMutation({
    mutationFn: (values: EditValues) => updateOrder(order.uuid, changedOnly(values)),
    onSuccess: (response) => {
      toast.success(response.Message ?? t("orders.edit.done"));
      onOpenChange(false);
      /*
       * This order and every list it sits in. The captain's own app is told by the server, not
       * from here — the push and the route recompute hang off the write.
       */
      void queryClient.invalidateQueries({ queryKey: queryKeys.orders.detail(order.uuid) });
      void queryClient.invalidateQueries({ queryKey: ["orders"] });
      void queryClient.invalidateQueries({ queryKey: ["tracking"] });
    },
    onError: (error) => {
      /*
       * A refused field belongs under that field. Whether this part of the order may still be
       * edited at all is not a field — it can be collected while the dialog is open — so that
       * answer stays a toast.
       */
      const shown = applyFieldErrors(error, (field, message) =>
        form.setError(field as never, { message }),
      );
      if (shown === 0) toast.error(friendlyErrorMessage(error, t));
    },
  });

  /*
   * `watch()` re-renders on every keystroke, which is what keeps the button in step; the values
   * come from `getValues()` because the bare `watch()` overload is untyped.
   *
   * The reason does not count as a change. It travels with an edit rather than being one, and a
   * button enabled by it alone would send a payload the server reads as asking for nothing.
   */
  form.watch();
  const nothingChanged =
    Object.keys(changedOnly(form.getValues())).filter((field) => field !== "reason").length === 0;

  const itemErrors = form.formState.errors.items;

  return (
    <>
      <DialogHeader>
        <DialogTitle>{t("orders.edit.title", { order: order.order_number ?? "" })}</DialogTitle>
        <DialogDescription>{t("orders.edit.body")}</DialogDescription>
      </DialogHeader>

      {locks.reasonRequired ? <Alert variant="warning">{t("orders.edit.inTransit")}</Alert> : null}

      <form
        className="space-y-6"
        onSubmit={form.handleSubmit((values) => {
          if (save.isPending) return;
          save.mutate(values);
        })}
      >
        <section className="space-y-4">
          <h3 className="text-sm font-semibold">{t("orders.edit.sectionCustomer")}</h3>
          <div className="grid gap-4 sm:grid-cols-2">
            <FormField
              label={t("orders.customerName")}
              htmlFor="edit-customer-name"
              required
              error={form.formState.errors.customer_name?.message}
            >
              <Input id="edit-customer-name" kind="letters" {...form.register("customer_name")} />
            </FormField>
            <FormField
              label={t("orders.customerPhone")}
              htmlFor="edit-customer-phone"
              error={form.formState.errors.customer_phone?.message}
            >
              <PhoneField
                id="edit-customer-phone"
                value={form.watch("customer_phone")}
                onChange={(value) => form.setValue("customer_phone", value, { shouldValidate: true })}
                invalid={form.formState.errors.customer_phone !== undefined}
              />
            </FormField>
          </div>
        </section>

        <section className="space-y-4">
          <h3 className="text-sm font-semibold">{t("orders.edit.sectionPickup")}</h3>

          {locks.pickup ? (
            <p className="text-sm text-muted-foreground">{t("orders.edit.pickupLocked")}</p>
          ) : (
            <>
              <div className="grid gap-4 sm:grid-cols-2">
                <FormField
                  label={t("orders.edit.pickupName")}
                  htmlFor="edit-pickup-name"
                  error={form.formState.errors.pickup_name?.message}
                >
                  <Input id="edit-pickup-name" {...form.register("pickup_name")} />
                </FormField>
                <FormField
                  label={t("orders.pickupAddress")}
                  htmlFor="edit-pickup-address"
                  required
                  error={form.formState.errors.pickup_address?.message}
                >
                  <Input id="edit-pickup-address" {...form.register("pickup_address")} />
                </FormField>
              </div>
              <div className="grid gap-4 sm:grid-cols-2">
                <FormField
                  label={t("orders.pickupLat")}
                  htmlFor="edit-pickup-lat"
                  error={form.formState.errors.pickup_lat?.message}
                >
                  <Input id="edit-pickup-lat" dir="ltr" inputMode="decimal" {...form.register("pickup_lat")} />
                </FormField>
                <FormField
                  label={t("orders.pickupLng")}
                  htmlFor="edit-pickup-lng"
                  error={form.formState.errors.pickup_lng?.message}
                >
                  <Input id="edit-pickup-lng" dir="ltr" inputMode="decimal" {...form.register("pickup_lng")} />
                </FormField>
              </div>
            </>
          )}
        </section>

        <section className="space-y-4">
          <h3 className="text-sm font-semibold">{t("orders.edit.sectionDropoff")}</h3>

          <FormField
            label={t("orders.dropoffAddress")}
            htmlFor="edit-dropoff-address"
            required
            error={form.formState.errors.dropoff_address?.message}
            hint={t("orders.edit.dropoffHint")}
          >
            <Input id="edit-dropoff-address" {...form.register("dropoff_address")} />
          </FormField>

          <div className="grid gap-4 sm:grid-cols-2">
            <FormField
              label={t("orders.dropoffLat")}
              htmlFor="edit-dropoff-lat"
              error={form.formState.errors.dropoff_lat?.message}
            >
              <Input id="edit-dropoff-lat" dir="ltr" inputMode="decimal" {...form.register("dropoff_lat")} />
            </FormField>
            <FormField
              label={t("orders.dropoffLng")}
              htmlFor="edit-dropoff-lng"
              error={form.formState.errors.dropoff_lng?.message}
            >
              <Input id="edit-dropoff-lng" dir="ltr" inputMode="decimal" {...form.register("dropoff_lng")} />
            </FormField>
          </div>
        </section>

        <section className="space-y-4">
          <div className="flex items-center justify-between gap-3">
            <h3 className="text-sm font-semibold">{t("orders.edit.sectionItems")}</h3>
            {itemsEditable ? (
              <Button
                type="button"
                variant="outline"
                size="sm"
                onClick={() => items.append({ name: "", quantity: 1, unit_price: "", sku: "" })}
              >
                <Plus className="h-4 w-4" aria-hidden />
                {t("orders.addItem")}
              </Button>
            ) : null}
          </div>

          {locks.items ? (
            <p className="text-sm text-muted-foreground">{t("orders.edit.itemsLocked")}</p>
          ) : hasCancelledLine ? (
            <p className="text-sm text-muted-foreground">{t("orders.edit.itemsHaveCancelledLine")}</p>
          ) : (
            <>
              <p className="text-sm text-muted-foreground">{t("orders.edit.itemsHint")}</p>

              {items.fields.length === 0 ? (
                <p className="text-sm text-muted-foreground">{t("orders.noItemsHere")}</p>
              ) : null}

              <div className="space-y-3">
                {items.fields.map((field, index) => (
                  <div
                    key={field.id}
                    className="grid gap-3 rounded-lg border bg-muted/20 p-3 sm:grid-cols-[1fr_5rem_7rem_8rem_2.5rem]"
                  >
                    <FormField
                      label={t("orders.item")}
                      htmlFor={`edit-item-${index}-name`}
                      error={itemErrors?.[index]?.name?.message}
                    >
                      <Input id={`edit-item-${index}-name`} {...form.register(`items.${index}.name`)} />
                    </FormField>
                    <FormField
                      label={t("orders.quantity")}
                      htmlFor={`edit-item-${index}-quantity`}
                      error={itemErrors?.[index]?.quantity?.message}
                    >
                      <Input
                        id={`edit-item-${index}-quantity`}
                        type="number"
                        min={1}
                        max={999}
                        dir="ltr"
                        {...form.register(`items.${index}.quantity`)}
                      />
                    </FormField>
                    <FormField
                      label={t("orders.unitPrice")}
                      htmlFor={`edit-item-${index}-unit-price`}
                      error={itemErrors?.[index]?.unit_price?.message}
                    >
                      <Input
                        id={`edit-item-${index}-unit-price`}
                        dir="ltr"
                        inputMode="decimal"
                        {...form.register(`items.${index}.unit_price`)}
                      />
                    </FormField>
                    <FormField
                      label={t("orders.itemSku")}
                      htmlFor={`edit-item-${index}-sku`}
                      error={itemErrors?.[index]?.sku?.message}
                    >
                      <Input id={`edit-item-${index}-sku`} {...form.register(`items.${index}.sku`)} />
                    </FormField>
                    <span className="flex items-end justify-end">
                      <Button
                        type="button"
                        variant="ghost"
                        size="icon"
                        className="text-muted-foreground hover:text-destructive"
                        onClick={() => items.remove(index)}
                        aria-label={t("orders.removeItem")}
                      >
                        <Trash2 className="h-4 w-4" aria-hidden />
                      </Button>
                    </span>
                  </div>
                ))}
              </div>
            </>
          )}
        </section>

        <section className="space-y-4">
          <h3 className="text-sm font-semibold">{t("orders.edit.sectionMoney")}</h3>

          <FormField
            label={t("orders.amountToCollect")}
            htmlFor="edit-amount"
            error={form.formState.errors.amount_to_collect?.message}
            hint={t("orders.edit.amountHint")}
          >
            <Input id="edit-amount" dir="ltr" inputMode="decimal" {...form.register("amount_to_collect")} />
          </FormField>

          <div className="grid gap-4 sm:grid-cols-2">
            <FormField
              label={t("orders.customerNote")}
              htmlFor="edit-customer-note"
              error={form.formState.errors.customer_note?.message}
            >
              <Textarea id="edit-customer-note" rows={2} {...form.register("customer_note")} />
            </FormField>
            <FormField
              label={t("orders.internalNote")}
              htmlFor="edit-note"
              error={form.formState.errors.note?.message}
            >
              <Textarea id="edit-note" rows={2} {...form.register("note")} />
            </FormField>
          </div>
        </section>

        {/* Only once the goods are moving. On a pending order it would collect the word "update". */}
        {locks.reasonRequired ? (
          <FormField
            label={t("orders.edit.reason")}
            htmlFor="edit-reason"
            required
            error={form.formState.errors.reason?.message}
            hint={t("orders.edit.reasonHint")}
          >
            <Textarea
              id="edit-reason"
              rows={2}
              maxLength={REASON_MAX}
              placeholder={t("orders.edit.reasonPlaceholder")}
              {...form.register("reason")}
            />
          </FormField>
        ) : null}

        <DialogFooter>
          <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
            {t("common.cancel")}
          </Button>
          <Button type="submit" loading={save.isPending} disabled={nothingChanged}>
            {t("common.save")}
          </Button>
        </DialogFooter>
      </form>
    </>
  );
}

/** The basket as it would be sent, so "did anything move?" compares like with like. */
function asPayloadItems(rows: ItemValues[]): UpdateOrderItemInput[] {
  return rows.map((line) => ({
    name: line.name,
    quantity: Number(line.quantity),
    unit_price: line.unit_price === "" ? null : Number(line.unit_price),
    sku: line.sku === "" ? null : line.sku,
  }));
}
