// Conexion del cerebrito con la nube: sesion del negocio, lectura del pedido
// con su ruteo (producto -> categoria -> puesto) y canal en vivo del panel.
import { createClient, type RealtimeChannel, type SupabaseClient } from '@supabase/supabase-js';
import type { CambioPanel, Config, PedidoNube, Puesto } from './tipos.js';

// Mismo contrato que packages/shared/src/realtime.ts (el trigger fn_bcast_panel).
export const EVENTO_CAMBIO = 'cambio';
export const canalPanel = (rid: string) => `rest-bcast-${rid}`;

const COLS_PEDIDO =
  'id,codigo,estado,tipo_entrega,mesa,cuenta_id,para_llevar,mesero_nombre,nota,recibido_en,origen,' +
  'cliente_nombre,cliente_telefono,direccion,metodo_pago,total_usd,costo_delivery_usd,tasa_bs,' +
  'pagos(metodo,estado,referencia),' +
  'pedido_items(id,nombre,cantidad,nota,a_cocina,producto:productos(puesto_id,categoria:categorias(puesto_id)))';

export interface Nube {
  supabase: SupabaseClient;
  restauranteId: string;
  nombre: string;
  puestos(): Promise<Puesto[]>;
  pedido(id: string): Promise<PedidoNube | null>;
  pendientes(horas: number): Promise<{ id: string }[]>;
  escuchar(
    onCambio: (c: CambioPanel) => void,
    onEstado: (status: string) => void,
  ): { quitar: () => void };
  /** Vuelve a entrar con la cuenta guardada (sesion caida tras dormir/caida larga). */
  recuperarSesion(motivo: string): Promise<void>;
  /**
   * Papel impreso = cocina lo tiene: marca el pedido "preparando" solo (el
   * cocinero de papel no toca pantallas). Solo avanza desde confirmado.
   */
  marcarPreparando(pedidoId: string): Promise<void>;
  /** Todos los pedidos CONFIRMADOS de una cuenta de mesa (para la comanda agrupada). */
  pedidosDeCuenta(cuentaId: string): Promise<PedidoNube[]>;
  /** Lo COBRADO de una cuenta cerrada: pedidos pagados + sus pagos reales (para la copia del negocio). */
  cuentaCobrada(cuentaId: string): Promise<{
    pedidos: PedidoNube[];
    pagos: { metodo: string | null; monto_usd: number | null; propina_usd: number | null; referencia: string | null }[];
  } | null>;
  /** Cuentas de mesa cerradas en las ultimas N horas (barrido de la copia del negocio). */
  cuentasCerradas(horas: number): Promise<{ id: string }[]>;
  /** Ventas de caja (nacen entregadas) de las ultimas N horas (barrido de sus comandas). */
  ventasCaja(horas: number): Promise<{ id: string }[]>;
}

