"use client";

import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { CheckCircle2, Clock, MapPin, Store, Users } from "lucide-react";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Alert } from "@/components/ui/alert";
import { StatusBadge } from "@/components/ui/badge";
import { EmptyState, ErrorState } from "@/components/shared/states";
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
import {
  assignOrder,
  assignOrderEnforced,
  getCaptainSuggestions,
  getOrder,
} from "@/services/orders";
import { queryKeys } from "@/lib/query-keys";
import { cn } from "@/lib/cn";
import { useI18n, useT } from "@/i18n";
import { useCan } from "@/features/auth/use-can";
import { friendlyErrorMessage } from "@/lib/errors";
import { formatDistanceKm, formatNumber } from "@/lib/format";
import type {
  BatchVerdict,
  CaptainDispatchState,
  GpsFreshness,
  SuggestionCandidate,
} from "@/types/dispatch";

type Variant = "secondary" | "success" | "warning" | "destructive" | "info";

const STATE_VARIANT: Record<CaptainDispatchState, Variant> = {
  idle: "success",
  batchable: "info",
  full: "secondary",
  offline: "secondary",
  on_break: "warning",
  unapproved: "destructive",
};

/**
 * A stale fix is drawn as a warning, not hidden: the algorithm already discounts it, and the
 * dispatcher deserves to see *why* a captain who looks close sits lower than they expected.
 */
const GPS_VARIANT: Record<GpsFreshness, Variant> = {
  fresh: "success",
  aging: "warning",
  stale: "destructive",
  missing: "secondary",
};

const BATCH_VARIANT: Record<Exclude<BatchVerdict, "not_applicable">, Variant> = {
  accepted: "success",
  rejected_detour: "destructive",
  rejected_promise: "destructive",
};

/** States in which a captain cannot physically take an order, whatever the ranking says. */
const CANNOT_CARRY: CaptainDispatchState[] = ["full", "offline", "on_break", "unapproved"];

/**
 * Put an order in front of a captain.
 *
 * The list is the dispatch algorithm's, and it is only a suggestion — the dispatcher decides.
 * So every row shows the numbers that placed it: the adjusted ETA it is sorted on, whether the
 * captain is idle or already carrying something, how far their GPS can be trusted, and the
 * server's own sentences saying why. A dispatcher who overrides the first choice should be able
 * to see exactly what they are overriding.
 */
