import {
  BOOKING_WINDOW_WS_EVENTS,
  BOOKING_WINDOW_WS_NAMESPACE,
  type BatchBoardChangedEvent,
  type BookingWindowPhaseEvent,
} from "@edr/types";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { io } from "socket.io-client";

import { API_BASE_URL } from "@/constants/apiConfig";
import { AUTH_TOKEN_COOKIE, getCookie } from "@/auth/cookies";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";

// The socket namespace lives at the server root, not under the `/api` REST
// prefix — strip a trailing `/api` if the base URL carries one.
const SOCKET_ORIGIN = String(API_BASE_URL ?? "").replace(/\/api\/?$/, "");

// The two carousel window lists share the MyBookingWindow-shaped row and can be
// patched in place. The batch board is a richer, differently-shaped view, so it
// stays on a (debounced) invalidate.
const WINDOW_ACTIONS = new Set(["all-booking-windows", "contractBookingWindows"]);

/** Shape shared by both carousel window lists (all-lanes + contract-scoped). */
interface WindowRow {
  scheduleId: string;
  windowPhase: string | null;
  isOpenNow: boolean;
  windowOpensAt: string | null;
  windowClosesAt: string | null;
  docReviewEndsAt: string | null;
  paymentPhaseEndsAt: string | null;
  bookingWindowStatus: string;
  bookingCycleNo: number;
  departureDate: string;
}

function isWindowKey(key: readonly unknown[]): boolean {
  return key[0] === "train-scheduling" && WINDOW_ACTIONS.has(String(key[1]));
}

/**
 * Fold a server phase push onto a cached window row, recomputing isOpenNow the
 * same way the server does (phase OPEN + status OPEN) so live-patched state can
 * never disagree with a fresh REST fetch on refresh.
 */
function applyEvent<T extends WindowRow>(row: T, event: BookingWindowPhaseEvent): T {
  return {
    ...row,
    windowPhase: event.phase,
    bookingWindowStatus: event.bookingWindowStatus ?? row.bookingWindowStatus,
    bookingCycleNo: event.bookingCycleNo,
    isOpenNow: event.phase === "OPEN" && event.bookingWindowStatus === "OPEN",
    windowOpensAt: event.windowOpensAt,
    windowClosesAt: event.windowClosesAt,
    docReviewEndsAt: event.docReviewEndsAt,
    paymentPhaseEndsAt: event.paymentPhaseEndsAt,
    departureDate: event.scheduledDepartureDate ?? row.departureDate,
  };
}

/**
 * Subscribes to live booking-window pushes for staff. A phase transition carries
 * the schedule's full new state; we fold it straight into the carousel window
 * lists with setQueriesData rather than invalidating — same rationale as the
 * portal hook (no per-push refetch storm; live + refreshed state agree, killing
 * the refresh-jump). The batch board is a different-shaped view, so it keeps a
 * debounced invalidate, as do pushes for schedules not present in any list.
 */
export function useBookingWindowSocket(enabled: boolean = true) {
  const qc = useQueryClient();

  useEffect(() => {
    if (!enabled) return;
    const token = getCookie(AUTH_TOKEN_COOKIE);
    if (!token) return;

    const socket = io(`${SOCKET_ORIGIN}/${BOOKING_WINDOW_WS_NAMESPACE}`, {
      auth: { token },
      transports: ["websocket"],
      withCredentials: true,
    });

    // Deliberate console breadcrumbs: "live updates not arriving" is only
    // diagnosable from the browser when connect/reject outcomes are visible.
    let hadConnected = false;
    socket.on("connect", () => {
      console.debug("[booking-windows] socket connected", socket.id);
      // A RE-connect means pushes may have been missed while offline — refetch
      // every board view (list + any open detail share the "batch-board" key
      // prefix) once so the gap self-heals immediately.
      if (hadConnected) {
        void qc.invalidateQueries({
          queryKey: ["train-scheduling", "batch-board"],
        });
      }
      hadConnected = true;
    });
    socket.on("connect_error", (err) =>
      console.warn("[booking-windows] socket connect failed:", err.message),
    );
    socket.on("disconnect", (reason) =>
      console.debug("[booking-windows] socket disconnected:", reason),
    );

    // Coalesce the batch-board refresh (and the unknown-schedule fallback) so a
    // burst of pushes triggers at most one invalidation per window.
    let refetchTimer: ReturnType<typeof setTimeout> | null = null;
    const scheduleRefetch = (includeWindowLists: boolean) => {
      if (refetchTimer) return;
      refetchTimer = setTimeout(() => {
        refetchTimer = null;
        void qc.invalidateQueries({
          queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoard(),
        });
        if (includeWindowLists) {
          void qc.invalidateQueries({
            predicate: (q) => isWindowKey(q.queryKey),
          });
        }
      }, 800);
    };

    socket.on(
      BOOKING_WINDOW_WS_EVENTS.PHASE,
      (event: BookingWindowPhaseEvent) => {
        let patchedSomewhere = false;

        qc.setQueriesData<WindowRow[]>(
          { predicate: (q) => isWindowKey(q.queryKey) },
          (rows) => {
            if (!rows) return rows;
            let changed = false;
            const next = rows.map((row) => {
              if (row.scheduleId !== event.scheduleId) return row;
              changed = true;
              patchedSomewhere = true;
              return applyEvent(row, event);
            });
            return changed ? next : rows;
          },
        );

        // Refresh the batch-board DETAIL for the schedule that transitioned so the
        // Priority Tracking tab reranks + updates its countdowns immediately (the
        // detail is a different shape from the list — invalidate, don't patch).
        void qc.invalidateQueries({
          queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(event.scheduleId),
        });

        // Always refresh the batch board (different shape, not patched). When the
        // schedule wasn't in any window list either, refresh those too so a newly
        // announced window surfaces. Both debounced — no per-push stampede.
        scheduleRefetch(!patchedSomewhere);
      },
    );

    // Board-data pushes (payment settled, allocation, fill, expiry, …): refetch
    // the schedule's detail immediately, refresh the list debounced. The event
    // carries no data — the board views are rich, differently-shaped queries.
    socket.on(
      BOOKING_WINDOW_WS_EVENTS.BATCH_CHANGED,
      (event: BatchBoardChangedEvent) => {
        void qc.invalidateQueries({
          queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(event.scheduleId),
        });
        scheduleRefetch(false);
      },
    );

    return () => {
      if (refetchTimer) clearTimeout(refetchTimer);
      socket.off();
      socket.disconnect();
    };
  }, [enabled, qc]);
}
