import { useEffect, useRef, useState } from 'react';
import type { RealtimeChannel } from '@supabase/supabase-js';
import { canalPanelRestaurante, EVENTO_CAMBIO_PANEL, type CambioPanel } from '@gustito/shared';
import { supabase } from './supabase';

/**
 * Canal en vivo del panel — UN solo websocket-canal por restaurante y por
 * dispositivo, compartido entre todas las vistas (Cocina, Pedidos, Mesas,
 * alertas). Cada vista se cuelga con `useCambiosRestaurante` y filtra por
 * `tabla`. Antes cada vista abria su propio postgres_changes (7 canales por
 * dispositivo) y eso no escala con muchos locales conectados a la vez.
 *
 * Ademas resuelve el problema de las pantallas siempre abiertas (KDS/TV):
 *  - Reconexion sola con backoff cuando el canal se cae.
 *  - Al volver la pestana (visibilitychange/pageshow) o la red (online):
 *    reconecta de una y avisa `onResync` para que cada vista re-pida lo suyo.
 *  - Estado observable (`useEstadoTiempoReal`) para pintar un indicador
 *    honesto de conexion en vez de una pantalla "normal" que esta ciega.
 *
 * El canal es PRIVADO: la policy de realtime.messages solo deja escuchar al
 * personal del restaurante. Por eso hay que refrescar el token de realtime
 * (setAuth) antes de suscribir.
 */

export type EstadoTiempoReal = 'conectando' | 'en_vivo' | 'reconectando';

type Oyente = (c: CambioPanel) => void;

interface Gestor {
  id: string;
  oyentes: Set<Oyente>;
  oyentesResync: Set<() => void>;
  oyentesEstado: Set<() => void>;
  estado: EstadoTiempoReal;
  canal: RealtimeChannel | null;
  intentos: number;
  timer: ReturnType<typeof setTimeout> | null;
  /** Ya estuvo en vivo alguna vez: la proxima suscripcion exitosa emite resync. */
  estuvoVivo: boolean;
  quitarGlobales: () => void;
}

const gestores = new Map<string, Gestor>();

// ── QUIEN GASTA UNA CONEXION EN VIVO Y QUIEN NO ──────────────────────────────
//
// El cupo de Realtime es UNO SOLO para toda la plataforma y se gasta por
// DISPOSITIVO, no por pantalla. Un local en su pico tiene 40 telefonos de
// meseros: cien locales asi son 4.000 conexiones, y eso no lo arregla ningun
// plan (el add-on de computo sube el pool de la base, NO el cupo de Realtime).
//
// La conexion en vivo hace falta para ENTERARSE SIN ESTAR MIRANDO: al dueño y
// al encargado les entran pedidos y llamadas mientras hacen otra cosa. El
// mesero tiene el telefono en la mano y sus pantallas YA se refrescan solas
// (medido con un mesero real: `mis_cuentas_mesa` cada 6 s y `llamadas_activas`
// cada 5 s, aun teniendo el socket abierto). Por eso al mesero se le quita el
// socket y no pierde nada.
let rolActual: string | null = null;

/** Lo llama el Panel al saber con quien esta tratando. */
export function fijarRolTiempoReal(rol: string | null | undefined): void {
  const nuevo = rol ?? null;
  if (nuevo === rolActual) return;
  rolActual = nuevo;
  // Si cambio quien esta usando el equipo, se rearma con el criterio nuevo.
  for (const g of gestores.values()) {
    if (!sinOyentes(g)) void conectar(g);
  }
}

/** El mesero trabaja sin conexion en vivo: sus vistas ya se refrescan solas. */
function sinConexionEnVivo(): boolean {
  return rolActual === 'staff';
}

function emitirEstado(g: Gestor, e: EstadoTiempoReal) {
  if (g.estado === e) return;
  g.estado = e;
  g.oyentesEstado.forEach((f) => {
    try {
      f();
    } catch {
      /* un oyente roto no tumba al resto */
    }
  });
}

function emitirResync(g: Gestor) {
  g.oyentesResync.forEach((f) => {
    try {
      f();
    } catch {
      /* idem */
    }
  });
}

function sinOyentes(g: Gestor): boolean {
  return g.oyentes.size === 0 && g.oyentesResync.size === 0 && g.oyentesEstado.size === 0;
}