export function AssignOrderDialog({
  uuid,
  open,
  onOpenChange,
}: {
  uuid: string;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}) {
  const t = useT();
  const { locale } = useI18n();
  const queryClient = useQueryClient();

  const { can } = useCan();
  // Directing a captain is its own grant on the server; the button follows it rather than
  // being drawn for everybody and answering 403.
  const canEnforce = can("orders.assign_enforced");
  const [selected, setSelected] = useState<SuggestionCandidate | null>(null);
  const [assigning, setAssigning] = useState(false);

  const orderQuery = useQuery({
    queryKey: queryKeys.orders.detail(uuid),
    queryFn: () => getOrder(uuid),
    enabled: open,
  });

  const order = orderQuery.data?.Model ?? null;
  const hasPickupCoords = order?.pickup_lat != null && order?.pickup_lng != null;

  const captainsQuery = useQuery({
    queryKey: queryKeys.orders.captains(uuid),
    queryFn: () => getCaptainSuggestions(uuid),
    enabled: open && Boolean(hasPickupCoords),
  });

  const list = captainsQuery.data?.Model ?? null;
  const candidates = list?.candidates ?? [];

  async function handleAssign(enforced = false) {
    if (!selected || !list) return;
    setAssigning(true);
    try {
      // The list and the row it came from, so the dispatch KPIs can tell a first-choice pick
      // from an override.
      const source = { suggestion_uuid: list.suggestion_uuid, rank: selected.rank };
      const response = enforced
        ? await assignOrderEnforced(uuid, selected.captain.uuid, source)
        : await assignOrder(uuid, selected.captain.uuid, source);
      toast.success(response.Message || t("assign.success"));
      void queryClient.invalidateQueries({ queryKey: queryKeys.orders.detail(uuid) });
      void queryClient.invalidateQueries({ queryKey: queryKeys.orders.captains(uuid) });
      setSelected(null);
      setAssigning(false);
      onOpenChange(false);
    } catch (error) {
      // A busy captain on this list can still be refused by the assignment itself, and a list
      // is a snapshot — somebody else may have taken the captain since. The server's message
      // says which, and a fresh list shows where things stand now.
      toast.error(friendlyErrorMessage(error, t));
      setSelected(null);
      setAssigning(false);
      void captainsQuery.refetch();
    }
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-2xl">
        <DialogHeader>
          <DialogTitle>{t("assign.title")}</DialogTitle>
          <DialogDescription>
            {order ? `${order.order_number} — ${t("assign.subtitle")}` : t("assign.subtitle")}
          </DialogDescription>
        </DialogHeader>

        {order?.driver ? (
          <Alert variant="warning" title={t("orders.captain")} className="my-2">
            {order.driver.name}
          </Alert>
        ) : null}

        {orderQuery.isLoading ? (
          <ListSkeleton />
        ) : orderQuery.isError || !order ? (
          <ErrorState
            message={friendlyErrorMessage(orderQuery.error ?? new Error(t("common.notFound")), t)}
            onRetry={() => orderQuery.refetch()}
          />
        ) : !hasPickupCoords ? (
          <Alert variant="info" title={t("assign.unavailable")} className="mt-2" />
        ) : captainsQuery.isLoading ? (
          <ListSkeleton />
        ) : captainsQuery.isError || !list ? (
          <ErrorState
            message={friendlyErrorMessage(captainsQuery.error, t)}
            onRetry={() => captainsQuery.refetch()}
          />
        ) : (
          <div className="space-y-3">
            <p className="flex flex-wrap items-center gap-x-1.5 text-xs text-muted-foreground">
              <MapPin className="h-3.5 w-3.5 text-primary" aria-hidden />
              {t("assign.searchedWithin", { km: formatNumber(list.radius_km, locale) })}
              {list.search_expanded ? <span>· {t("assign.searchWidened")}</span> : null}
            </p>

            {/*
              The maps engine did not answer. The list is still usable — ranked on straight-line
              estimates rather than failing — but the times are guesses, and a dispatcher has to
              know that before trusting "3 min".
            */}
            {list.ranking_degraded ? (
              <Alert variant="warning" title={t("assign.degradedTitle")}>
                {list.degraded_reason_label ? `${list.degraded_reason_label}. ` : ""}
                {t("assign.degradedBody")}
              </Alert>
            ) : null}

            {list.excluded_stale.length > 0 ? (
              <p className="text-xs text-muted-foreground">
                {t("assign.staleHidden", { count: list.excluded_stale.length })}
              </p>
            ) : null}

            {candidates.length === 0 ? (
              // An empty list is an answer with a reason, never a blank screen.
              <EmptyState
                title={t("assign.empty")}
                description={list.no_candidates_reason ?? t("assign.emptyHint")}
                icon={Users}
              />
            ) : (
              <div className="flex max-h-[52vh] flex-col divide-y divide-border overflow-y-auto rounded-lg border border-border">
                {candidates.map((candidate) => (
                  <CandidateRow
                    key={candidate.captain.uuid}
                    candidate={candidate}
                    locale={locale}
                    onChoose={() => setSelected(candidate)}
                  />
                ))}
              </div>
            )}
          </div>
        )}

        <ConfirmDialog
          open={selected !== null}
          onOpenChange={(dialogOpen) => !dialogOpen && !assigning && setSelected(null)}
          title={
            selected
              ? t("assign.confirmTitle", {
                  order: order?.order_number ?? "",
                  captain: selected.captain.name,
                })
              : ""
          }
          description={selected ? t("assign.confirmBody") : undefined}
          confirmLabel={t("assign.choose")}
          loading={assigning}
          onConfirm={() => handleAssign()}
          /*
            Offered only where it can work: an employee captain, and an operator who holds the
            grant. A freelance captain's arrangement is that they may refuse, and the server
            refuses this for them — so the button is not drawn rather than drawn to fail.
          */
          extraAction={
            selected?.captain.employment_type === "employee" && canEnforce
              ? {
                  label: t("assign.enforce"),
                  hint: t("assign.enforceHint"),
                  onAct: () => handleAssign(true),
                }
              : undefined
          }
        />
      </DialogContent>
    </Dialog>
  );
}

