// Contexto Global de Realtime con Motor Server-Side en PostgreSQL (Supabase RPC)
const { createContext, useContext, useState, useEffect, useRef, useCallback, useMemo } = React;

const BingoRoomContext = createContext(null);

function BingoRoomProvider({ children, userRole = 'spectator', userName = 'Espectador', adminPin: propAdminPin = '' }) {
  const getEffectiveAdminPin = useCallback(() => {
    if (propAdminPin) return propAdminPin;
    try {
      const saved = localStorage.getItem('bingo_session_user');
      if (saved) {
        const parsed = JSON.parse(saved);
        if (parsed && parsed.adminPin) return parsed.adminPin;
      }
    } catch (e) {}
    return userRole === 'admin' ? '1234' : '';
  }, [propAdminPin, userRole]);
  // ==========================================
  // 1. ESTADO DEL BOLILLERO Y PARTIDA (Server-Side)
  // ==========================================
  const [calledBalls, setCalledBalls] = useState([]);
  const [currentBall, setCurrentBall] = useState(null);
  const [isAutoDrawing, setIsAutoDrawing] = useState(false);
  const [drawPausedByPrize, setDrawPausedByPrize] = useState(false);
  const [countdown, setCountdown] = useState(5);
  const [showMatrix, setShowMatrix] = useState(false);
  const [isDrawingBall, setIsDrawingBall] = useState(false);

  // Estado Oficial de Venta de Cartones (Habilitar / Cerrar Venta)
  const [isSalesOpen, setIsSalesOpen] = useState(() => {
    try {
      const saved = localStorage.getItem('bingo_sales_open');
      if (saved !== null) return saved === 'true';
    } catch (e) {}
    return true; // Por defecto abierta
  });

  // Premios Oficiales
  const [prize1Winner, setPrize1Winner] = useState(null);
  const [prizeBingoWinner, setPrizeBingoWinner] = useState(null);
  const [prizeCardRaffleWinner, setPrizeCardRaffleWinner] = useState(null);
  const [isRafflingCard, setIsRafflingCard] = useState(false);
  const [raffleDisplayCode, setRaffleDisplayCode] = useState('CR-?????');
  const [latestAnnouncement, setLatestAnnouncement] = useState(null);

  // Estructura de Rondas Oficiales ("Bingo la Mami": Premio 3 $50k, Premio 2 $50k, Premio 1 Mayor $100k)
  const [currentRound, setCurrentRound] = useState(3);
  const [roundBingoPrize, setRoundBingoPrize] = useState(50000);
  const [roundLineaPrize, setRoundLineaPrize] = useState(10000);
  const [roundCartonPrize, setRoundCartonPrize] = useState(10000);

  // Radar de Líderes Pre-Calculado por PostgreSQL Server-Side
  const [radarRanking, setRadarRanking] = useState([]);

  // Audio / Locutor con Web Speech API
  const [voiceEnabled, setVoiceEnabled] = useState(true);
  const [availableVoices, setAvailableVoices] = useState([]);
  const [selectedVoiceURI, setSelectedVoiceURI] = useState('');
  const [speechRate, setSpeechRate] = useState(1.05); // Ritmo natural y claro
  const [speechPitch, setSpeechPitch] = useState(1.05);
  const [voiceStyle, setVoiceStyle] = useState('clasico'); // 'clasico' ("¡Número 48, el muerto!") o 'oficial'

  // Enlace de Video / Transmisión en Vivo del Bolillero Real (YouTube, Twitch, Meet, etc.)
  const [videoUrl, setVideoUrl] = useState(() => {
    try {
      return localStorage.getItem('bingo_room_video_url') || '';
    } catch (e) {
      return '';
    }
  });

  // ==========================================
  // 2. BASE DE DATOS SUPABASE
  // ==========================================
  const [realRoomCards, setRealRoomCards] = useState([]);
  const [receipts, setReceipts] = useState([]);
  const [loadingDb, setLoadingDb] = useState(true);

  // ==========================================
  // 3. PRESENCIA & ESPECTADORES EN VIVO
  // ==========================================
  const [viewersCount, setViewersCount] = useState(52);
  const [channelStatus, setChannelStatus] = useState('connecting');

  const channelRef = useRef(null);
  const clientId = useRef(`client-${Math.random().toString(36).substr(2, 9)}`);
  const lastSpokenBallRef = useRef(null);

  // Cargar catálogo de voces del navegador
  useEffect(() => {
    if (!('speechSynthesis' in window)) return;
    const updateVoices = () => {
      const allVoices = window.speechSynthesis.getVoices() || [];
      const esVoices = allVoices.filter(v => v.lang.startsWith('es') || v.lang.startsWith('ES'));
      setAvailableVoices(esVoices.length > 0 ? esVoices : allVoices);

      if (!selectedVoiceURI && esVoices.length > 0) {
        const preferred = esVoices.find(v => v.name.includes('Natural') || v.name.includes('Online') || v.name.includes('Google') || v.name.includes('Elena') || v.name.includes('Sabina') || v.name.includes('Raul')) || esVoices[0];
        setSelectedVoiceURI(preferred.voiceURI);
      }
    };

    updateVoices();
    if (window.speechSynthesis.onvoiceschanged !== undefined) {
      window.speechSynthesis.onvoiceschanged = updateVoices;
    }
  }, [selectedVoiceURI]);

  // Obtener voz seleccionada
  const getSelectedVoice = useCallback(() => {
    if (!('speechSynthesis' in window)) return null;
    const voices = window.speechSynthesis.getVoices() || [];
    return voices.find(v => v.voiceURI === selectedVoiceURI) || voices.find(v => v.lang.startsWith('es')) || null;
  }, [selectedVoiceURI]);

  // Gestor seguro de habla que previene Garbage Collection en Chrome y evita que se corte el audio
  const safeSpeakUtterance = useCallback((utterance) => {
    if (!utterance || !('speechSynthesis' in window)) return;

    window._activeBingoUtterances = window._activeBingoUtterances || [];
    window._activeBingoUtterances.push(utterance);

    const cleanup = () => {
      const idx = window._activeBingoUtterances.indexOf(utterance);
      if (idx !== -1) window._activeBingoUtterances.splice(idx, 1);
    };

    utterance.addEventListener('end', cleanup);
    utterance.addEventListener('error', cleanup);

    if (window.speechSynthesis.paused) {
      window.speechSynthesis.resume();
    }

    window.speechSynthesis.speak(utterance);
  }, []);

  // Frases dinámicas y variadas para el locutor de bingo (sin repetir 'número' constantemente)
  const getDynamicBallPhrase = useCallback((ballNum, style = 'clasico', callIndex = 0) => {
    if (!ballNum) return '';

    // Apodos clásicos tradicionales de lotería y bingo
    const classicNicknames = {
      1: "¡El uno, el palito!",
      2: "¡El dos, el patito!",
      3: "¡El tres, San Cono!",
      4: "¡El cuatro, la cama!",
      5: "¡El cinco, la espina!",
      11: "¡El once, los dos palitos!",
      13: "¡El trece, la mala suerte!",
      14: "¡El catorce, el borracho!",
      15: "¡El quince, la niña bonita!",
      17: "¡El diecisiete, la desgracia!",
      20: "¡El veinte, la fiesta!",
      22: "¡Veintidós, los dos patitos!",
      25: "¡El veinticinco, la gallina!",
      32: "¡Treinta y dos, el dinero!",
      33: "¡Treinta y tres, la edad de Cristo!",
      45: "¡Cuarenta y cinco, el vino!",
      48: "¡El cuarenta y ocho, el muerto!",
      50: "¡El cincuenta, el pan!",
      60: "¡El sesenta, la virgen!",
      77: "¡Setenta y siete, las dos banderas!",
      88: "¡Ochenta y ocho, los dos ochos!",
      90: "¡El noventa, el abuelo!"
    };

    if (style === 'clasico' && classicNicknames[ballNum]) {
      return classicNicknames[ballNum];
    }

    // Variaciones naturales del locutor de sala
    const variations = [
      `¡El ${ballNum}!`,
      `¡Sale el ${ballNum}!`,
      `¡${ballNum}!`,
      `¡Atención con el ${ballNum}!`,
      `¡Y viene el ${ballNum}!`,
      `¡Siguiente, el ${ballNum}!`,
      `¡Tenemos el ${ballNum}!`
    ];

    const variantIndex = (ballNum + callIndex) % variations.length;
    return variations[variantIndex];
  }, []);

  // Síntesis de voz oficial con locución variada, completa y sin cortes
  const speakBallAndPrize = useCallback((ballNum, prizeAnnouncementText = null) => {
    if (!voiceEnabled || !('speechSynthesis' in window) || !ballNum) return;

    // Evitar dictar dos veces seguidas la misma bolilla
    if (lastSpokenBallRef.current === ballNum && !prizeAnnouncementText) {
      return;
    }
    lastSpokenBallRef.current = ballNum;

    try {
      // Frase variada y natural de locutor
      const ballPhrase = getDynamicBallPhrase(ballNum, voiceStyle, calledBalls.length);

      const chosenVoice = getSelectedVoice();
      const ballUtterance = new SpeechSynthesisUtterance(ballPhrase);
      ballUtterance.lang = 'es-ES';
      ballUtterance.rate = speechRate || 1.05;
      ballUtterance.pitch = speechPitch || 1.05;
      if (chosenVoice) ballUtterance.voice = chosenVoice;

      if (prizeAnnouncementText) {
        ballUtterance.onend = () => {
          try {
            const prizeUtterance = new SpeechSynthesisUtterance(prizeAnnouncementText);
            prizeUtterance.lang = 'es-ES';
            prizeUtterance.rate = Math.min(1.15, (speechRate || 1.05) * 1.02);
            prizeUtterance.pitch = Math.min(1.25, (speechPitch || 1.05) * 1.1);
            if (chosenVoice) prizeUtterance.voice = chosenVoice;
            safeSpeakUtterance(prizeUtterance);
          } catch (e) {}
        };
      }

      safeSpeakUtterance(ballUtterance);
    } catch (e) {
      console.warn("Error en speakBallAndPrize:", e);
    }
  }, [voiceEnabled, voiceStyle, calledBalls.length, speechRate, speechPitch, getSelectedVoice, safeSpeakUtterance, getDynamicBallPhrase]);

  const speakDirect = useCallback((text, options = {}) => {
    if (!voiceEnabled || !('speechSynthesis' in window)) return;
    try {
      const chosenVoice = getSelectedVoice();
      const utterance = new SpeechSynthesisUtterance(text);
      utterance.lang = 'es-ES';
      utterance.rate = options.rate || speechRate || 1.05;
      utterance.pitch = options.pitch || speechPitch || 1.05;
      if (chosenVoice) utterance.voice = chosenVoice;
      safeSpeakUtterance(utterance);
    } catch (e) {
      console.warn("Error en speakDirect:", e);
    }
  }, [voiceEnabled, speechRate, speechPitch, getSelectedVoice, safeSpeakUtterance]);

  const speakPhrase = useCallback((phraseType) => {
    if (phraseType === 'bolilla_52') {
      speakDirect("¡El cincuenta y dos!", { pitch: speechPitch, rate: speechRate });
    } else if (phraseType === 'linea_en_juego') {
      speakDirect("¡Primera línea en juego por diez mil pesos!", { pitch: speechPitch * 1.05, rate: speechRate });
    } else if (phraseType === 'linea_ganador') {
      speakDirect("¡¡ATENCIÓN, ATENCIÓN!! ¡¡Tenemos ganador de Primera Línea con el número cuarenta y dos!! ¡¡Felicitaciones a Daniela G, ganadora de diez mil pesos!!", { pitch: speechPitch * 1.15, rate: speechRate * 1.05 });
    } else if (phraseType === 'bingo' || phraseType === 'bingo_ganador') {
      speakDirect("¡¡¡BIIINGO, BINGO, BIIINGO CON EL NÚMERO ONCE!!! ¡¡¡TENEMOS GANADOR DE CARTÓN LLENO!!! ¡¡¡UNA LOCURA TOTAL, FELICITACIONES A MARCO, GANADOR DEL GRAN PREMIO MAYOR DE CINCUENTA MIL PESOS!!!", { pitch: speechPitch * 1.22, rate: speechRate * 1.05 });
    } else if (phraseType === 'sorteo_serie') {
      speakDirect("¡¡ATENCIÓN AL SORTEO DE SERIE!! ¡¡EL CARTÓN GANADOR ES DE EDUVIJES CON EL NÚMERO C R 38589!! ¡¡FELICITACIONES, SE LLEVA DIEZ MIL PESOS!!", { pitch: speechPitch * 1.15, rate: speechRate });
    }
  }, [speakDirect, speechPitch, speechRate]);

  const testVoice = useCallback((sampleText = "¡El cincuenta y dos! ¡Felicitaciones a Daniela G, ganadora de diez mil pesos!") => {
    speakDirect(sampleText, { pitch: speechPitch, rate: speechRate });
  }, [speakDirect, speechPitch, speechRate]);

  // Cargar estado inicial de la sala y radar desde PostgreSQL
  const loadServerGameState = useCallback(async () => {
    if (!window.supabaseClient) return;
    try {
      const [sessionRes, radarRes] = await Promise.all([
        window.supabaseClient.rpc('get_game_session', { p_room_id: 'main-room' }),
        window.supabaseClient.rpc('get_radar_ranking', { p_room_id: 'main-room' })
      ]);

      if (!sessionRes.error && sessionRes.data) {
        const data = sessionRes.data;
        const balls = data.called_balls || [];
        setCalledBalls(balls);
        if (data.current_ball) {
          setCurrentBall({
            num: data.current_ball,
            range: data.current_ball <= 18 ? 1 : data.current_ball <= 36 ? 2 : data.current_ball <= 54 ? 3 : data.current_ball <= 72 ? 4 : 5
          });
        }
        if (data.prize_1_winner) setPrize1Winner(data.prize_1_winner);
        if (data.prize_bingo_winner) setPrizeBingoWinner(data.prize_bingo_winner);
        if (data.prize_card_raffle_winner) setPrizeCardRaffleWinner(data.prize_card_raffle_winner);
        if (data.latest_announcement) setLatestAnnouncement(data.latest_announcement);
        if (data.video_url !== undefined && data.video_url !== null) setVideoUrl(data.video_url || '');
        if (data.status === 'paused') setDrawPausedByPrize(true);
        if (data.current_round) setCurrentRound(data.current_round);
        if (data.prize_bingo_amount) setRoundBingoPrize(data.prize_bingo_amount);
        if (data.prize_linea_amount) setRoundLineaPrize(data.prize_linea_amount);
        if (data.prize_carton_amount) setRoundCartonPrize(data.prize_carton_amount);

        // Sincronizar estado oficial de venta de cartones
        if (data.latest_announcement && typeof data.latest_announcement.sales_open === 'boolean') {
          setIsSalesOpen(data.latest_announcement.sales_open);
          try { localStorage.setItem('bingo_sales_open', String(data.latest_announcement.sales_open)); } catch (e) {}
        } else if (data.status === 'sales_closed') {
          setIsSalesOpen(false);
          try { localStorage.setItem('bingo_sales_open', 'false'); } catch (e) {}
        }
      }

      if (!radarRes.error && radarRes.data) {
        setRadarRanking(radarRes.data || []);
      }
    } catch (e) {
      console.warn("Error cargando sesión de juego y radar:", e);
    }
  }, []);

  // Cargar cartones y comprobantes
  const loadDatabaseData = useCallback(async () => {
    setLoadingDb(true);
    if (window.ReceiptService) {
      const [cardsData, receiptsData] = await Promise.all([
        window.ReceiptService.getAllActiveCards(),
        window.ReceiptService.getAllReceipts()
      ]);
      setRealRoomCards(cardsData || []);
      setReceipts(receiptsData || []);
    }
    setLoadingDb(false);
  }, []);

  // =========================================================================
  // CANAL ÚNICO DE REALTIME EN SUPABASE: 'bingo-room-live'
  // =========================================================================
  useEffect(() => {
    loadDatabaseData();
    loadServerGameState();

    if (!window.supabaseClient) {
      setChannelStatus('offline');
      return;
    }

    const channel = window.supabaseClient.channel('bingo-room-live', {
      config: {
        presence: { key: clientId.current },
        broadcast: { self: false }
      }
    });

    channelRef.current = channel;

    // 1. BROADCAST: Sincronización instantánea de eventos emitidos por el servidor / organizador
    channel
      .on('broadcast', { event: 'server_ball_drawn' }, ({ payload }) => {
        setCalledBalls(payload.called_balls || []);
        if (payload.radar_ranking) setRadarRanking(payload.radar_ranking);
        if (payload.selected_ball) {
          setCurrentBall({
            num: payload.selected_ball,
            range: payload.selected_ball <= 18 ? 1 : payload.selected_ball <= 36 ? 2 : payload.selected_ball <= 54 ? 3 : payload.selected_ball <= 72 ? 4 : 5
          });
        }
        if (payload.prize_1_winner) setPrize1Winner(payload.prize_1_winner);
        if (payload.prize_bingo_winner) setPrizeBingoWinner(payload.prize_bingo_winner);
        if (payload.latest_announcement) setLatestAnnouncement(payload.latest_announcement);
        if (payload.is_paused_by_prize) {
          setIsAutoDrawing(false);
          setDrawPausedByPrize(true);
        }

        // Si viene texto de premio con el nombre del ganador, locutarlo con prioridad
        if (payload.prize_speech_text) {
          speakDirect(payload.prize_speech_text, { pitch: 1.18, rate: 1.05 });
        } else if (payload.selected_ball) {
          speakBallAndPrize(payload.selected_ball);
        }
      })
      .on('broadcast', { event: 'server_game_reset' }, ({ payload }) => {
        lastSpokenBallRef.current = null;
        setCalledBalls([]);
        setCurrentBall(null);
        setIsAutoDrawing(false);
        setDrawPausedByPrize(false);
        setCountdown(5);
        setPrize1Winner(null);
        setPrizeBingoWinner(null);
        setPrizeCardRaffleWinner(null);
        setLatestAnnouncement(null);
        setRaffleDisplayCode('CR-?????');
        if (payload?.radar_ranking) {
          setRadarRanking(payload.radar_ranking);
        } else {
          setRadarRanking(prev => prev.map(c => ({
            ...c,
            matchedCount: 0,
            missingCount: 15,
            missingNumbers: c.allNumbers || (c.grid ? c.grid.flat().filter(n => n !== null && n !== undefined) : []),
            completedRows: 0,
            rowHits: [0, 0, 0]
          })));
        }
        speakDirect("¡Comienza una nueva partida de Bingo!", { pitch: 1.15 });
      })
      .on('broadcast', { event: 'server_round_changed' }, ({ payload }) => {
        if (payload?.round) setCurrentRound(payload.round);
        if (payload?.prize_bingo_amount) setRoundBingoPrize(payload.prize_bingo_amount);
        if (payload?.prize_linea_amount) setRoundLineaPrize(payload.prize_linea_amount);
        if (payload?.prize_carton_amount) setRoundCartonPrize(payload.prize_carton_amount);
        lastSpokenBallRef.current = null;
        setCalledBalls([]);
        setCurrentBall(null);
        setIsAutoDrawing(false);
        setDrawPausedByPrize(false);
        setCountdown(5);
        setPrize1Winner(null);
        setPrizeBingoWinner(null);
        setPrizeCardRaffleWinner(null);
        setLatestAnnouncement(null);
        setRaffleDisplayCode('CR-?????');
        if (payload?.radar_ranking) {
          setRadarRanking(payload.radar_ranking);
        } else {
          setRadarRanking(prev => prev.map(c => ({
            ...c,
            matchedCount: 0,
            missingCount: 15,
            missingNumbers: c.allNumbers || (c.grid ? c.grid.flat().filter(n => n !== null && n !== undefined) : []),
            completedRows: 0,
            rowHits: [0, 0, 0]
          })));
        }
        const bMonto = payload?.prize_bingo_amount || (payload?.round === 1 ? 100000 : 50000);
        speakDirect(`¡Atención señoras y señores! Se inicia la Ronda ${payload?.round || 1} de Bingo la Mami. Premio Mayor del Bingo: ${Number(bMonto).toLocaleString('es-AR')} pesos. ¡Mucha suerte a todos!`, { pitch: 1.15, rate: 1.05 });
      })
      .on('broadcast', { event: 'auto_draw_toggle' }, ({ payload }) => {
        setIsAutoDrawing(payload.isAutoDrawing);
        setDrawPausedByPrize(payload.drawPausedByPrize || false);
      })
      .on('broadcast', { event: 'server_card_raffle_winner' }, ({ payload }) => {
        setPrizeCardRaffleWinner(payload.winner_data);
        setRaffleDisplayCode(payload.winner_data.cardId);
        setLatestAnnouncement(payload.announcement);
        speakDirect(`¡¡ATENCIÓN SEÑORAS Y SEÑORES!! ¡¡LLEGAMOS AL GRAN SORTEO FINAL POR NÚMERO DE CARTÓN DE BINGO LA MAMI!! ¡¡EL CARTÓN GANADOR DE LOS DIEZ MIL PESOS ES DE ${payload.winner_data.title} CON EL NÚMERO ${payload.winner_data.cardId}!! ¡¡MUCHAS FELICITACIONES!!`, { pitch: 1.2, rate: 1.05 });
      })
      .on('broadcast', { event: 'video_url_update' }, ({ payload }) => {
        if (payload && payload.video_url !== undefined) {
          setVideoUrl(payload.video_url || '');
        }
      })
      .on('broadcast', { event: 'server_sales_status' }, ({ payload }) => {
        if (payload && typeof payload.sales_open === 'boolean') {
          setIsSalesOpen(payload.sales_open);
          try { localStorage.setItem('bingo_sales_open', String(payload.sales_open)); } catch (e) {}
          if (payload.sales_open) {
            speakDirect("¡Atención! La venta oficial de cartones ha sido ABIERTA.", { pitch: 1.1 });
          } else {
            speakDirect("¡Atención! La venta oficial de cartones para este sorteo se encuentra CERRADA.", { pitch: 1.05 });
          }
        }
      })
      .on('broadcast', { event: 'card_sold' }, ({ payload }) => {
        if (payload?.soldCards && Array.isArray(payload.soldCards)) {
          setRealRoomCards(prev => {
            const existingCodes = new Set(prev.map(c => c.id || c.card_code));
            const newSold = payload.soldCards.filter(c => !existingCodes.has(c.id || c.card_code));
            return newSold.length > 0 ? [...prev, ...newSold] : prev;
          });
        }
        loadDatabaseData();
      })

      // 2. POSTGRES CHANGES: Escucha centralizada de cambios en BD
      .on('postgres_changes', { event: '*', schema: 'public', table: 'game_sessions' }, () => {
        loadServerGameState();
      })
      .on('postgres_changes', { event: '*', schema: 'public', table: 'receipts' }, () => {
        loadDatabaseData();
      })
      .on('postgres_changes', { event: '*', schema: 'public', table: 'bingo_cards' }, () => {
        loadDatabaseData();
      })

      // 3. PRESENCE: Contador de espectadores en tiempo real
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState();
        const count = Object.keys(state).length;
        setViewersCount(Math.max(45, count + 40));
      })
      .subscribe((status) => {
        setChannelStatus(status === 'SUBSCRIBED' ? 'connected' : status);
        if (status === 'SUBSCRIBED') {
          channel.track({
            user: userName,
            role: userRole,
            online_at: new Date().toISOString()
          });
        }
      });

    return () => {
      if (channelRef.current && window.supabaseClient) {
        window.supabaseClient.removeChannel(channelRef.current);
      }
    };
  }, [loadDatabaseData, loadServerGameState, speakBallAndPrize, speakDirect, userRole, userName]);

  // =========================================================================
  // ACCIONES: BROADCAST-FIRST + PERSISTENCIA ASÍNCRONA EN SUPABASE
  // =========================================================================

  // 1. Extraer siguiente bolilla (Server-Side Authority en PostgreSQL + Realtime Broadcast)
  const drawNextBall = useCallback(async () => {
    if (isDrawingBall || calledBalls.length >= 90) return;
    setIsDrawingBall(true);

    try {
      if (window.supabaseClient) {
        const { data, error } = await window.supabaseClient.rpc('draw_next_ball', { 
          p_room_id: 'main-room',
          p_admin_pin: getEffectiveAdminPin()
        });

        if (!error && data && data.success) {
          const newBall = data.selected_ball;
          const allBalls = data.called_balls || [];

          const newBallObj = {
            num: newBall,
            range: newBall <= 18 ? 1 : newBall <= 36 ? 2 : newBall <= 54 ? 3 : newBall <= 72 ? 4 : 5
          };

          setCurrentBall(newBallObj);
          setCalledBalls(allBalls);

          if (data.radar_ranking) setRadarRanking(data.radar_ranking);
          if (data.prize_1_winner) setPrize1Winner(data.prize_1_winner);
          if (data.prize_bingo_winner) setPrizeBingoWinner(data.prize_bingo_winner);
          if (data.latest_announcement) setLatestAnnouncement(data.latest_announcement);

          // Transmitir a todos los espectadores conectados en tiempo real
          if (channelRef.current) {
            channelRef.current.send({
              type: 'broadcast',
              event: 'server_ball_drawn',
              payload: data
            });
          }

          // Locución oficial: si hay premio, proclamar al ganador; si no, dictar la bolilla
          if (data.prize_speech_text) {
            speakDirect(data.prize_speech_text, { pitch: 1.18, rate: 1.05 });
          } else {
            speakBallAndPrize(newBall);
          }

          if (data.is_paused_by_prize) {
            setIsAutoDrawing(false);
            setDrawPausedByPrize(true);
          }
        }
      } else {
        // Fallback local en caso de estar offline
        const availableNumbers = [];
        for (let i = 1; i <= 90; i++) {
          if (!calledBalls.includes(i)) availableNumbers.push(i);
        }
        if (availableNumbers.length > 0) {
          const nextBallNum = availableNumbers[Math.floor(Math.random() * availableNumbers.length)];
          const updatedBalls = [nextBallNum, ...calledBalls];
          setCurrentBall({
            num: nextBallNum,
            range: nextBallNum <= 18 ? 1 : nextBallNum <= 36 ? 2 : nextBallNum <= 54 ? 3 : nextBallNum <= 72 ? 4 : 5
          });
          setCalledBalls(updatedBalls);
          speakBallAndPrize(nextBallNum);
        }
      }
    } catch (err) {
      console.error("Error al extraer bolilla:", err);
    } finally {
      setIsDrawingBall(false);
    }
  }, [calledBalls, isDrawingBall, speakBallAndPrize, speakDirect]);

  // 1b. Cantar bolilla específica manualmente (para bolillero físico con video)
  const callSpecificBall = useCallback(async (ballNum) => {
    const num = parseInt(ballNum, 10);
    if (isNaN(num) || num < 1 || num > 90) {
      alert("Por favor ingresa un número de bolilla válido entre 1 y 90.");
      return { success: false, error: 'Número fuera de rango (1-90)' };
    }
    if (calledBalls.includes(num)) {
      alert(`La bolilla ${num} ya fue cantada en esta partida.`);
      return { success: false, error: `Bolilla ${num} ya cantada` };
    }
    if (isDrawingBall) return { success: false, error: 'Procesando otra bolilla...' };

    setIsDrawingBall(true);

    try {
      if (window.supabaseClient) {
        const { data, error } = await window.supabaseClient.rpc('call_specific_ball', { 
          p_ball_num: num,
          p_room_id: 'main-room',
          p_admin_pin: getEffectiveAdminPin()
        });

        if (error) {
          console.error("Error al registrar bolilla manual en Supabase:", error);
          alert(`Error al registrar la bolilla ${num}: ` + (error.message || error));
          return { success: false, error: error.message };
        }

        if (data && data.success) {
          const newBall = data.selected_ball;
          const allBalls = data.called_balls || [];

          const newBallObj = {
            num: newBall,
            range: newBall <= 18 ? 1 : newBall <= 36 ? 2 : newBall <= 54 ? 3 : newBall <= 72 ? 4 : 5
          };

          setCurrentBall(newBallObj);
          setCalledBalls(allBalls);

          if (data.radar_ranking) setRadarRanking(data.radar_ranking);
          if (data.prize_1_winner) setPrize1Winner(data.prize_1_winner);
          if (data.prize_bingo_winner) setPrizeBingoWinner(data.prize_bingo_winner);
          if (data.latest_announcement) setLatestAnnouncement(data.latest_announcement);

          // Transmitir a todos los espectadores conectados en tiempo real
          if (channelRef.current) {
            channelRef.current.send({
              type: 'broadcast',
              event: 'server_ball_drawn',
              payload: data
            });
          }

          // Locución oficial: si hay premio, proclamar al ganador; si no, dictar la bolilla
          if (data.prize_speech_text) {
            speakDirect(data.prize_speech_text, { pitch: 1.18, rate: 1.05 });
          } else {
            speakBallAndPrize(newBall);
          }

          if (data.is_paused_by_prize) {
            setIsAutoDrawing(false);
            setDrawPausedByPrize(true);
          }

          return { success: true, ball: newBall };
        } else {
          const errMsg = data?.error || 'No se pudo registrar la bolilla';
          alert(errMsg);
          return { success: false, error: errMsg };
        }
      } else {
        // Fallback local en caso de estar offline
        const updatedBalls = [num, ...calledBalls];
        setCurrentBall({
          num: num,
          range: num <= 18 ? 1 : num <= 36 ? 2 : num <= 54 ? 3 : num <= 72 ? 4 : 5
        });
        setCalledBalls(updatedBalls);
        speakBallAndPrize(num);
        return { success: true, ball: num };
      }
    } catch (err) {
      console.error("Error al registrar bolilla manual:", err);
      return { success: false, error: err.message };
    } finally {
      setIsDrawingBall(false);
    }
  }, [calledBalls, isDrawingBall, speakBallAndPrize, speakDirect]);

  // 2. Alternar sorteo automático
  const toggleAutoDraw = useCallback(() => {
    const nextState = !isAutoDrawing;
    setIsAutoDrawing(nextState);
    if (nextState) setDrawPausedByPrize(false);

    if (channelRef.current) {
      channelRef.current.send({
        type: 'broadcast',
        event: 'auto_draw_toggle',
        payload: {
          isAutoDrawing: nextState,
          drawPausedByPrize: false
        }
      });
    }
  }, [isAutoDrawing]);

  // 3. Reiniciar partida (Server-Side)
  const resetGame = useCallback(async () => {
    const confirmed = window.confirm("¿Estás seguro de que deseas iniciar una nueva partida? Se reiniciarán todas las bolillas cantadas, premios y el radar a cero.");
    if (!confirmed) return;

    // Reset instantáneo de estado local
    lastSpokenBallRef.current = null;
    setCalledBalls([]);
    setCurrentBall(null);
    setIsAutoDrawing(false);
    setDrawPausedByPrize(false);
    setCountdown(5);
    setPrize1Winner(null);
    setPrizeBingoWinner(null);
    setPrizeCardRaffleWinner(null);
    setLatestAnnouncement(null);
    setRaffleDisplayCode('CR-?????');
    setRadarRanking(prev => prev.map(c => ({
      ...c,
      matchedCount: 0,
      missingCount: 15,
      missingNumbers: c.allNumbers || (c.grid ? c.grid.flat().filter(n => n !== null && n !== undefined) : []),
      completedRows: 0,
      rowHits: [0, 0, 0]
    })));

    speakDirect("¡Comienza una nueva partida de Bingo!", { pitch: 1.15 });

    if (!window.supabaseClient) return;

    try {
      const { data, error } = await window.supabaseClient.rpc('reset_game_session', { 
        p_room_id: 'main-room',
        p_admin_pin: getEffectiveAdminPin()
      });
      if (error) {
        console.error("Error al reiniciar partida en Supabase:", error);
        alert("Error al sincronizar reinicio con Supabase: " + (error.message || error));
        return;
      }

      if (data && data.success) {
        if (data.radar_ranking) {
          setRadarRanking(data.radar_ranking);
        }

        if (channelRef.current) {
          channelRef.current.send({
            type: 'broadcast',
            event: 'server_game_reset',
            payload: {
              radar_ranking: data.radar_ranking,
              current_round: currentRound,
              prize_bingo_amount: roundBingoPrize
            }
          });
        }
      }
    } catch (err) {
      console.error("Error al reiniciar partida en Supabase:", err);
      alert("Error inesperado al reiniciar: " + err.message);
    }
  }, [speakDirect, currentRound, roundBingoPrize]);

  // 3b. Cambiar de Premio Oficial (Premio 1: $100.000 Bingo, Premio 2: $50.000, Premio 3: $50.000)
  const setActiveRound = useCallback(async (roundNum) => {
    const round = parseInt(roundNum, 10);
    if (![1, 2, 3].includes(round)) return;
    const bingoMonto = round === 1 ? 100000 : 50000;
    const roundName = round === 1 ? 'Premio 1 (Pozo Mayor $100.000)' : round === 2 ? 'Premio 2 ($50.000)' : 'Premio 3 ($50.000)';
    const confirmed = window.confirm(`¿Situarse en ${roundName}? Se activará el sorteo de ${roundName} y se reiniciarán las bolillas para esta etapa.`);
    if (!confirmed) return;

    setCurrentRound(round);
    setRoundBingoPrize(bingoMonto);
    setRoundLineaPrize(10000);
    setRoundCartonPrize(10000);
    lastSpokenBallRef.current = null;
    setCalledBalls([]);
    setCurrentBall(null);
    setIsAutoDrawing(false);
    setDrawPausedByPrize(false);
    setCountdown(5);
    setPrize1Winner(null);
    setPrizeBingoWinner(null);
    setPrizeCardRaffleWinner(null);
    setLatestAnnouncement(null);
    setRaffleDisplayCode('CR-?????');
    setRadarRanking(prev => prev.map(c => ({
      ...c,
      matchedCount: 0,
      missingCount: 15,
      missingNumbers: c.allNumbers || (c.grid ? c.grid.flat().filter(n => n !== null && n !== undefined) : []),
      completedRows: 0,
      rowHits: [0, 0, 0]
    })));

    const introSpeech = round === 1 
      ? `¡Atención señoras y señores! ¡Llegó el momento del Gran Premio Mayor de Bingo la Mami! Pozo en juego: 100.000 pesos en Bingo y 10.000 pesos en Línea. ¡Mucha suerte a todos!`
      : round === 2 
      ? `¡Atención señoras y señores! Se inicia el sorteo del Premio 2 de Bingo la Mami. Pozo en juego: 50.000 pesos en Bingo y 10.000 pesos en Línea. ¡Mucha suerte a todos!`
      : `¡Atención señoras y señores! Se inicia el primer sorteo: Premio 3 de Bingo la Mami. Pozo en juego: 50.000 pesos en Bingo y 10.000 pesos en Línea. ¡Mucha suerte a todos!`;

    speakDirect(introSpeech, { pitch: 1.15, rate: 1.05 });

    if (!window.supabaseClient) return;

    try {
      const { data, error } = await window.supabaseClient.rpc('set_active_round', {
        p_room_id: 'main-room',
        p_round: round,
        p_admin_pin: getEffectiveAdminPin()
      });

      if (error) {
        console.error("Error al cambiar ronda en Supabase:", error);
        alert("Error al cambiar ronda en Supabase: " + (error.message || error));
        return;
      }

      if (data && data.success) {
        if (data.radar_ranking) {
          setRadarRanking(data.radar_ranking);
        }
        if (channelRef.current) {
          channelRef.current.send({
            type: 'broadcast',
            event: 'server_round_changed',
            payload: {
              round: round,
              prize_bingo_amount: bingoMonto,
              prize_linea_amount: 10000,
              prize_carton_amount: 10000,
              radar_ranking: data.radar_ranking
            }
          });
        }
      }
    } catch (err) {
      console.error("Error inesperado al cambiar de ronda:", err);
      alert("Error inesperado: " + err.message);
    }
  }, [speakDirect]);

  // 4. Sorteo especial de cartón (Server-Side)
  const raffleCardNumber = useCallback(async () => {
    if (!window.supabaseClient) return;
    setIsRafflingCard(true);

    let counter = 0;
    const interval = setInterval(async () => {
      const pool = realRoomCards.length > 0 ? realRoomCards : [];
      if (pool.length > 0) {
        const randomCard = pool[Math.floor(Math.random() * pool.length)];
        setRaffleDisplayCode(randomCard.id);
      }
      counter++;

      if (counter > 25) {
        clearInterval(interval);
        try {
          const { data, error } = await window.supabaseClient.rpc('raffle_card_number', { 
            p_room_id: 'main-room',
            p_admin_pin: getEffectiveAdminPin()
          });
          setIsRafflingCard(false);

          if (!error && data.success) {
            setPrizeCardRaffleWinner(data.winner_data);
            setRaffleDisplayCode(data.winner_data.cardId);
            setLatestAnnouncement(data.announcement);

            if (channelRef.current) {
              channelRef.current.send({
                type: 'broadcast',
                event: 'server_card_raffle_winner',
                payload: data
              });
            }

            speakDirect(`¡¡ATENCIÓN SEÑORAS Y SEÑORES!! ¡¡LLEGAMOS AL GRAN SORTEO FINAL POR NÚMERO DE CARTÓN DE BINGO LA MAMI!! ¡¡EL CARTÓN GANADOR DE LOS DIEZ MIL PESOS ES DE ${data.winner_data.title} CON EL NÚMERO ${data.winner_data.cardId}!! ¡¡MUCHAS FELICITACIONES!!`, { pitch: 1.2, rate: 1.05 });
          }
        } catch (err) {
          setIsRafflingCard(false);
        }
      }
    }, 80);
  }, [realRoomCards, speakDirect]);

  // Temporizador de sorteo automático oficial (5 segundos para ritmo ágil)
  useEffect(() => {
    let timer = null;
    if (isAutoDrawing && !drawPausedByPrize) {
      timer = setInterval(() => {
        setCountdown(prev => {
          if (prev <= 1) {
            drawNextBall();
            return 5;
          }
          return prev - 1;
        });
      }, 1000);
    } else {
      setCountdown(5);
    }
    return () => {
      if (timer) clearInterval(timer);
    };
  }, [isAutoDrawing, drawPausedByPrize, drawNextBall]);

  // Actualizar y sincronizar enlace de video del sorteo (YouTube, Twitch, Meet, etc.)
  const updateVideoUrl = useCallback(async (newUrl) => {
    const cleanUrl = (newUrl || '').trim();
    setVideoUrl(cleanUrl);
    try {
      localStorage.setItem('bingo_room_video_url', cleanUrl);
    } catch (e) {}

    if (window.supabaseClient) {
      try {
        await window.supabaseClient.rpc('set_room_video_url', {
          p_video_url: cleanUrl,
          p_room_id: 'main-room',
          p_admin_pin: getEffectiveAdminPin()
        });
      } catch (err) {
        console.warn("Error guardando video_url en Supabase:", err);
      }

      if (channelRef.current) {
        channelRef.current.send({
          type: 'broadcast',
          event: 'video_url_update',
          payload: { video_url: cleanUrl }
        });
      }
    }
  }, []);

  // Habilitar / Cerrar Venta de Cartones (Persistencia en Supabase + Broadcast)
  const toggleSalesStatus = useCallback(async (forcedStatus = null) => {
    // Protección de Seguridad: Solo el Organizador Oficial (admin) puede cambiar el estado de venta
    if (userRole !== 'admin') {
      alert("Acceso denegado: Solo el Organizador Oficial puede habilitar o cerrar la venta de cartones.");
      return;
    }

    const newStatus = forcedStatus !== null ? forcedStatus : !isSalesOpen;
    const confirmMsg = newStatus 
      ? "¿Deseas HABILITAR y ABRIR la venta oficial de cartones a todos los jugadores y vendedores?"
      : "¿Deseas CERRAR la venta de cartones? Los jugadores y vendedores ya no podrán adquirir ni emitir nuevos cartones para este sorteo.";
    
    if (!window.confirm(confirmMsg)) return;

    setIsSalesOpen(newStatus);
    try {
      localStorage.setItem('bingo_sales_open', String(newStatus));
    } catch (e) {}

    // 1. Broadcast instantáneo a todos los clientes conectados
    if (channelRef.current) {
      channelRef.current.send({
        type: 'broadcast',
        event: 'server_sales_status',
        payload: { sales_open: newStatus }
      });
    }

    // 2. Dictado de locutor
    if (newStatus) {
      speakDirect("¡Atención! La venta oficial de cartones ha sido ABIERTA.", { pitch: 1.1 });
    } else {
      speakDirect("¡Atención! La venta oficial de cartones para este sorteo se encuentra CERRADA.", { pitch: 1.05 });
    }

    // 3. Persistencia en Supabase game_sessions
    if (window.supabaseClient) {
      try {
        const updatedAnnouncement = {
          ...(latestAnnouncement || {}),
          sales_open: newStatus,
          sales_updated_at: new Date().toISOString()
        };
        setLatestAnnouncement(updatedAnnouncement);

        await window.supabaseClient
          .from('game_sessions')
          .update({ 
            latest_announcement: updatedAnnouncement,
            status: newStatus ? 'waiting' : 'sales_closed',
            updated_at: new Date().toISOString()
          })
          .eq('id', 'main-room');
      } catch (err) {
        console.warn("Error guardando estado de venta en Supabase:", err);
      }
    }
  }, [isSalesOpen, latestAnnouncement, speakDirect, userRole]);

  const value = useMemo(() => ({
    // Bolillero y Sorteo
    calledBalls,
    currentBall,
    isAutoDrawing,
    drawPausedByPrize,
    countdown,
    showMatrix,
    setShowMatrix,
    voiceEnabled,
    setVoiceEnabled,
    availableVoices,
    selectedVoiceURI,
    setSelectedVoiceURI,
    speechRate,
    setSpeechRate,
    speechPitch,
    setSpeechPitch,
    voiceStyle,
    setVoiceStyle,
    testVoice,
    speakPhrase,
    speakBallAndPrize,
    // Video en Vivo del Bolillero
    videoUrl,
    updateVideoUrl,
    // Premios y Rondas Oficiales ("Bingo la Mami")
    currentRound,
    roundBingoPrize,
    roundLineaPrize,
    roundCartonPrize,
    prize1Winner,
    prizeBingoWinner,
    prizeCardRaffleWinner,
    isRafflingCard,
    raffleDisplayCode,
    latestAnnouncement,
    setLatestAnnouncement,
    // Base de Datos & Radar Server-Side
    realRoomCards,
    radarRanking,
    receipts,
    loadingDb,
    reloadDatabase: loadDatabaseData,
    reloadGameState: loadServerGameState,
    // Estado Oficial de Ventas (Abierta / Cerrada)
    isSalesOpen,
    setIsSalesOpen,
    toggleSalesStatus,
    // Presencia y Conexión
    viewersCount,
    channelStatus,
    // Rol y Autenticación del Contexto
    userRole,
    userName,
    adminPin: getEffectiveAdminPin(),
    // Acciones Server-Side
    drawNextBall,
    callSpecificBall,
    toggleAutoDraw,
    resetGame,
    setActiveRound,
    raffleCardNumber,
    speakDirect
  }), [
    calledBalls,
    currentBall,
    isAutoDrawing,
    drawPausedByPrize,
    countdown,
    showMatrix,
    voiceEnabled,
    availableVoices,
    selectedVoiceURI,
    speechRate,
    speechPitch,
    voiceStyle,
    testVoice,
    speakBallAndPrize,
    prize1Winner,
    prizeBingoWinner,
    prizeCardRaffleWinner,
    isRafflingCard,
    raffleDisplayCode,
    latestAnnouncement,
    realRoomCards,
    radarRanking,
    receipts,
    loadingDb,
    loadDatabaseData,
    loadServerGameState,
    isSalesOpen,
    toggleSalesStatus,
    viewersCount,
    channelStatus,
    drawNextBall,
    callSpecificBall,
    toggleAutoDraw,
    resetGame,
    setActiveRound,
    raffleCardNumber,
    speakDirect,
    videoUrl,
    updateVideoUrl,
    currentRound,
    roundBingoPrize,
    roundLineaPrize,
    roundCartonPrize,
    userRole,
    userName,
    getEffectiveAdminPin
  ]);

  return (
    <BingoRoomContext.Provider value={value}>
      {children}
    </BingoRoomContext.Provider>
  );
}

// Hook de acceso
function useBingoRoom() {
  const context = useContext(BingoRoomContext);
  if (!context) {
    throw new Error("useBingoRoom debe usarse dentro de un BingoRoomProvider");
  }
  return context;
}

window.BingoRoomContext = BingoRoomContext;
window.BingoRoomProvider = BingoRoomProvider;
window.useBingoRoom = useBingoRoom;

// Alias para arquitectura modular
window.SupabaseProvider = BingoRoomProvider;
window.useSupabaseRoom = useBingoRoom;