export async function conectarNube(cfg: Config, log: (m: string) => void): Promise<Nube> {
  const supabase = createClient(cfg.supabaseUrl, cfg.anonKey, {
    auth: { persistSession: false, autoRefreshToken: true },
  });

  const { data: ses, error } = await supabase.auth.signInWithPassword({
    email: cfg.correo,
    password: cfg.clave,
  });
  if (error || !ses.session) {
    throw new Error('No se pudo iniciar sesion: ' + (error?.message ?? 'sin sesion'));
  }

  const { data: perfil } = await supabase
    .from('perfiles')
    .select('rol,restaurante_id')
    .eq('id', ses.session.user.id)
    .maybeSingle();
  const rol = perfil?.rol ?? '';
  if (!perfil?.restaurante_id || !['dueno', 'encargado', 'staff', 'admin'].includes(rol)) {
    throw new Error('La cuenta no pertenece al personal de un restaurante');
  }
  if (rol === 'staff') {
    log('OJO: con una cuenta de mesero solo se ven SUS mesas. Usa una cuenta de encargado o dueno.');
  }
  const restauranteId = perfil.restaurante_id;

  const { data: rest } = await supabase
    .from('restaurantes')
    .select('nombre')
    .eq('id', restauranteId)
    .maybeSingle();

  // Canal privado: realtime necesita el token de la sesion (y refrescarlo cuando rote).
  await supabase.realtime.setAuth(ses.session.access_token);

  // GUARDIAN DE SESION: si el refresco falla (laptop dormida horas, caida larga,
  // rotacion perdida), el puente NO se queda zombie: re-entra solo con la cuenta
  // guardada, con paciencia infinita (cuando vuelva el internet, recupera).
  let recuperando = false;
  async function recuperarSesion(motivo: string): Promise<void> {
    if (recuperando) return;
    recuperando = true;
    try {
      log(`Sesion caida (${motivo}); entrando de nuevo...`);
      for (;;) {
        const { data, error: e } = await supabase.auth.signInWithPassword({
          email: cfg.correo,
          password: cfg.clave,
        });
        if (!e && data.session) {
          await supabase.realtime.setAuth(data.session.access_token);
          log('Sesion recuperada.');
          return;
        }
        // Clave cambiada de verdad: no tiene sentido martillar cada 10s.
        const espera = e && /invalid|credentials/i.test(e.message) ? 300_000 : 10_000;
        if (espera > 10_000) log('La clave guardada ya no sirve; reintento en 5 min (reconfigura el asistente).');
        await new Promise((r) => setTimeout(r, espera));
      }
    } finally {
      recuperando = false;
    }
  }
  supabase.auth.onAuthStateChange((ev, s) => {
    if (ev === 'SIGNED_OUT') void recuperarSesion('cierre de sesion');
    else void supabase.realtime.setAuth(s?.access_token ?? null);
  });
  const esErrorDeSesion = (m: string) => /jwt|token|expired|401/i.test(m);

  return {
    recuperarSesion,

    async marcarPreparando(pedidoId: string) {
      const { error: e } = await supabase.rpc('cambiar_estado_pedido', {
        p_pedido_id: pedidoId,
        p_nuevo_estado: 'preparando',
      });
      if (e) {
        if (esErrorDeSesion(e.message)) void recuperarSesion('marcar preparando');
        log(`No pude marcar preparando el ${pedidoId.slice(0, 8)}: ${e.message}`);
      }
    },
    supabase,
    restauranteId,
    nombre: rest?.nombre ?? restauranteId,

    async puestos() {
      const { data, error: e } = await supabase
        .from('puestos_impresion')
        .select('id,nombre,orden,es_defecto,activo')
        .eq('restaurante_id', restauranteId)
        .order('orden');
      if (e) throw new Error('No pude leer los puestos: ' + e.message);
      return (data ?? []) as Puesto[];
    },

    async pedido(id: string) {
      const { data, error: e } = await supabase
        .from('pedidos')
        .select(COLS_PEDIDO)
        .eq('id', id)
        .maybeSingle();
      if (e) {
        if (esErrorDeSesion(e.message)) void recuperarSesion('lectura con sesion vencida');
        log(`No pude leer el pedido ${id}: ${e.message}`);
        return null;
      }
      return (data as unknown as PedidoNube) ?? null;
    },

    async pedidosDeCuenta(cuentaId: string) {
      const { data, error: e } = await supabase
        .from('pedidos')
        .select(COLS_PEDIDO)
        .eq('cuenta_id', cuentaId)
        .eq('estado', 'confirmado')
        .order('recibido_en');
      if (e) {
        if (esErrorDeSesion(e.message)) void recuperarSesion('lectura de cuenta');
        log(`No pude leer la cuenta ${cuentaId.slice(0, 8)}: ${e.message}`);
        return [];
      }
      return (data ?? []) as unknown as PedidoNube[];
    },

    async cuentaCobrada(cuentaId: string) {
      // Solo lo que quedo PAGADO (pago_mesa_id puesto por cobrar_y_cerrar_mesa /
      // cobrar_parte_mesa): es exactamente lo que se cobro, sin borradores sueltos.
      const { data: pedidos, error: e1 } = await supabase
        .from('pedidos')
        .select(COLS_PEDIDO)
        .eq('cuenta_id', cuentaId)
        .neq('estado', 'cancelado')
        .not('pago_mesa_id', 'is', null);
      if (e1) {
        if (esErrorDeSesion(e1.message)) void recuperarSesion('lectura de cuenta cobrada');
        log(`No pude leer la cuenta cobrada ${cuentaId.slice(0, 8)}: ${e1.message}`);
        return null;
      }
      const { data: pagos, error: e2 } = await supabase
        .from('pagos_mesa')
        .select('metodo,monto_usd,propina_usd,referencia')
        .eq('cuenta_id', cuentaId);
      if (e2) {
        log(`No pude leer los pagos de ${cuentaId.slice(0, 8)}: ${e2.message}`);
        return null;
      }
      return {
        pedidos: (pedidos ?? []) as unknown as PedidoNube[],
        pagos: (pagos ?? []) as { metodo: string | null; monto_usd: number | null; propina_usd: number | null; referencia: string | null }[],
      };
    },

    async cuentasCerradas(horas: number) {
      const desde = new Date(Date.now() - horas * 3600_000).toISOString();
      const { data, error: e } = await supabase
        .from('cuentas_mesa')
        .select('id')
        .eq('restaurante_id', restauranteId)
        .eq('estado', 'cerrada')
        .gte('cerrada_en', desde);
      if (e) throw new Error('cuentas cerradas: ' + e.message);
      return data ?? [];
    },

    async ventasCaja(horas: number) {
      const desde = new Date(Date.now() - horas * 3600_000).toISOString();
      const { data, error: e } = await supabase
        .from('pedidos')
        .select('id')
        .eq('restaurante_id', restauranteId)
        .eq('origen', 'caja')
        .gte('recibido_en', desde);
      if (e) throw new Error('ventas de caja: ' + e.message);
      return data ?? [];
    },

    async pendientes(horas: number) {
      const desde = new Date(Date.now() - horas * 3600_000).toISOString();
      // OJO: la ventana mira la hora de CONFIRMADO (o la de recibido si aquella
      // esta vacia): un pedido recibido en la mañana y confirmado durante una
      // caida larga TIENE que entrar al barrido cuando vuelva el internet.
      const { data, error: e } = await supabase
        .from('pedidos')
        .select('id')
        .eq('restaurante_id', restauranteId)
        .in('estado', ['confirmado', 'preparando'])
        .or(`confirmado_en.gte.${desde},recibido_en.gte.${desde}`);
      if (e) {
        if (esErrorDeSesion(e.message)) void recuperarSesion('barrido con sesion vencida');
        throw new Error('barrido: ' + e.message);
      }
      return data ?? [];
    },

    escuchar(onCambio, onEstado) {
      let canal: RealtimeChannel | null = supabase.channel(canalPanel(restauranteId), {
        config: { private: true },
      });
      canal
        .on('broadcast', { event: EVENTO_CAMBIO }, ({ payload }) => onCambio(payload as CambioPanel))
        .subscribe((status) => {
          if (canal) onEstado(status);
        });
      return {
        quitar() {
          const c = canal;
          canal = null; // silencia el CLOSED que dispara el propio remove
          if (c) void supabase.removeChannel(c);
        },
      };
    },
  };
}
