"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 { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useI18n, useT } from "@/i18n";
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>
        {/* 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 ?? "");
  const [lat, setLat] = useState(editing ? String(editing.lat) : "");
  const [lng, setLng] = useState(editing ? String(editing.lng) : "");
  const [radius, setRadius] = useState(editing ? 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) : "",
  );
  const [overwriteChildren, setOverwriteChildren] = useState(false);

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

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

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

  /** 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(),
          lat: numeric(lat),
          lng: numeric(lng),
          radius_m: numeric(radius),
          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) }),
          ...(atStake && rateChanged ? { overwrite_own_children: overwriteChildren } : {}),
        });
      }

      return createCity({
        name: name.trim(),
        lat: numeric(lat),
        lng: numeric(lng),
        radius_m: numeric(radius),
        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) }),
      });
    },
    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-4"
        onSubmit={(event) => {
          event.preventDefault();
          if (invalid || save.isPending) return;
          save.mutate();
        }}
      >
        <div className="space-y-1.5">
          <Label htmlFor="city-name">{t("cities.name")}</Label>
          <Input
            id="city-name"
            value={name}
            maxLength={120}
            onChange={(event) => setName(event.target.value)}
            aria-invalid={fields.get("name") !== undefined}
            autoFocus
          />
          {fields.get("name") ? (
            <p className="text-xs text-destructive">{fields.get("name")}</p>
          ) : null}
        </div>

        <div className="grid grid-cols-2 gap-3">
          <div className="space-y-1.5">
            <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}
            />
            {fields.get("lat") ? (
              <p className="text-xs text-destructive">{fields.get("lat")}</p>
            ) : null}
          </div>
          <div className="space-y-1.5">
            <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}
            />
            {fields.get("lng") ? (
              <p className="text-xs text-destructive">{fields.get("lng")}</p>
            ) : null}
          </div>
        </div>

        <div className="space-y-1.5">
          <Label htmlFor="city-radius">{t("cities.radius")}</Label>
          <Input
            id="city-radius"
            type="number"
            min="100"
            max="100000"
            step="50"
            value={radius}
            onChange={(event) => setRadius(event.target.value)}
            aria-invalid={fields.get("radius_m") !== undefined}
          />
          {fields.get("radius_m") ? (
            <p className="text-xs text-destructive">{fields.get("radius_m")}</p>
          ) : (
            // Said plainly, because the overlap rule is not guessable: the tightest area wins.
            <p className="text-xs text-muted-foreground">{t("cities.radiusHint")}</p>
          )}
        </div>

        {isDistrict ? (
          <label className="flex items-start gap-2 rounded-md border p-3">
            <Checkbox
              checked={inherit}
              onCheckedChange={(checked) => setInherit(checked === true)}
              aria-label={t("cities.inherit")}
            />
            <span className="text-sm">
              <span className="font-medium">{t("cities.inherit")}</span>
              <span className="mt-0.5 block text-xs text-muted-foreground">
                {parent
                  ? t("cities.inheritHint", {
                      name: parent.name,
                      amount: formatCurrency(parent.delivery_earning, currency, locale),
                    })
                  : t("cities.inheritHintPlain")}
              </span>
            </span>
          </label>
        ) : null}

        {!inherit ? (
          <div className="space-y-1.5">
            <Label htmlFor="city-earning">
              {t("cities.earning")} ({currency})
            </Label>
            <Input
              id="city-earning"
              type="number"
              min="0"
              step="100"
              value={earning}
              onChange={(event) => setEarning(event.target.value)}
              aria-invalid={fields.get("delivery_earning") !== undefined}
            />
            {fields.get("delivery_earning") ? (
              <p className="text-xs text-destructive">{fields.get("delivery_earning")}</p>
            ) : (
              <p className="text-xs text-muted-foreground">{t("cities.earningHint")}</p>
            )}
          </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-2 rounded-md border border-warning/40 bg-warning/5 p-3">
            <Checkbox
              checked={overwriteChildren}
              onCheckedChange={(checked) => setOverwriteChildren(checked === true)}
              aria-label={t("cities.overwriteChildren")}
            />
            <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-0.5 block text-xs 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 rounded-md border p-3">
          <span className="text-sm">
            <span className="font-medium">{t("cities.active")}</span>
            <span className="mt-0.5 block text-xs text-muted-foreground">
              {t("cities.activeHint")}
            </span>
          </span>
          <Switch checked={active} onCheckedChange={setActive} aria-label={t("cities.active")} />
        </div>

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