import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from "react";
import {
  DndContext,
  KeyboardSensor,
  PointerSensor,
  TouchSensor,
  closestCenter,
  useSensor,
  useSensors,
  type DragEndEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  arrayMove,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useBranding } from "../BrandingContext.js";
import type { RouteMapStop } from "../components/RouteMap.js";

// Leaflet is heavy and only needed on this page — load it as a separate chunk.
const RouteMap = lazy(() => import("../components/RouteMap.js"));

// ─── Types (mirror groombook/api /api/routes responses) ─────────────────────────

type RouteStatus = "draft" | "optimized" | "in_progress" | "completed";

interface RouteRow {
  id: string;
  staffId: string;
  routeDate: string;
  status: RouteStatus;
  totalTravelMins: number | null;
  totalDistanceKm: string | null;
}

interface ConflictFlags {
  hasConflict: boolean;
}

interface RouteStop {
  id: string;
  appointmentId: string;
  stopOrder: number;
  latitude: number;
  longitude: number;
  travelMinsFromPrev: number | null;
  travelDistanceKmFromPrev: string | null;
  bufferMins: number;
  appointmentStartTime: string;
  appointmentEndTime: string;
  appointmentStatus: string;
  clientId: string;
  clientName: string;
  clientAddress: string | null;
  conflict: ConflictFlags;
}

interface RouteResponse {
  route: RouteRow;
  stops: RouteStop[];
  hasConflicts: boolean;
  conflictCount: number;
  warnings?: string[];
  skipped?: Array<{ appointmentId: string; clientName: string; reason: string }>;
}

interface StaffMember {
  id: string;
  name: string;
  role: "groomer" | "receptionist" | "manager";
  active: boolean;
}

// ─── Helpers ────────────────────────────────────────────────────────────────────

function todayIso(): string {
  return new Date().toISOString().slice(0, 10);
}

function fmtDuration(mins: number | null | undefined): string {
  if (mins == null) return "—";
  if (mins < 60) return `${mins} min`;
  const h = Math.floor(mins / 60);
  const m = mins % 60;
  return m === 0 ? `${h} h` : `${h} h ${m} min`;
}

function fmtTime(iso: string): string {
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return iso;
  return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}

// ─── Navigation export ────────────────────────────────────────────────────────

/** Navigation target platforms supported by the API export endpoints. */
type NavigationPlatform = "google-maps" | "apple-maps";

type DevicePlatform = "ios" | "android" | "other";

/**
 * Best-effort mobile-OS detection so we can surface the most useful navigation
 * app first. Apple Maps deep links (`maps://`) only resolve on iOS; everywhere
 * else Google Maps is the safe default. iPadOS 13+ reports a desktop UA, so we
 * also treat a touch-capable "MacIntel" device as iOS.
 */
function detectPlatform(): DevicePlatform {
  if (typeof navigator === "undefined") return "other";
  const ua = navigator.userAgent || "";
  if (/iphone|ipad|ipod/i.test(ua)) return "ios";
  if (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) return "ios";
  if (/android/i.test(ua)) return "android";
  return "other";
}

// ─── Offline map-tile pre-warming ────────────────────────────────────────────

/** OSM tile zoom levels pre-fetched around a route so the map renders offline. */
const PREWARM_ZOOM_LEVELS = [12, 13, 14] as const;
/** Hard cap on tiles fetched per pre-warm pass — keeps us friendly to OSM. */
const MAX_PREWARM_TILES = 80;
/** Subdomains Leaflet's default OSM TileLayer rotates through (`{s}`). */
const TILE_SUBDOMAINS = ["a", "b", "c"] as const;

/** Web-Mercator longitude → tile X index at the given zoom. */
function lonToTileX(lon: number, z: number): number {
  return Math.floor(((lon + 180) / 360) * 2 ** z);
}

