// @File: src/hooks/useWhatsappChatSocket.ts
// @Desc: Thin React wrapper around the /api/client/whatsapp/ws endpoint.
//        Maintains a single WebSocket connection keyed by the caller's JWT,
//        auto-reconnects with backoff, and exposes presence + typing + message
//        events to the chat UI via callbacks. Also provides helpers to publish
//        typing state and mark-read notifications upstream.
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { getToken } from "@/lib/auth";

export type PresenceEntry = {
  contact_id: number;
  online: boolean;
  last_seen_at: string | null;
};

export type WhatsappWsMessage =
  | {
      type: "snapshot";
      presence: PresenceEntry[];
      typing_contact_ids: number[];
    }
  | { type: "presence"; contact_id: number; online: boolean; last_seen_at: string | null }
  | { type: "typing"; contact_id: number; typing: boolean }
  | { type: "message"; message: Record<string, unknown> }
  | { type: "pong" }
  | { type: string; [key: string]: unknown };

type Options = {
  enabled?: boolean;
  onMessage?: (msg: Record<string, unknown>) => void;
};

function buildWsUrl(token: string): string {
  if (typeof window === "undefined") return "";
  const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
  return `${proto}//${window.location.host}/api/client/whatsapp/ws?token=${encodeURIComponent(token)}`;
}

export function useWhatsappChatSocket(options: Options = {}) {
  const { enabled = true, onMessage } = options;

  const [presenceMap, setPresenceMap] = useState<Record<number, PresenceEntry>>({});
  const [typingSet, setTypingSet] = useState<Set<number>>(() => new Set());
  const [connected, setConnected] = useState(false);

  const socketRef = useRef<WebSocket | null>(null);
  const reconnectTimerRef = useRef<number | null>(null);
  const pingTimerRef = useRef<number | null>(null);
  const attemptsRef = useRef(0);
  const onMessageRef = useRef(onMessage);
  onMessageRef.current = onMessage;

  const clearTimers = useCallback(() => {
    if (reconnectTimerRef.current !== null) {
      window.clearTimeout(reconnectTimerRef.current);
      reconnectTimerRef.current = null;
    }
    if (pingTimerRef.current !== null) {
      window.clearInterval(pingTimerRef.current);
      pingTimerRef.current = null;
    }
  }, []);

  const send = useCallback((payload: Record<string, unknown>) => {
    const ws = socketRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    try {
      ws.send(JSON.stringify(payload));
    } catch {
      /* noop */
    }
  }, []);

  const publishTyping = useCallback(
    (contactId: number, typing: boolean) => {
      send({ type: "typing", contact_id: contactId, typing });
    },
    [send],
  );

  useEffect(() => {
    if (!enabled) return;
    const token = getToken();
    if (!token || typeof window === "undefined") return;

    let cancelled = false;

    const connect = () => {
      if (cancelled) return;
      const url = buildWsUrl(token);
      if (!url) return;

      let ws: WebSocket;
      try {
        ws = new WebSocket(url);
      } catch {
        scheduleReconnect();
        return;
      }
      socketRef.current = ws;

      ws.onopen = () => {
        if (cancelled) return;
        attemptsRef.current = 0;
        setConnected(true);
        // Keep connection warm against idle LB timeouts.
        pingTimerRef.current = window.setInterval(() => {
          send({ type: "ping" });
        }, 25_000);
      };

      ws.onmessage = (evt) => {
        if (cancelled) return;
        let data: WhatsappWsMessage;
        try {
          data = JSON.parse(evt.data) as WhatsappWsMessage;
        } catch {
          return;
        }
        if (!data || typeof data !== "object") return;

        if (data.type === "snapshot") {
          const map: Record<number, PresenceEntry> = {};
          for (const p of data.presence ?? []) {
            map[p.contact_id] = p;
          }
          setPresenceMap(map);
          setTypingSet(new Set(data.typing_contact_ids ?? []));
          return;
        }
        if (data.type === "presence") {
          setPresenceMap((prev) => ({
            ...prev,
            [data.contact_id]: {
              contact_id: data.contact_id,
              online: data.online,
              last_seen_at: data.last_seen_at,
            },
          }));
          return;
        }
        if (data.type === "typing") {
          setTypingSet((prev) => {
            const next = new Set(prev);
            if (data.typing) next.add(data.contact_id);
            else next.delete(data.contact_id);
            return next;
          });
          return;
        }
        if (data.type === "message") {
          const msg = data.message;
          if (msg && typeof msg === "object" && onMessageRef.current) {
            onMessageRef.current(msg as Record<string, unknown>);
          }
          return;
        }
      };

      ws.onclose = () => {
        if (cancelled) return;
        setConnected(false);
        if (pingTimerRef.current !== null) {
          window.clearInterval(pingTimerRef.current);
          pingTimerRef.current = null;
        }
        scheduleReconnect();
      };

      ws.onerror = () => {
        try {
          ws.close();
        } catch {
          /* noop */
        }
      };
    };

    const scheduleReconnect = () => {
      if (cancelled) return;
      attemptsRef.current = Math.min(attemptsRef.current + 1, 6);
      const delay = Math.min(1_000 * 2 ** (attemptsRef.current - 1), 30_000);
      reconnectTimerRef.current = window.setTimeout(connect, delay);
    };

    connect();

    return () => {
      cancelled = true;
      clearTimers();
      const ws = socketRef.current;
      socketRef.current = null;
      if (ws) {
        try {
          ws.close();
        } catch {
          /* noop */
        }
      }
      setConnected(false);
    };
  }, [enabled, clearTimers, send]);

  const presenceFor = useCallback(
    (contactId: number | null): PresenceEntry | null => {
      if (contactId === null) return null;
      return presenceMap[contactId] ?? null;
    },
    [presenceMap],
  );

  const isTyping = useCallback(
    (contactId: number | null) => {
      if (contactId === null) return false;
      return typingSet.has(contactId);
    },
    [typingSet],
  );

  const typingContactIds = useMemo(() => Array.from(typingSet), [typingSet]);

  return {
    connected,
    presenceMap,
    presenceFor,
    isTyping,
    typingContactIds,
    publishTyping,
  };
}