"use client";

import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { FormField } from "@/components/ui/form-field";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useI18n, useT } from "@/i18n";
import { LocationPicker } from "@/features/orders/location-picker";
import { friendlyErrorMessage } from "@/lib/errors";
import { formatCurrency, formatNumber } from "@/lib/format";
import { queryKeys } from "@/lib/query-keys";
import { useFieldErrors } from "@/lib/use-field-errors";
import { createCity, updateCity } from "@/services/cities";
import type { ApiResponse } from "@/types/api";
import type { CascadeResult, City, CityUpdateResponse } from "@/types/city";

type Mode =
  /** A city with no parent. */
  | { kind: "city" }
  /** A district inside this city. */
  | { kind: "district"; parent: City }
  /** An existing area, city or district. */
  | { kind: "edit"; city: City; parent: City | null };

interface CityDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  mode: Mode;
  currency: string;
}

/**
 * Add or change a delivery area.
 *
 * **The rate field is the whole reason this dialog is careful.** Three things can happen to the money
 * behind it, and each is said in words before the button is pressed: a district left blank follows its
 * city, a district given a figure stops following it, and a **city's** change carries every district
 * that follows — overwriting the ones that do not only when the operator ticks the box that names how
 * many they are.
 */
export function CityDialog(props: CityDialogProps) {
  return (
    <Dialog open={props.open} onOpenChange={props.onOpenChange}>
      <DialogContent className="max-w-xl">
        {/* Its own component, so the fields are born with this area's figures rather than reset to
            them: the dialog unmounts its content on close, and a half-typed radius must never be
            carried to the next district somebody opens. */}
        <CityForm {...props} />
      </DialogContent>
    </Dialog>
  );
}

/**
 * The cascade counts, when the answer carries them.
 *
 * Only an update reports what happened to the districts below; a creation has nothing below it yet.
 * Written as a guard rather than an `in` check because the union of the two responses widens to `{}`
 * on a property access, and a cast would hide the day one of them changes shape.
 */
function cascadeOf(response: ApiResponse<City> | CityUpdateResponse): CascadeResult | null {
  return "Districts" in response ? response.Districts : null;
}

