"use client";

import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { RotateCcw, SlidersHorizontal } from "lucide-react";
import { Alert } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { useCan } from "@/features/auth/use-can";
import { useI18n, useT } from "@/i18n";
import { friendlyErrorMessage } from "@/lib/errors";
import { useFieldErrors } from "@/lib/use-field-errors";
import { formatDateTime } from "@/lib/format";
import { queryKeys } from "@/lib/query-keys";
import {
  getDispatchSettings,
  updateDispatchSettings,
} from "@/services/dispatch";
import type { DispatchSettingRow } from "@/types/dispatch";

/** A pending edit per setting key, as typed — kept as text so a half-typed number is not a zero. */
type Draft = Record<string, string>;

/**
 * What the dispatch algorithm is running on, and the handful of values a dispatcher may change.
 *
 * **Two lists, and the difference matters.** `editable` is what this screen can write; `effective`
 * is every value in force, most of them fixed in configuration and shown read-only. Hiding the
 * second list would leave somebody tuning three numbers with no idea what the other twenty are.
 *
 * Only changed fields are sent, and **resetting a setting sends `null`** rather than re-typing the
 * default — an explicit reset clears the override, while writing the same number as the default
 * would leave the setting pinned to it.
 */
export function DispatchSettingsScreen() {
  const t = useT();
  const { locale } = useI18n();
  const { can } = useCan();
  const queryClient = useQueryClient();
  const canEdit = can("dispatch.settings");

  const query = useQuery({
    queryKey: queryKeys.dispatch.settings,
    queryFn: async () => (await getDispatchSettings()).Model ?? null,
  });

  const settings = query.data ?? null;
  const [draft, setDraft] = useState<Draft>({});

  // The server bounds each value by its own key, so a refusal is shown on that row — not as one
  // toast for a form holding several numbers. The owned names are whatever the server says is
  // editable, plus `settings`, which is its name for "you sent nothing at all".
  const fields = useFieldErrors({
    only: [...(settings?.editable ?? []).map((row) => row.key), "settings"],
  });

  const save = useMutation({
    mutationFn: (values: Record<string, number | null>) =>
      updateDispatchSettings(values),
    onSuccess: (response) => {
      toast.success(response.Message ?? t("dispatchSettings.saved"));
      setDraft({});
      void queryClient.invalidateQueries({
        queryKey: queryKeys.dispatch.settings,
      });
      // Every suggestion and every route is priced with these numbers; the KPI window that follows
      // is measuring a different algorithm than the one before it.
      void queryClient.invalidateQueries({ queryKey: ["dispatch", "kpis"] });
    },
    onMutate: fields.reset,
    onError: (error) => {
      // A refusal this screen cannot place — a permission, a 409 — still has to be said out loud.
      if (!fields.capture(error)) toast.error(friendlyErrorMessage(error, t));
    },
  });

  const rows = settings?.editable ?? [];

  /** The changed fields only, refused rather than sent when one of them is not a number. */
  const changed = (): Record<string, number | null> | "invalid" => {
    const values: Record<string, number | null> = {};

    for (const row of rows) {
      const typed = draft[row.key];
      if (typed === undefined) continue;
      if (typed.trim() === "") return "invalid";

      const value = Number(typed);
      if (!Number.isFinite(value) || value < row.min || value > row.max)
        return "invalid";
      if (value === row.value) continue;

      values[row.key] = value;
    }

    return values;
  };

  const pending = changed();
  const dirty = pending !== "invalid" && Object.keys(pending).length > 0;

  return (
    <div className="space-y-6">
      {query.isError ? (
        <Alert variant="destructive" title={t("dispatchSettings.loadFailed")}>
          {friendlyErrorMessage(query.error, t)}
        </Alert>
      ) : null}

      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <SlidersHorizontal className="h-4 w-4 text-primary" aria-hidden />
            {t("dispatchSettings.editableTitle")}
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <p className="text-sm text-muted-foreground">
            {t("dispatchSettings.editableHint")}
          </p>

          {query.isPending ? (
            <div className="space-y-3">
              <Skeleton className="h-16 w-full" />
              <Skeleton className="h-16 w-full" />
              <Skeleton className="h-16 w-full" />
            </div>
          ) : (
            <div className="space-y-4">
              {rows.map((row) => (
                <SettingField
                  key={row.key}
                  row={row}
                  value={draft[row.key] ?? String(row.value)}
                  disabled={!canEdit || save.isPending}
                  locale={locale}
                  // The server refuses each value under its own key, which is the row's key too.
                  serverError={fields.get(row.key)}
                  onChange={(next) =>
                    setDraft((current) => ({ ...current, [row.key]: next }))
                  }
                  onReset={
                    canEdit && row.overridden
                      ? () => save.mutate({ [row.key]: null })
                      : undefined
                  }
                />
              ))}
            </div>
          )}

          {canEdit ? (
            <div className="flex flex-wrap items-center gap-2">
              <Button
                type="button"
                disabled={!dirty || save.isPending}
                onClick={() => {
                  if (pending === "invalid") return;
                  save.mutate(pending);
                }}
              >
                {save.isPending ? t("common.saving") : t("common.save")}
              </Button>
              <Button
                type="button"
                variant="outline"
                disabled={Object.keys(draft).length === 0 || save.isPending}
                onClick={() => setDraft({})}
              >
                {t("common.clear")}
              </Button>
              {pending === "invalid" ? (
                <p className="text-xs text-destructive">
                  {t("dispatchSettings.outOfRange")}
                </p>
              ) : fields.get("settings") ? (
                // The server's name for "you sent nothing at all", which belongs to the form as a
                // whole rather than to any one row.
                <p className="text-xs text-destructive">
                  {fields.get("settings")}
                </p>
              ) : (
                <p className="text-xs text-muted-foreground">
                  {t("dispatchSettings.saveHint")}
                </p>
              )}
            </div>
          ) : (
            <Alert variant="info" title={t("dispatchSettings.readOnlyTitle")}>
              {t("dispatchSettings.readOnlyBody")}
            </Alert>
          )}
        </CardContent>
      </Card>

      <Card>
        <CardHeader>
          <CardTitle>{t("dispatchSettings.effectiveTitle")}</CardTitle>
        </CardHeader>
        <CardContent>
          <p className="mb-4 text-sm text-muted-foreground">
            {t("dispatchSettings.effectiveHint")}
          </p>
          <div className="overflow-x-auto">
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>{t("dispatchSettings.setting")}</TableHead>
                  <TableHead>{t("dispatchSettings.inForce")}</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {Object.entries(settings?.effective ?? {}).map(
                  ([key, value]) => (
                    <TableRow key={key}>
                      {/* The key as the API names it: this table is read beside the backend
                        configuration and a prettified label would break that match. */}
                      <TableCell className="font-mono text-xs" dir="ltr">
                        {key}
                      </TableCell>
                      <TableCell className="numeric text-sm" dir="ltr">
                        {Array.isArray(value)
                          ? value.join(", ")
                          : String(value)}
                      </TableCell>
                    </TableRow>
                  ),
                )}
              </TableBody>
            </Table>
          </div>
        </CardContent>
      </Card>
    </div>
  );
}