async function conectar(g: Gestor) {
  if (g.timer) {
    clearTimeout(g.timer);
    g.timer = null;
  }
  if (sinOyentes(g)) return;

  // Cerrar el canal anterior ANTES de anular la referencia: el callback de
  // status del canal viejo se ignora comparando `canal !== g.canal`.
  const viejo = g.canal;
  g.canal = null;
  if (viejo) {
    try {
      await supabase.removeChannel(viejo);
    } catch {
      /* ya estaba muerto */
    }
  }

  // Mesero: no se abre canal. Se deja el estado en vivo a proposito para que el
  // indicador no le mienta diciendo "reconectando" a alguien que nunca se
  // conecta; sus pantallas se refrescan solas y estan al dia igual.
  if (sinConexionEnVivo()) {
    emitirEstado(g, 'en_vivo');
    return;
  }

  // Canal privado: asegurar que realtime tenga el token de la sesion actual
  // (en frio, el socket puede seguir con el token anon y la policy rebota).
  try {
    const { data } = await supabase.auth.getSession();
    await supabase.realtime.setAuth(data.session?.access_token ?? null);
  } catch {
    /* sin sesion: el subscribe fallara y el backoff reintenta */
  }

  const canal = supabase.channel(canalPanelRestaurante(g.id), { config: { private: true } });
  g.canal = canal;
  canal
    .on('broadcast', { event: EVENTO_CAMBIO_PANEL }, ({ payload }) => {
      const cambio = payload as CambioPanel;
      g.oyentes.forEach((f) => {
        try {
          f(cambio);
        } catch {
          /* un oyente roto no tumba al resto */
        }
      });
    })
    .subscribe((status) => {
      if (canal !== g.canal) return; // callback de un canal ya reemplazado
      if (status === 'SUBSCRIBED') {
        g.intentos = 0;
        emitirEstado(g, 'en_vivo');
        // Si venimos de una caida, en el hueco pudieron pasar cosas: que cada
        // vista re-pida lo suyo (React Query desduplica, es barato).
        if (g.estuvoVivo) emitirResync(g);
        g.estuvoVivo = true;
      } else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT' || status === 'CLOSED') {
        emitirEstado(g, 'reconectando');
        programarReintento(g);
      }
    });
}

function programarReintento(g: Gestor) {
  if (g.timer || sinOyentes(g)) return;
  // Backoff exponencial con tope de 30s + jitter (que 200 dispositivos no
  // vuelvan a tocar la puerta todos en el mismo milisegundo).
  const espera = Math.min(30_000, 1_000 * 2 ** Math.min(g.intentos, 5)) + Math.random() * 500;
  g.intentos += 1;
  g.timer = setTimeout(() => {
    g.timer = null;
    void conectar(g);
  }, espera);
}

function obtenerGestor(id: string): Gestor {
  const existente = gestores.get(id);
  if (existente) return existente;

  const g: Gestor = {
    id,
    oyentes: new Set(),
    oyentesResync: new Set(),
    oyentesEstado: new Set(),
    estado: 'conectando',
    canal: null,
    intentos: 0,
    timer: null,
    estuvoVivo: false,
    quitarGlobales: () => {},
  };
  gestores.set(id, g);

  // Despertares de pantalla/red: el caso TV. Si la pestana estuvo suspendida,
  // al volver reconectamos de una (sin esperar el backoff) y resincronizamos.
  const alDespertar = () => {
    if (document.visibilityState !== 'visible') return;
    g.intentos = 0;
    if (g.estado !== 'en_vivo') void conectar(g);
    emitirResync(g);
  };
  const alVolverRed = () => {
    g.intentos = 0;
    if (g.estado !== 'en_vivo') void conectar(g);
  };
  document.addEventListener('visibilitychange', alDespertar);
  window.addEventListener('pageshow', alDespertar);
  window.addEventListener('online', alVolverRed);
  g.quitarGlobales = () => {
    document.removeEventListener('visibilitychange', alDespertar);
    window.removeEventListener('pageshow', alDespertar);
    window.removeEventListener('online', alVolverRed);
  };

  void conectar(g);
  return g;
}

function soltarGestor(g: Gestor) {
  if (!sinOyentes(g)) return;
  if (g.timer) {
    clearTimeout(g.timer);
    g.timer = null;
  }
  const canal = g.canal;
  g.canal = null; // que el callback CLOSED del canal se ignore
  if (canal) void supabase.removeChannel(canal);
  g.quitarGlobales();
  gestores.delete(g.id);
}

/**
 * Cuelga una vista al canal del restaurante.
 *  - `onCambio`: cada evento del panel (filtrar por `c.tabla`).
 *  - `onResync`: la conexion volvio (o la pestana desperto): re-pedir datos.
 * Pasar `restauranteId` null/undefined desactiva la suscripcion (p. ej. una
 * alerta que solo aplica al dueno).
 */
export function useCambiosRestaurante(
  restauranteId: string | null | undefined,
  opciones: { onCambio?: Oyente; onResync?: () => void },
) {
  const ref = useRef(opciones);
  ref.current = opciones;

  useEffect(() => {
    if (!restauranteId) return;
    const g = obtenerGestor(restauranteId);
    const oyente: Oyente = (c) => ref.current.onCambio?.(c);
    const resync = () => ref.current.onResync?.();
    g.oyentes.add(oyente);
    g.oyentesResync.add(resync);
    // Si el gestor quedo sin canal (todas las vistas se fueron y volvieron), reconectar.
    if (!g.canal && !g.timer) void conectar(g);
    return () => {
      g.oyentes.delete(oyente);
      g.oyentesResync.delete(resync);
      soltarGestor(g);
    };
  }, [restauranteId]);
}

/** Estado de la conexion en vivo, para el indicador honesto de las pantallas. */
export function useEstadoTiempoReal(restauranteId: string | null | undefined): EstadoTiempoReal {
  const [estado, setEstado] = useState<EstadoTiempoReal>('conectando');

  useEffect(() => {
    if (!restauranteId) return;
    const g = obtenerGestor(restauranteId);
    setEstado(g.estado);
    const cb = () => setEstado(g.estado);
    g.oyentesEstado.add(cb);
    if (!g.canal && !g.timer) void conectar(g);
    return () => {
      g.oyentesEstado.delete(cb);
      soltarGestor(g);
    };
  }, [restauranteId]);

  return estado;
}