function CityForm({ onOpenChange, mode, currency }: CityDialogProps) {
  const t = useT();
  const { locale } = useI18n();
  const queryClient = useQueryClient();

  const editing = mode.kind === "edit" ? mode.city : null;
  const parent = mode.kind === "district" ? mode.parent : mode.kind === "edit" ? mode.parent : null;
  const isDistrict = mode.kind === "district" || (editing?.is_district ?? false);

  const [name, setName] = useState(editing?.name ?? "");
  // An imported area may have no place yet: its boxes start empty, not as "null".
  const [lat, setLat] = useState(editing?.lat != null ? String(editing.lat) : "");
  const [lng, setLng] = useState(editing?.lng != null ? String(editing.lng) : "");
  const [radius, setRadius] = useState(editing?.radius_m != null ? String(editing.radius_m) : "1000");
  const [active, setActive] = useState(editing?.is_active ?? true);

  // A district that follows its city starts with the box ticked and the field hidden, which is the
  // ordinary case; typing a rate is how somebody says this district is different.
  const [inherit, setInherit] = useState(
    isDistrict && (editing === null || editing.earning_source === "inherited"),
  );
  const [earning, setEarning] = useState(
    editing ? String(editing.delivery_earning) : parent ? String(parent.delivery_earning) : "",
  );
  /*
   * Held as a percentage because that is how the decision is spoken — "the captain keeps 80"
   * — and sent as a fraction because that is what the API stores. Converting at the edge keeps
   * the 0.8-vs-80 confusion out of everything between.
   */
  const [share, setShare] = useState(
    editing
      ? String(Math.round(editing.freelance_share * 10000) / 100)
      : parent
        ? String(Math.round(parent.freelance_share * 10000) / 100)
        : "100",
  );
  const [overwriteChildren, setOverwriteChildren] = useState(false);

  const fields = useFieldErrors({
    only: ["name", "parent_uuid", "lat", "lng", "radius_m", "delivery_earning", "freelance_share", "inherit"],
  });

  const numeric = (value: string) => (value.trim() === "" ? Number.NaN : Number(value));

  /*
   * The place is optional, and whole or not at all: a centre chosen means a radius too. Without one the
   * area is priced and listed, but no delivery is matched to it - the state an imported area arrives in.
   */
  const located = lat.trim() !== "" || lng.trim() !== "";
  const place = located ? { lat: numeric(lat), lng: numeric(lng), radius_m: numeric(radius) } : {};

  const invalid =
    name.trim() === "" ||
    (located &&
      (Number.isNaN(numeric(lat)) ||
        Number.isNaN(numeric(lng)) ||
        Number.isNaN(numeric(radius)) ||
        numeric(radius) < 100)) ||
    (!inherit && (Number.isNaN(numeric(earning)) || numeric(earning) < 0)) ||
    (!inherit && (Number.isNaN(numeric(share)) || numeric(share) < 0 || numeric(share) > 100));

  /** A city whose rate is changing, with districts that were priced by hand. */
  const atStake =
    editing !== null && !editing.is_district && (editing.districts_with_own_earning ?? 0) > 0;
  const rateChanged = editing !== null && numeric(earning) !== editing.delivery_earning;

  const save = useMutation({
    mutationFn: () => {
      if (editing !== null) {
        return updateCity(editing.uuid, {
          name: name.trim(),
          ...place,
          is_active: active,
          // The two are mutually exclusive and the server refuses them together, so only one is
          // ever sent.
          ...(inherit && editing.is_district
            ? { inherit: true }
            : { delivery_earning: numeric(earning), freelance_share: numeric(share) / 100 }),
          ...(atStake && rateChanged ? { overwrite_own_children: overwriteChildren } : {}),
        });
      }

      return createCity({
        name: name.trim(),
        ...place,
        is_active: active,
        ...(parent ? { parent_uuid: parent.uuid } : {}),
        // Omitted entirely when a district is to follow its city: sending the city's figure would
        // record it as the district's own and silently stop it following.
        ...(inherit && parent
          ? {}
          : { delivery_earning: numeric(earning), freelance_share: numeric(share) / 100 }),
      });
    },
    onMutate: fields.reset,
    onSuccess: (response) => {
      // Say what the change did below it, not "saved": a rate change may have moved nine districts.
      const cascade = cascadeOf(response);

      if (cascade && cascade.followed + cascade.overwritten + cascade.kept > 0) {
        toast.success(
          t("cities.cascadeResult", {
            followed: formatNumber(cascade.followed, locale),
            overwritten: formatNumber(cascade.overwritten, locale),
            kept: formatNumber(cascade.kept, locale),
          }),
        );
      } else {
        toast.success(response.Message ?? t("cities.saved"));
      }

      onOpenChange(false);
      void queryClient.invalidateQueries({ queryKey: queryKeys.cities.all });
    },
    onError: (err) => {
      if (!fields.capture(err)) toast.error(friendlyErrorMessage(err, t));
    },
  });

  const title = editing
    ? t("cities.editTitle")
    : isDistrict
      ? t("cities.addDistrictTitle")
      : t("cities.addCityTitle");

  return (
    <>
      <DialogHeader>
        <DialogTitle>{title}</DialogTitle>
        <DialogDescription>
          {parent ? t("cities.insideCity", { name: parent.name }) : t("cities.cityBody")}
        </DialogDescription>
      </DialogHeader>

      <form
        className="space-y-5"
        onSubmit={(event) => {
          event.preventDefault();
          if (invalid || save.isPending) return;
          save.mutate();
        }}
      >
        {/* Where the area is: its name, its centre on the map, and how far it reaches. */}
        <FormField label={t("cities.name")} htmlFor="city-name" required error={fields.get("name")}>
          <Input
            id="city-name"
            value={name}
            maxLength={120}
            onChange={(event) => setName(event.target.value)}
            aria-invalid={fields.get("name") !== undefined}
            autoFocus
          />
        </FormField>

        {/* The centre of the area, chosen on the map (typed boxes when there is no Maps key). The circle on the map is the radius below. */}
        <FormField
          label={t("cities.centre")}
          error={fields.get("lat") ?? fields.get("lng")}
          hint={located ? undefined : t("cities.locationOptional")}
        >
          <LocationPicker
            lat={lat}
            lng={lng}
            radiusM={Number.isFinite(numeric(radius)) && numeric(radius) > 0 ? numeric(radius) : null}
            title={t("cities.chooseCentre")}
            clearable={false}
            onChange={(nextLat, nextLng) => {
              setLat(nextLat === null ? "" : String(nextLat));
              setLng(nextLng === null ? "" : String(nextLng));
            }}
            fallback={
              <>
                <div className="space-y-2">
                  <Label htmlFor="city-lat">{t("cities.lat")}</Label>
                  <Input id="city-lat" type="number" step="0.0000001" value={lat} onChange={(event) => setLat(event.target.value)} aria-invalid={fields.get("lat") !== undefined} />
                </div>
                <div className="space-y-2">
                  <Label htmlFor="city-lng">{t("cities.lng")}</Label>
                  <Input id="city-lng" type="number" step="0.0000001" value={lng} onChange={(event) => setLng(event.target.value)} aria-invalid={fields.get("lng") !== undefined} />
                </div>
              </>
            }
          />
        </FormField>

        <FormField
          label={t("cities.radius")}
          htmlFor="city-radius"
          required={located}
          error={fields.get("radius_m")}
          // Said plainly, because the overlap rule is not guessable: the tightest area wins.
          hint={t("cities.radiusHint")}
        >
          <div className="flex items-center gap-2">
            <Input
              id="city-radius"
              type="number"
              min="100"
              max="100000"
              step="1"
              className="numeric"
              value={radius}
              onChange={(event) => setRadius(event.target.value)}
              aria-invalid={fields.get("radius_m") !== undefined}
            />
            <span className="w-10 shrink-0 text-sm text-muted-foreground" dir="ltr">
              m
            </span>
          </div>
        </FormField>

        {/* What a delivery here pays: the rate, who keeps how much of it, and whether the area is in use. */}
        <div className="space-y-5 border-t pt-5">
          {isDistrict ? (
            <label className="flex items-start gap-3 rounded-lg border p-4">
              <Checkbox
                checked={inherit}
                onCheckedChange={(checked) => setInherit(checked === true)}
                aria-label={t("cities.inherit")}
                className="mt-0.5"
              />
              <span className="text-sm">
                <span className="font-medium">{t("cities.inherit")}</span>
                <span className="mt-1 block text-xs leading-5 text-muted-foreground">
                  {parent
                    ? t("cities.inheritHint", {
                        name: parent.name,
                        amount: formatCurrency(parent.delivery_earning, currency, locale),
                      })
                    : t("cities.inheritHintPlain")}
                </span>
              </span>
            </label>
          ) : null}

          {/*
            The rate and what the freelancer keeps of it sit side by side because they are one decision: the rate is what the trip is
            worth, the share is who gets how much of it, and reading them apart invites setting one and forgetting the other.
          */}
          {!inherit ? (
            <div className="grid items-start gap-5 sm:grid-cols-2">
              <FormField
                label={t("cities.earning")}
                htmlFor="city-earning"
                required
                error={fields.get("delivery_earning")}
                hint={t("cities.earningHint")}
              >
                <div className="flex items-center gap-2">
                  <Input
                    id="city-earning"
                    type="number"
                    min="0"
                    step="any"
                    className="numeric"
                    value={earning}
                    onChange={(event) => setEarning(event.target.value)}
                    aria-invalid={fields.get("delivery_earning") !== undefined}
                  />
                  <span className="w-10 shrink-0 text-sm text-muted-foreground" dir="ltr">
                    {currency}
                  </span>
                </div>
              </FormField>

              <FormField
                label={t("cities.freelanceShare")}
                htmlFor="city-share"
                required
                error={fields.get("freelance_share")}
                hint={t("cities.freelanceShareHint", {
                  amount: formatCurrency(
                    Number.isNaN(numeric(earning)) || Number.isNaN(numeric(share))
                      ? 0
                      : (numeric(earning) * numeric(share)) / 100,
                    currency,
                    locale,
                  ),
                })}
              >
                <div className="flex items-center gap-2">
                  <Input
                    id="city-share"
                    type="number"
                    min="0"
                    max="100"
                    step="any"
                    className="numeric"
                    value={share}
                    onChange={(event) => setShare(event.target.value)}
                    aria-invalid={fields.get("freelance_share") !== undefined}
                  />
                  <span className="w-10 shrink-0 text-sm text-muted-foreground" dir="ltr">
                    %
                  </span>
                </div>
              </FormField>
            </div>
          ) : null}

          {/*
            The cascade, asked rather than assumed. It appears only when this city's rate is actually
            changing and only when there are districts with a rate of their own — and it names how many,
            because "overwrite children" means nothing to somebody who does not know there are two.
          */}
          {atStake && rateChanged ? (
            <label className="flex items-start gap-3 rounded-lg border border-warning/40 bg-warning/5 p-4">
              <Checkbox
                checked={overwriteChildren}
                onCheckedChange={(checked) => setOverwriteChildren(checked === true)}
                aria-label={t("cities.overwriteChildren")}
                className="mt-0.5"
              />
              <span className="text-sm">
                <span className="flex items-center gap-1.5 font-medium text-warning">
                  <AlertTriangle className="h-3.5 w-3.5" aria-hidden />
                  {t("cities.overwriteChildren")}
                </span>
                <span className="mt-1 block text-xs leading-5 text-muted-foreground">
                  {t("cities.overwriteChildrenHint", {
                    count: formatNumber(editing?.districts_with_own_earning ?? 0, locale),
                  })}
                </span>
              </span>
            </label>
          ) : null}

          <div className="flex items-center justify-between gap-4 rounded-lg border p-4">
            <span className="text-sm">
              <span className="font-medium">{t("cities.active")}</span>
              <span className="mt-1 block text-xs leading-5 text-muted-foreground">{t("cities.activeHint")}</span>
            </span>
            <Switch checked={active} onCheckedChange={setActive} aria-label={t("cities.active")} />
          </div>
        </div>

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