function SettingField({
  row,
  value,
  disabled,
  locale,
  serverError,
  onChange,
  onReset,
}: {
  row: DispatchSettingRow;
  value: string;
  disabled: boolean;
  locale: "en" | "ar";
  serverError?: string;
  onChange: (value: string) => void;
  onReset?: () => void;
}) {
  const t = useT();

  return (
    <div className="rounded-lg border p-4">
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div className="min-w-0">
          {/* The label is the server's, already translated by `Accept-Language`. */}
          <Label htmlFor={`setting-${row.key}`}>{row.label}</Label>
          <p
            className="mt-0.5 font-mono text-[0.7rem] text-muted-foreground"
            dir="ltr"
          >
            {row.key}
          </p>
        </div>
        {row.overridden ? (
          <Badge variant="warning">{t("dispatchSettings.overridden")}</Badge>
        ) : (
          <Badge variant="secondary">{t("dispatchSettings.atDefault")}</Badge>
        )}
      </div>

      <div className="mt-3 flex flex-wrap items-center gap-2">
        <Input
          id={`setting-${row.key}`}
          type="number"
          className="max-w-[10rem]"
          value={value}
          min={row.min}
          max={row.max}
          step="any"
          disabled={disabled}
          dir="ltr"
          aria-invalid={serverError !== undefined}
          onChange={(event) => onChange(event.target.value)}
        />
        <span className="text-xs text-muted-foreground" dir="ltr">
          {t("dispatchSettings.bounds", {
            min: String(row.min),
            max: String(row.max),
            default: String(row.default),
          })}
        </span>
        {onReset ? (
          <Button
            type="button"
            variant="outline"
            size="sm"
            className="gap-1.5"
            onClick={onReset}
          >
            <RotateCcw className="h-3.5 w-3.5" aria-hidden />
            {t("dispatchSettings.reset")}
          </Button>
        ) : null}
      </div>

      {serverError ? (
        <p className="mt-2 text-xs text-destructive">{serverError}</p>
      ) : null}

      {row.updated_at ? (
        <p className="mt-2 text-xs text-muted-foreground">
          {t("dispatchSettings.changedBy", {
            who: row.updated_by ?? t("common.notAvailable"),
            when: formatDateTime(row.updated_at, locale),
          })}
        </p>
      ) : null}
    </div>
  );
}