function CandidateRow({
  candidate,
  locale,
  onChoose,
}: {
  candidate: SuggestionCandidate;
  locale: "en" | "ar";
  onChoose: () => void;
}) {
  const t = useT();
  const best = candidate.rank === 1;
  const cannotCarry = CANNOT_CARRY.includes(candidate.state);

  return (
    <div className={cn("flex gap-3 px-4 py-3", best && "bg-primary-soft/40")}>
      <span
        className={cn(
          "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold",
          best ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground",
        )}
        aria-label={t("assign.rank", { rank: candidate.rank })}
      >
        {candidate.rank}
      </span>

      <div className="min-w-0 flex-1">
        <div className="flex flex-wrap items-center gap-1.5">
          <p className="truncate text-sm font-semibold text-foreground">{candidate.captain.name}</p>
          {best ? (
            <span className="text-[0.7rem] font-medium text-primary">{t("assign.bestMatch")}</span>
          ) : null}
        </div>

        <div className="mt-1 flex flex-wrap items-center gap-1.5">
          <StatusBadge variant={STATE_VARIANT[candidate.state]}>{candidate.state_label}</StatusBadge>
          {/* Which arrangement they are on, because it decides whether this captain can be told to
              take the order or only asked. Said on the row, before the choice, not after it. */}
          <StatusBadge
            variant={candidate.captain.employment_type === "employee" ? "primary" : "secondary"}
          >
            {candidate.captain.employment_type_label}
          </StatusBadge>
          <StatusBadge variant={GPS_VARIANT[candidate.gps_freshness]}>
            {candidate.gps_freshness_label}
          </StatusBadge>
          {candidate.at_store_batch ? (
            <span className="inline-flex items-center gap-1 text-xs text-primary">
              <Store className="h-3 w-3" aria-hidden />
              {t("assign.atStore")}
            </span>
          ) : null}
          {candidate.batch_verdict !== "not_applicable" ? (
            <StatusBadge variant={BATCH_VARIANT[candidate.batch_verdict]}>
              {candidate.batch_verdict_label}
            </StatusBadge>
          ) : null}
        </div>

        {candidate.reasons.length > 0 ? (
          <ul className="mt-1.5 space-y-0.5 text-xs text-muted-foreground">
            {candidate.reasons.map((reason) => (
              <li key={reason}>· {reason}</li>
            ))}
          </ul>
        ) : null}
      </div>

      <div className="flex shrink-0 flex-col items-end justify-between gap-2">
        {/*
          The adjusted ETA is what the list is sorted on, so it leads. Distance is shown under it
          because it is the number people instinctively reach for — and the gap between the two
          is exactly what the ranking exists to account for.
        */}
        <div className="text-end">
          {candidate.adjusted_eta_min !== null ? (
            <p className="flex items-center justify-end gap-1 text-sm font-semibold text-foreground">
              <Clock className="h-3.5 w-3.5 text-muted-foreground" aria-hidden />
              <span className="numeric">
                {t("assign.minutes", { minutes: formatNumber(candidate.adjusted_eta_min, locale) })}
              </span>
            </p>
          ) : (
            <p className="text-xs text-muted-foreground">{t("assign.noRoadTime")}</p>
          )}
          {candidate.eta_estimated ? (
            <p className="text-[0.7rem] text-warning">{t("assign.estimated")}</p>
          ) : null}
          <p className="numeric text-xs text-muted-foreground">
            {formatDistanceKm(candidate.distance_km, locale)}
          </p>
        </div>

        <Button size="sm" onClick={onChoose} disabled={cannotCarry}>
          <CheckCircle2 className="h-4 w-4" aria-hidden />
          {t("assign.choose")}
        </Button>
      </div>
    </div>
  );
}

function ListSkeleton() {
  return (
    <div className="space-y-3 py-1">
      {Array.from({ length: 4 }).map((_, index) => (
        <Skeleton key={index} className="h-20 w-full" />
      ))}
    </div>
  );
}