/** Web-Mercator latitude → tile Y index at the given zoom. */
function latToTileY(lat: number, z: number): number {
  const rad = (lat * Math.PI) / 180;
  return Math.floor(
    ((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * 2 ** z
  );
}

/**
 * Warm the browser/service-worker cache with the OSM tiles covering the route's
 * bounding box (plus a one-tile margin) across a few zoom levels. Tiles are
 * fetched via `new Image()` so they hit the same URLs Leaflet later requests and
 * land in the CacheFirst tile cache, making the map viewable offline. Bounded by
 * MAX_PREWARM_TILES so a sprawling route never floods the network.
 */
function prewarmRouteTiles(
  stops: Array<{ latitude: number; longitude: number }>
): void {
  if (typeof window === "undefined" || stops.length === 0) return;
  const lats = stops.map((s) => s.latitude);
  const lons = stops.map((s) => s.longitude);
  const minLat = Math.min(...lats);
  const maxLat = Math.max(...lats);
  const minLon = Math.min(...lons);
  const maxLon = Math.max(...lons);

  const urls: string[] = [];
  for (const z of PREWARM_ZOOM_LEVELS) {
    const x0 = lonToTileX(minLon, z) - 1;
    const x1 = lonToTileX(maxLon, z) + 1;
    // Tile Y grows as latitude decreases, so maxLat → smaller Y.
    const y0 = latToTileY(maxLat, z) - 1;
    const y1 = latToTileY(minLat, z) + 1;
    for (let x = x0; x <= x1; x++) {
      for (let y = y0; y <= y1; y++) {
        if (x < 0 || y < 0 || x >= 2 ** z || y >= 2 ** z) continue;
        const s = TILE_SUBDOMAINS[(x + y) % TILE_SUBDOMAINS.length];
        urls.push(`https://${s}.tile.openstreetmap.org/${z}/${x}/${y}.png`);
      }
    }
  }

  for (const url of urls.slice(0, MAX_PREWARM_TILES)) {
    const img = new Image();
    img.src = url;
  }
}

// ─── Responsive layout ────────────────────────────────────────────────────────

/** Tracks a `max-width` media query so the page can adapt to phone widths. */
function useIsMobile(maxWidthPx = 768): boolean {
  const query = `(max-width: ${maxWidthPx}px)`;
  const [isMobile, setIsMobile] = useState(
    () => typeof window !== "undefined" && typeof window.matchMedia === "function"
      ? window.matchMedia(query).matches
      : false
  );
  useEffect(() => {
    if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
    const mq = window.matchMedia(query);
    const onChange = (e: MediaQueryListEvent) => setIsMobile(e.matches);
    setIsMobile(mq.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, [query]);
  return isMobile;
}

const STATUS_STYLES: Record<RouteStatus, { bg: string; fg: string; label: string }> = {
  draft: { bg: "#f1f5f9", fg: "#475569", label: "Draft" },
  optimized: { bg: "#ecfdf5", fg: "#047857", label: "Optimized" },
  in_progress: { bg: "#eff6ff", fg: "#1d4ed8", label: "In progress" },
  completed: { bg: "#f5f3ff", fg: "#6d28d9", label: "Completed" },
};

function StatusBadge({ status }: { status: RouteStatus }) {
  const s = STATUS_STYLES[status] ?? STATUS_STYLES.draft;
  return (
    <span
      style={{
        background: s.bg,
        color: s.fg,
        borderRadius: 999,
        padding: "0.2rem 0.7rem",
        fontSize: 12,
        fontWeight: 600,
        textTransform: "none",
      }}
    >
      {s.label}
    </span>
  );
}

const inputStyle: React.CSSProperties = {
  padding: "0.4rem 0.6rem",
  borderRadius: 6,
  border: "1px solid #cbd5e1",
  fontSize: 14,
};

/**
 * A single draggable stop card. The drag handle (⠿) carries the dnd-kit
 * listeners so the rest of the card stays scrollable/selectable; the handle is
 * sized for touch and works with pointer, touch and keyboard sensors.
 */
function SortableStop({
  stop,
  primaryColor,
  disabled,
}: {
  stop: RouteStop;
  primaryColor: string;
  disabled: boolean;
}) {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
    useSortable({ id: stop.id, disabled });

  return (
    <div
      ref={setNodeRef}
      style={{
        background: "#fff",
        border: `1px solid ${stop.conflict?.hasConflict ? "#fca5a5" : "#e2e8f0"}`,
        borderRadius: 8,
        padding: "0.7rem 0.85rem",
        display: "flex",
        gap: 10,
        transform: CSS.Transform.toString(transform),
        transition,
        opacity: isDragging ? 0.6 : 1,
        boxShadow: isDragging ? "0 6px 16px rgba(0,0,0,0.18)" : "none",
        touchAction: "none",
      }}
    >
      <button
        type="button"
        aria-label={`Drag to reorder ${stop.clientName}`}
        disabled={disabled}
        {...attributes}
        {...listeners}
        style={{
          flexShrink: 0,
          alignSelf: "stretch",
          width: 28,
          border: "none",
          background: "transparent",
          color: "#94a3b8",
          fontSize: 18,
          lineHeight: 1,
          cursor: disabled ? "not-allowed" : "grab",
          touchAction: "none",
          padding: 0,
        }}
      >
        ⠿
      </button>
      <div
        style={{
          flexShrink: 0,
          width: 26,
          height: 26,
          borderRadius: "50%",
          background: primaryColor,
          color: "#fff",
          fontSize: 13,
          fontWeight: 700,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        {stop.stopOrder}
      </div>
      <div style={{ minWidth: 0, flex: 1 }}>
        <div style={{ display: "flex", justifyContent: "space-between", gap: 8 }}>
          <strong style={{ fontSize: 14, color: "#1a202c" }}>{stop.clientName}</strong>
          <span style={{ fontSize: 13, color: "#4b5563", whiteSpace: "nowrap" }}>{fmtTime(stop.appointmentStartTime)}</span>
        </div>
        <div style={{ fontSize: 12, color: "#6b7280", marginTop: 2 }}>{stop.clientAddress || "No address on file"}</div>
        <div style={{ fontSize: 12, color: "#6b7280", marginTop: 4 }}>
          {stop.stopOrder === 1 || stop.travelMinsFromPrev == null
            ? "Start of route"
            : `${fmtDuration(stop.travelMinsFromPrev)} travel from previous`}
        </div>
        {stop.conflict?.hasConflict && (
          <div style={{ fontSize: 12, color: "#b91c1c", marginTop: 4, fontWeight: 600 }}>
            ⚠ Tight schedule — travel + buffer may exceed the gap
          </div>
        )}
      </div>
    </div>
  );
}

/**
 * Navigation export controls. Fetches a platform deep-link from the API and opens
 * it. The button matching the detected device OS is shown prominently (filled);
 * the other is offered as a secondary outline button. On desktop both are
 * secondary and Google Maps leads.
 */
function NavExportButtons({
  routeId,
  primaryColor,
  fullWidth,
}: {
  routeId: string;
  primaryColor: string;
  fullWidth: boolean;
}) {
  const [busy, setBusy] = useState<NavigationPlatform | null>(null);
  const [error, setError] = useState<string | null>(null);
  const platform = useMemo(detectPlatform, []);

  const openIn = useCallback(
    async (target: NavigationPlatform) => {
      setBusy(target);
      setError(null);
      // Pre-open a tab synchronously: mobile Safari/Chrome block window.open()
      // calls that happen after an await (no longer in the user-gesture turn).
      const win = window.open("", "_blank");
      try {
        const r = await fetch(`/api/routes/${encodeURIComponent(routeId)}/export/${target}`);
        if (!r.ok) {
          const body = await r.json().catch(() => ({}));
          throw new Error(body.error || `Export failed (${r.status})`);
        }
        const { url } = (await r.json()) as { url: string };
        if (win) win.location.href = url;
        else window.location.href = url;
      } catch (e) {
        win?.close();
        setError(e instanceof Error ? e.message : "Export failed");
      } finally {
        setBusy(null);
      }
    },
    [routeId]
  );

  const baseBtn: React.CSSProperties = {
    padding: "0.55rem 1rem",
    borderRadius: 6,
    fontWeight: 600,
    fontSize: 14,
    cursor: busy ? "wait" : "pointer",
    flex: fullWidth ? "1 1 0" : "0 0 auto",
  };
  const primaryBtn: React.CSSProperties = {
    ...baseBtn,
    border: "none",
    background: primaryColor,
    color: "#fff",
  };
  const secondaryBtn: React.CSSProperties = {
    ...baseBtn,
    border: `1px solid ${primaryColor}`,
    background: "#fff",
    color: primaryColor,
  };

  const label = (p: NavigationPlatform) =>
    busy === p
      ? "Opening…"
      : p === "google-maps"
        ? "Open in Google Maps"
        : "Open in Apple Maps";

  const google = (
    <button
      key="google"
      type="button"
      onClick={() => openIn("google-maps")}
      disabled={busy !== null}
      style={platform === "ios" ? secondaryBtn : primaryBtn}
    >
      {label("google-maps")}
    </button>
  );
  const apple = (
    <button
      key="apple"
      type="button"
      onClick={() => openIn("apple-maps")}
      disabled={busy !== null}
      style={platform === "ios" ? primaryBtn : secondaryBtn}
    >
      {label("apple-maps")}
    </button>
  );
  // Prominent (filled) button first; secondary second.
  const ordered = platform === "ios" ? [apple, google] : [google, apple];

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
        <span style={{ fontSize: 12, color: "#4b5563", fontWeight: 600, marginRight: 4 }}>
          Navigate
        </span>
        {ordered}
      </div>
      {error && <div style={{ fontSize: 12, color: "#991b1b" }}>{error}</div>}
    </div>
  );
}

// ─── Page ───────────────────────────────────────────────────────────────────────

export function RoutesPage() {
  const { branding } = useBranding();
  const primaryColor = branding.primaryColor || "#4f8a6f";

  const [me, setMe] = useState<StaffMember | null>(null);
  const [meLoaded, setMeLoaded] = useState(false);
  const [groomers, setGroomers] = useState<StaffMember[]>([]);
  const [staffId, setStaffId] = useState<string>("");
  const [date, setDate] = useState<string>(todayIso());

  const [data, setData] = useState<RouteResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [optimizing, setOptimizing] = useState(false);
  const [reordering, setReordering] = useState(false);
  const [manuallyReordered, setManuallyReordered] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const isGroomer = me?.role === "groomer";
  const isMobile = useIsMobile();

  // Resolve the current staff member; groomers are pinned to their own route.
  useEffect(() => {
    fetch("/api/staff/me")
      .then((r) => (r.ok ? r.json() : null))
      .then((row: StaffMember | null) => {
        setMe(row);
        if (row?.role === "groomer") setStaffId(row.id);
      })
      .catch(() => setMe(null))
      .finally(() => setMeLoaded(true));
  }, []);

  // Managers / receptionists pick a groomer; groomers never see the selector.
  useEffect(() => {
    if (!meLoaded || isGroomer) return;
    fetch("/api/staff")
      .then((r) => (r.ok ? r.json() : []))
      .then((rows: StaffMember[]) => {
        const gs = rows.filter((s) => s.active && s.role === "groomer");
        setGroomers(gs);
        setStaffId((cur) => cur || gs[0]?.id || "");
      })
      .catch(() => setGroomers([]));
  }, [meLoaded, isGroomer]);

  const loadRoute = useCallback(async () => {
    if (!staffId || !date) return;
    setLoading(true);
    setError(null);
    try {
      const r = await fetch(
        `/api/routes/daily?staffId=${encodeURIComponent(staffId)}&date=${encodeURIComponent(date)}`
      );
      if (!r.ok) {
        const body = await r.json().catch(() => ({}));
        throw new Error(body.error || `Failed to load route (${r.status})`);
      }
      setData(await r.json());
      setManuallyReordered(false);
    } catch (e) {
      setData(null);
      setError(e instanceof Error ? e.message : "Failed to load route");
    } finally {
      setLoading(false);
    }
  }, [staffId, date]);

  useEffect(() => {
    void loadRoute();
  }, [loadRoute]);

  const optimize = useCallback(async () => {
    if (!staffId || !date) return;
    setOptimizing(true);
    setError(null);
    try {
      const r = await fetch("/api/routes/optimize", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ staffId, date }),
      });
      if (!r.ok) {
        const body = await r.json().catch(() => ({}));
        throw new Error(body.error || `Optimization failed (${r.status})`);
      }
      setData(await r.json());
      setManuallyReordered(false);
    } catch (e) {
      setError(e instanceof Error ? e.message : "Optimization failed");
    } finally {
      setOptimizing(false);
    }
  }, [staffId, date]);

  // Drag-to-reorder: pointer for desktop, touch (press-and-hold) for mobile
  // groomers, keyboard for accessibility. Touch uses a short delay so vertical
  // scrolling of the stop list still works without triggering a drag.
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
    useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 8 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
  );

  // Persist a manually reordered stop list. Optimistic: the UI is updated
  // immediately from the dropped order and rolled back if the PATCH fails.
  const reorder = useCallback(
    async (orderedIds: string[]) => {
      const routeId = data?.route?.id;
      if (!routeId) return;
      const previous = data;
      // Optimistic local update: renumber stopOrder to match the new order so
      // the list and the map reflect the drop before the server responds.
      const byId = new Map((data?.stops ?? []).map((s) => [s.id, s]));
      const optimisticStops = orderedIds
        .map((id, i) => {
          const s = byId.get(id);
          return s ? { ...s, stopOrder: i + 1 } : null;
        })
        .filter((s): s is RouteStop => s !== null);
      setData((cur) => (cur ? { ...cur, stops: optimisticStops } : cur));
      setManuallyReordered(true);
      setReordering(true);
      setError(null);
      try {
        const r = await fetch(`/api/routes/${encodeURIComponent(routeId)}/reorder`, {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ stopOrder: orderedIds }),
        });
        if (!r.ok) {
          const body = await r.json().catch(() => ({}));
          throw new Error(body.error || `Reorder failed (${r.status})`);
        }
        // Server recomputes travel legs, buffers and conflict flags — adopt its
        // authoritative response over the optimistic guess.
        setData(await r.json());
      } catch (e) {
        setData(previous); // rollback
        setError(e instanceof Error ? e.message : "Reorder failed");
      } finally {
        setReordering(false);
      }
    },
    [data]
  );

  const handleDragEnd = useCallback(
    (event: DragEndEvent) => {
      const { active, over } = event;
      if (!over || active.id === over.id) return;
      const ids = (data?.stops ?? []).map((s) => s.id);
      const from = ids.indexOf(String(active.id));
      const to = ids.indexOf(String(over.id));
      if (from === -1 || to === -1) return;
      void reorder(arrayMove(ids, from, to));
    },
    [data, reorder]
  );

  const mapStops: RouteMapStop[] = useMemo(
    () =>
      (data?.stops ?? []).map((s) => ({
        id: s.id,
        stopOrder: s.stopOrder,
        latitude: s.latitude,
        longitude: s.longitude,
        clientName: s.clientName,
      })),
    [data]
  );

  // Pre-warm OSM map tiles for the route area whenever a route (re)loads or is
  // re-optimized, so the map stays viewable offline. Runs after today's route is
  // fetched on page load and after every optimize/reorder that yields new stops.
  useEffect(() => {
    if (mapStops.length > 0) prewarmRouteTiles(mapStops);
  }, [mapStops]);

  const stops = data?.stops ?? [];
  const route = data?.route ?? null;

  return (
    <div style={{ padding: "1.25rem", maxWidth: 1280, margin: "0 auto" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap", marginBottom: "1rem" }}>
        <h1 style={{ fontSize: 22, fontWeight: 700, color: "#1a202c", margin: 0 }}>Route Planner</h1>
        {route && <StatusBadge status={route.status} />}
      </div>

      {/* Controls */}
      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "flex-end", marginBottom: "1rem" }}>
        <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12, color: "#4b5563", fontWeight: 600 }}>
          Date
          <input type="date" value={date} onChange={(e) => setDate(e.target.value)} style={inputStyle} />
        </label>

        {!isGroomer && (
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12, color: "#4b5563", fontWeight: 600 }}>
            Groomer
            <select
              value={staffId}
              onChange={(e) => setStaffId(e.target.value)}
              style={{ ...inputStyle, minWidth: 180 }}
            >
              {groomers.length === 0 && <option value="">No groomers</option>}
              {groomers.map((g) => (
                <option key={g.id} value={g.id}>
                  {g.name}
                </option>
              ))}
            </select>
          </label>
        )}

        <button
          onClick={optimize}
          disabled={optimizing || !staffId}
          style={{
            padding: "0.5rem 1.1rem",
            borderRadius: 6,
            border: "none",
            background: primaryColor,
            color: "#fff",
            fontWeight: 600,
            fontSize: 14,
            cursor: optimizing || !staffId ? "wait" : "pointer",
            opacity: optimizing || !staffId ? 0.7 : 1,
          }}
        >
          {optimizing ? "Optimizing…" : "Optimize"}
        </button>
      </div>

      {error && (
        <div style={{ background: "#fef2f2", border: "1px solid #fecaca", borderRadius: 6, padding: "0.6rem 0.8rem", color: "#991b1b", fontSize: 13, marginBottom: "1rem" }}>
          {error}
        </div>
      )}

      {data?.warnings && data.warnings.length > 0 && (
        <div style={{ background: "#fffbeb", border: "1px solid #fde68a", borderRadius: 6, padding: "0.6rem 0.8rem", color: "#92400e", fontSize: 13, marginBottom: "1rem" }}>
          {data.warnings.map((w, i) => (
            <div key={i}>{w}</div>
          ))}
        </div>
      )}

      {/* Summary */}
      <div style={{ display: "flex", gap: 24, flexWrap: "wrap", marginBottom: "1rem", padding: "0.8rem 1rem", background: "#fff", borderRadius: 8, border: "1px solid #e2e8f0" }}>
        <Summary label="Stops" value={String(stops.length)} />
        <Summary label="Total travel time" value={fmtDuration(route?.totalTravelMins)} />
        <Summary label="Total distance" value={route?.totalDistanceKm != null ? `${route.totalDistanceKm} km` : "—"} />
      </div>

      {/* Navigation export — open the route in the device's maps app */}
      {route && stops.length > 0 && (
        <div style={{ marginBottom: "1rem", padding: "0.8rem 1rem", background: "#fff", borderRadius: 8, border: "1px solid #e2e8f0" }}>
          <NavExportButtons routeId={route.id} primaryColor={primaryColor} fullWidth={isMobile} />
        </div>
      )}

      <div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "minmax(0, 1.5fr) minmax(280px, 1fr)", gap: 16, alignItems: "stretch" }}>
        {/* Map */}
        <div style={{ height: isMobile ? 340 : 540, background: "#e5e7eb", borderRadius: 8, overflow: "hidden", border: "1px solid #e2e8f0" }}>
          {mapStops.length > 0 ? (
            <Suspense fallback={<Centered>Loading map…</Centered>}>
              <RouteMap stops={mapStops} primaryColor={primaryColor} />
            </Suspense>
          ) : (
            <Centered>{loading ? "Loading route…" : "No stops to display. Click Optimize to build the route."}</Centered>
          )}
        </div>

        {/* Stop list panel — drag-to-reorder */}
        <div style={{ display: "flex", flexDirection: "column", gap: 10, maxHeight: isMobile ? "none" : 540, overflowY: isMobile ? "visible" : "auto" }}>
          {stops.length === 0 && !loading && (
            <div style={{ color: "#6b7280", fontSize: 14, padding: "1rem" }}>No stops for this day.</div>
          )}
          {stops.length > 0 && (
            <>
              {manuallyReordered && (
                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, background: "#fffbeb", border: "1px solid #fde68a", borderRadius: 8, padding: "0.55rem 0.7rem" }}>
                  <span style={{ fontSize: 12, color: "#92400e" }}>
                    Stops reordered manually. Re-optimize to recompute the best route.
                  </span>
                  <button
                    type="button"
                    onClick={optimize}
                    disabled={optimizing || reordering || !staffId}
                    style={{
                      flexShrink: 0,
                      padding: "0.35rem 0.7rem",
                      borderRadius: 6,
                      border: "none",
                      background: primaryColor,
                      color: "#fff",
                      fontSize: 13,
                      fontWeight: 600,
                      cursor: optimizing || reordering || !staffId ? "not-allowed" : "pointer",
                      opacity: optimizing || reordering || !staffId ? 0.6 : 1,
                    }}
                  >
                    {optimizing ? "Optimizing…" : "Re-optimize"}
                  </button>
                </div>
              )}
              <DndContext
                sensors={sensors}
                collisionDetection={closestCenter}
                onDragEnd={handleDragEnd}
              >
                <SortableContext items={stops.map((s) => s.id)} strategy={verticalListSortingStrategy}>
                  {stops.map((s) => (
                    <SortableStop key={s.id} stop={s} primaryColor={primaryColor} disabled={reordering} />
                  ))}
                </SortableContext>
              </DndContext>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

function Summary({ label, value }: { label: string; value: string }) {
  return (
    <div>
      <div style={{ fontSize: 12, color: "#6b7280", fontWeight: 600 }}>{label}</div>
      <div style={{ fontSize: 18, color: "#1a202c", fontWeight: 700 }}>{value}</div>
    </div>
  );
}

function Centered({ children }: { children: React.ReactNode }) {
  return (
    <div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#6b7280", fontSize: 14, textAlign: "center", padding: "1rem" }}>
      {children}
    </div>
  );
}
