package com.saas.voip.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.saas.admin.service.PhoneNumberService;
import com.saas.shared.dto.AiCostCalculationResult;
import com.saas.shared.service.ai.AiCostTrackingService;
import com.saas.voip.service.ai.GeminiCostCalculator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import jakarta.annotation.PostConstruct;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import jakarta.websocket.ContainerProvider;
import jakarta.websocket.WebSocketContainer;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Gemini 2.0 Flash Realtime Service
 * 
 * Integrates Gemini 2.0 Flash Multimodal Live API with Twilio Media Streams
 * 
 * Architecture:
 * - Twilio sends mulaw audio (8kHz) via WebSocket
 * - This service converts mulaw → PCM 16kHz for Gemini
 * - Gemini returns PCM audio (24kHz)
 * - This service converts PCM 24kHz → mulaw 8kHz for Twilio
 * 
 * Cost Tracking:
 * - Captures usage tokens from Gemini responses
 * - Calculates costs via GeminiCostCalculator
 * - Persists to admin database via AiCostTrackingService
 * 
 * Pricing (as of Oct 2025):
 * - Audio input: $0.30 per 1M tokens
 * - Audio output: $1.20 per 1M tokens
 * - 16x cheaper than OpenAI Realtime API!
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class GeminiRealtimeService {

    private final ObjectMapper objectMapper;
    private final PhoneNumberService phoneNumberService;
    private final GeminiCostCalculator geminiCostCalculator;
    private final AiCostTrackingService aiCostTrackingService;

    @Value("${gemini.api.key:#{null}}")
    private String geminiApiKey;
    
    @Value("${gemini.realtime.ws-url}")
    private String geminiWsUrl;
    
    @Value("${gemini.realtime.model:gemini-2.0-flash-exp}")
    private String geminiModel;

    private final Map<String, WebSocketSession> geminiSessions = new ConcurrentHashMap<>();
    private final Map<String, UsageData> sessionUsage = new ConcurrentHashMap<>();
    
    /**
     * Session registry - Maps streamSid to Twilio WebSocket session
     * Allows sending audio back from Gemini to Twilio
     */
    private final Map<String, WebSocketSession> twilioSessions = new ConcurrentHashMap<>();
    
    /**
     * Gemini WebSocket URL and model configuration now externalized to application.yml
     * - gemini.realtime.ws-url
     * - gemini.realtime.model
     */
    
    /**
     * Audio format constants
     */
    private static final int GEMINI_INPUT_SAMPLE_RATE = 16000;  // Gemini expects 16kHz
    private static final int GEMINI_OUTPUT_SAMPLE_RATE = 24000; // Gemini returns 24kHz
    private static final int TWILIO_SAMPLE_RATE = 8000;         // Twilio uses 8kHz
    
    /**
     * Inner class to track token usage per session
     */
    private static class UsageData {
        long audioInputTokens = 0;
        long audioOutputTokens = 0;
        long textInputTokens = 0;
        long textOutputTokens = 0;
        String tenantId;
        String phoneNumber;
        String callSid;
        LocalDateTime startTime;
        LocalDateTime endTime;
    }

    /**
     * Startup validation - Log Gemini API key status
     */
    @PostConstruct
    public void init() {
        if (geminiApiKey == null || geminiApiKey.isEmpty() || geminiApiKey.equals("your-gemini-api-key-here")) {
            log.error("❌❌❌ [Gemini] API key NOT configured! Set GEMINI_API_KEY environment variable.");
        } else {
            String maskedKey = geminiApiKey.substring(0, Math.min(10, geminiApiKey.length())) + "...";
            log.info("✅ [Gemini] API key configured: {}", maskedKey);
        }
    }

    /**
     * Connect to Gemini Realtime API
     * 
     * @param sessionId Twilio stream session ID
     * @param systemInstructions System instructions for Gemini
     * @param voiceName Voice name (Aoede, Charon, Fenrir, Kore, Puck)
     * @param phoneNumber Phone number for tenant resolution
     * @param callSid Twilio call SID for cost tracking
     * @return true if connection successful
     */
    public boolean connectToGemini(String sessionId, String systemInstructions, 
                                    String voiceName, String phoneNumber, String callSid) {
        try {
            if (geminiApiKey == null || geminiApiKey.isEmpty()) {
                log.error("❌ [Gemini] API key not configured");
                return false;
            }

            // Resolve tenant from phone number
            String tenantId = null;
            if (phoneNumber != null && !phoneNumber.isEmpty()) {
                Optional<String> tenantOpt = phoneNumberService.getTenantIdByPhoneNumber(phoneNumber);
                tenantId = tenantOpt.orElse("default_tenant");
            } else {
                tenantId = "default_tenant";
            }

            // Initialize usage tracking
            UsageData usageData = new UsageData();
            usageData.tenantId = tenantId;
            usageData.phoneNumber = phoneNumber;
            usageData.callSid = callSid;
            usageData.startTime = LocalDateTime.now();
            sessionUsage.put(sessionId, usageData);

            log.info("🔗 [Gemini] Connecting to Multimodal Live API for session: {}", sessionId);
            log.info("📞 [Gemini] Phone: {}, Tenant: {}", phoneNumber, tenantId);

            // Build WebSocket URL with API key
            String wsUrl = geminiWsUrl + "?key=" + geminiApiKey;
            
            // Create WebSocket client with increased buffer size
            WebSocketContainer container = ContainerProvider.getWebSocketContainer();
            container.setDefaultMaxTextMessageBufferSize(256 * 1024);  // 256 KB for large JSON responses
            container.setDefaultMaxBinaryMessageBufferSize(256 * 1024); // 256 KB for binary messages
            
            log.debug("📊 [Gemini] WebSocket buffer configured - Text: 256 KB, Binary: 256 KB");
            
            StandardWebSocketClient client = new StandardWebSocketClient(container);
            
            // Create handler for Gemini responses
            TextWebSocketHandler handler = new TextWebSocketHandler() {
                @Override
                protected void handleTextMessage(WebSocketSession session, TextMessage message) {
                    handleGeminiMessage(sessionId, message.getPayload());
                }
                
                @Override
                protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
                    try {
                        // Get ByteBuffer and ensure LITTLE-ENDIAN byte order (Gemini standard)
                        ByteBuffer buffer = message.getPayload();
                        buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN);
                        byte[] pcm24kData = new byte[buffer.remaining()];
                        buffer.get(pcm24kData);
                        
                        log.debug("🔊 [Gemini] Audio OUT - Session: {}, PCM24k: {} bytes", sessionId, pcm24kData.length);
                        
                        // DEBUG: Log first 10 PCM samples to verify byte order
                        if (pcm24kData.length >= 20) {
                            StringBuilder samplesDebug = new StringBuilder("🔍 First 10 PCM samples: ");
                            for (int i = 0; i < 10 && (i * 2 + 1) < pcm24kData.length; i++) {
                                // Little-endian
                                short littleEndian = (short) ((pcm24kData[i*2] & 0xFF) | ((pcm24kData[i*2+1] & 0xFF) << 8));
                                // Big-endian  
                                short bigEndian = (short) (((pcm24kData[i*2] & 0xFF) << 8) | (pcm24kData[i*2+1] & 0xFF));
                                samplesDebug.append(String.format("[LE:%d BE:%d] ", littleEndian, bigEndian));
                            }
                            log.info(samplesDebug.toString());
                        }
                        
                        // Skip tiny audio chunks (< 100 bytes = silence)
                        if (pcm24kData.length < 100) {
                            log.debug("⏭️ [Gemini] Skipping tiny audio chunk (silence): {} bytes", pcm24kData.length);
                            return;
                        }
                        
                        // Convert PCM 24kHz → mulaw 8kHz for Twilio
                        byte[] mulawData = convertPcm24kToMulaw(pcm24kData);
                        log.debug("🔄 [Gemini] Converted PCM24k ({} bytes) → Mulaw ({} bytes)", pcm24kData.length, mulawData.length);
                        
                        // Encode to Base64
                        String base64Mulaw = Base64.getEncoder().encodeToString(mulawData);
                        
                        // Send to Twilio
                        sendAudioToTwilio(sessionId, base64Mulaw);
                        
                    } catch (Exception e) {
                        log.error("❌ [Gemini] Error processing binary audio for session: {}", sessionId, e);
                    }
                }
                
                @Override
                public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
                    log.warn("🔌 [Gemini] Connection closed - Session: {}, Status: {}", sessionId, status);
                    // Extract callSid from sessionUsage Map
                    UsageData usage = sessionUsage.get(sessionId);
                    String extractedCallSid = (usage != null) ? usage.callSid : null;
                    // Trigger cleanup and cost saving
                    disconnectFromGemini(sessionId, extractedCallSid);
                }
                
                @Override
                public void handleTransportError(WebSocketSession session, Throwable exception) {
                    log.error("❌ [Gemini] Transport error - Session: {}", sessionId, exception);
                    // Extract callSid from sessionUsage Map
                    UsageData usage = sessionUsage.get(sessionId);
                    String extractedCallSid = (usage != null) ? usage.callSid : null;
                    // Trigger cleanup and cost saving
                    disconnectFromGemini(sessionId, extractedCallSid);
                }
            };

            // Connect to Gemini
            WebSocketSession geminiSession = client.doHandshake(handler, wsUrl).get();
            geminiSessions.put(sessionId, geminiSession);

            // Send setup message
            sendSetupMessage(geminiSession, systemInstructions, voiceName);
            
            // CRITICAL: Send initial prompt to force Gemini to greet proactively
            // Without this, Gemini waits for user to speak first (results in silence)
            sendInitialPrompt(geminiSession);

            log.info("✅ [Gemini] Connected successfully for session: {}", sessionId);
            return true;

        } catch (Exception e) {
            log.error("❌ [Gemini] Connection failed for session: {}", sessionId, e);
            return false;
        }
    }

    /**
     * Send setup message to Gemini (required immediately after connection)
     * Per Google docs: responseModalities and speechConfig MUST be under generationConfig
     */
    private void sendSetupMessage(WebSocketSession session, String systemInstructions, String voiceName) {
        try {
            // Default voice if not specified
            if (voiceName == null || voiceName.isEmpty()) {
                voiceName = "Puck"; // Default: friendly male voice
            }

            // Build setup message per official Gemini Live API docs
            Map<String, Object> setup = new HashMap<>();
            setup.put("model", "models/" + geminiModel);
            
            // Generation config (REQUIRED wrapper for audio response)
            Map<String, Object> generationConfig = new HashMap<>();
            generationConfig.put("responseModalities", "audio"); // String, not array
            
            // Speech config (under generationConfig)
            Map<String, Object> speechConfig = new HashMap<>();
            Map<String, Object> voiceConfig = new HashMap<>();
            Map<String, Object> prebuiltVoiceConfig = new HashMap<>();
            prebuiltVoiceConfig.put("voiceName", voiceName);
            voiceConfig.put("prebuiltVoiceConfig", prebuiltVoiceConfig);
            speechConfig.put("voiceConfig", voiceConfig);
            generationConfig.put("speechConfig", speechConfig);
            
            setup.put("generationConfig", generationConfig);
            
            // System instruction (under setup, not generationConfig)
            if (systemInstructions != null && !systemInstructions.isEmpty()) {
                Map<String, Object> systemInstruction = new HashMap<>();
                Map<String, Object> part = new HashMap<>();
                part.put("text", systemInstructions);
                systemInstruction.put("parts", new Map[]{part});
                setup.put("systemInstruction", systemInstruction);
            }
            
            Map<String, Object> message = new HashMap<>();
            message.put("setup", setup);
            
            String json = objectMapper.writeValueAsString(message);
            log.debug("📤 [Gemini] Sending setup JSON: {}", json.length() > 300 ? json.substring(0, 300) + "..." : json);
            session.sendMessage(new TextMessage(json));
            
            log.info("📤 [Gemini] Setup message sent - Model: {}, Voice: {}", geminiModel, voiceName);
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Failed to send setup message", e);
        }
    }
    
    /**
     * Send initial prompt to force Gemini to greet proactively
     * Without this, Gemini waits for user to speak first (results in silence)
     * 
     * Per Gemini Live API docs:
     * {
     *   "clientContent": {
     *     "turns": [
     *       {
     *         "role": "user",
     *         "parts": [{"text": "..."}]
     *       }
     *     ],
     *     "turnComplete": true
     *   }
     * }
     */
    private void sendInitialPrompt(WebSocketSession session) {
        try {
            // Build user message to trigger immediate greeting
            Map<String, Object> part = new HashMap<>();
            part.put("text", "Salue l'utilisateur et présente-toi brièvement.");
            
            Map<String, Object> turn = new HashMap<>();
            turn.put("role", "user");
            turn.put("parts", new Map[]{part});
            
            Map<String, Object> clientContent = new HashMap<>();
            clientContent.put("turns", new Map[]{turn});
            clientContent.put("turnComplete", true);
            
            Map<String, Object> message = new HashMap<>();
            message.put("clientContent", clientContent);
            
            String json = objectMapper.writeValueAsString(message);
            log.debug("📤 [Gemini] Sending initial prompt: {}", json);
            session.sendMessage(new TextMessage(json));
            
            log.info("✅ [Gemini] Initial prompt sent - Gemini will greet user proactively");
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Failed to send initial prompt", e);
        }
    }

    /**
     * Send audio chunk to Gemini
     * Converts Twilio's mulaw 8kHz to Gemini's PCM 16kHz
     * 
     * @param sessionId Twilio stream session ID
     * @param base64Audio Base64-encoded mulaw audio from Twilio
     */
    public void sendAudioToGemini(String sessionId, String base64Audio) {
        try {
            WebSocketSession geminiSession = geminiSessions.get(sessionId);
            if (geminiSession == null || !geminiSession.isOpen()) {
                log.warn("⚠️ [Gemini] No active session for: {}", sessionId);
                return;
            }

            // Decode mulaw audio from Twilio
            byte[] mulawData = Base64.getDecoder().decode(base64Audio);
            
            // Convert mulaw 8kHz → PCM 16kHz for Gemini
            byte[] pcm16kData = convertMulawToPcm16k(mulawData);
            
            log.debug("🎤 [Gemini] Audio IN - Session: {}, Mulaw: {} bytes → PCM16k: {} bytes", 
                    sessionId, mulawData.length, pcm16kData.length);
            
            // Encode to Base64
            String base64Pcm = Base64.getEncoder().encodeToString(pcm16kData);
            
            // Build realtimeInput message for Gemini
            Map<String, Object> mediaChunk = new HashMap<>();
            mediaChunk.put("mimeType", "audio/pcm;rate=16000");
            mediaChunk.put("data", base64Pcm);
            
            Map<String, Object> realtimeInput = new HashMap<>();
            realtimeInput.put("mediaChunks", new Map[]{mediaChunk});
            
            Map<String, Object> message = new HashMap<>();
            message.put("realtimeInput", realtimeInput);
            
            String json = objectMapper.writeValueAsString(message);
            geminiSession.sendMessage(new TextMessage(json));
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Error sending audio for session: {}", sessionId, e);
        }
    }

    /**
     * Handle incoming message from Gemini
     */
    private void handleGeminiMessage(String sessionId, String payload) {
        try {
            // LOG EVERY MESSAGE RECEIVED
            log.info("📩 [Gemini] Received from API - Session: {}, Length: {} bytes", sessionId, payload.length());
            log.debug("📩 [Gemini] Raw payload: {}", payload.length() > 500 ? payload.substring(0, 500) + "..." : payload);
            
            JsonNode response = objectMapper.readTree(payload);
            
            // Check for setup acknowledgment
            if (response.has("setupComplete")) {
                log.info("✅ [Gemini] Setup complete for session: {}", sessionId);
                return;
            }
            
            // Check for server content (AI response)
            if (response.has("serverContent")) {
                JsonNode serverContent = response.get("serverContent");
                
                // Extract audio from modelTurn
                if (serverContent.has("modelTurn")) {
                    JsonNode modelTurn = serverContent.get("modelTurn");
                    if (modelTurn.has("parts")) {
                        for (JsonNode part : modelTurn.get("parts")) {
                            if (part.has("inlineData")) {
                                JsonNode inlineData = part.get("inlineData");
                                String mimeType = inlineData.get("mimeType").asText();
                                String audioData = inlineData.get("data").asText();
                                
                                if (mimeType.contains("audio/pcm")) {
                                    // Process audio response
                                    processGeminiAudioResponse(sessionId, audioData);
                                }
                            }
                        }
                    }
                }
                
                // Capture usage metrics if present
                if (serverContent.has("groundingMetadata") || response.has("usageMetadata")) {
                    captureUsageMetrics(sessionId, response);
                }
                
                // Check if turn is complete
                if (serverContent.has("turnComplete") && serverContent.get("turnComplete").asBoolean()) {
                    log.debug("🔄 [Gemini] Turn complete for session: {}", sessionId);
                }
            }
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Error handling response for session: {}", sessionId, e);
        }
    }

    /**
     * Process audio response from Gemini
     * Converts Gemini's PCM 24kHz to Twilio's mulaw 8kHz
     */
    private void processGeminiAudioResponse(String sessionId, String base64PcmAudio) {
        try {
            // Decode PCM audio from Gemini (24kHz)
            byte[] pcm24kData = Base64.getDecoder().decode(base64PcmAudio);
            
            // Convert PCM 24kHz → mulaw 8kHz for Twilio
            byte[] mulawData = convertPcm24kToMulaw(pcm24kData);
            
            log.debug("🔊 [Gemini] Audio OUT - Session: {}, PCM24k: {} bytes → Mulaw: {} bytes", 
                    sessionId, pcm24kData.length, mulawData.length);
            
            // Encode to Base64
            String base64Mulaw = Base64.getEncoder().encodeToString(mulawData);
            
            // Send to Twilio via TwilioWebSocketHandler
            // (This will be handled by the handler that manages the Twilio connection)
            sendAudioToTwilio(sessionId, base64Mulaw);
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Error processing audio response for session: {}", sessionId, e);
        }
    }

    /**
     * Capture usage metrics from Gemini response
     */
    private void captureUsageMetrics(String sessionId, JsonNode response) {
        try {
            UsageData usage = sessionUsage.get(sessionId);
            if (usage == null) return;
            
            // Gemini may include usageMetadata in different places
            JsonNode usageMetadata = null;
            if (response.has("usageMetadata")) {
                usageMetadata = response.get("usageMetadata");
            } else if (response.has("serverContent") && 
                       response.get("serverContent").has("usageMetadata")) {
                usageMetadata = response.get("serverContent").get("usageMetadata");
            }
            
            if (usageMetadata != null) {
                if (usageMetadata.has("audioInputTokens")) {
                    usage.audioInputTokens += usageMetadata.get("audioInputTokens").asLong();
                }
                if (usageMetadata.has("audioOutputTokens")) {
                    usage.audioOutputTokens += usageMetadata.get("audioOutputTokens").asLong();
                }
                if (usageMetadata.has("textInputTokens")) {
                    usage.textInputTokens += usageMetadata.get("textInputTokens").asLong();
                }
                if (usageMetadata.has("textOutputTokens")) {
                    usage.textOutputTokens += usageMetadata.get("textOutputTokens").asLong();
                }
                
                log.debug("💰 [Gemini] Usage captured - Session: {}, AudioIn: {}, AudioOut: {}, TextIn: {}, TextOut: {}", 
                         sessionId, usage.audioInputTokens, usage.audioOutputTokens,
                         usage.textInputTokens, usage.textOutputTokens);
            }
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Error capturing usage metrics for session: {}", sessionId, e);
        }
    }

    /**
     * Disconnect from Gemini and save AI costs
     */
    public void disconnectFromGemini(String sessionId, String callSid) {
        try {
            // Close Gemini
            WebSocketSession geminiSession = geminiSessions.remove(sessionId);
            if (geminiSession != null && geminiSession.isOpen()) {
                geminiSession.close();
            }
            
            // ALSO close Twilio session for clean shutdown
            WebSocketSession twilioSession = twilioSessions.remove(sessionId);
            if (twilioSession != null && twilioSession.isOpen()) {
                twilioSession.close();
            }
            
            log.info("🔌 [Gemini] Disconnected session: {}", sessionId);

        } catch (Exception e) {
            log.error("❌ [Gemini] Error disconnecting session: {}", sessionId, e);
        } finally {
            // Save costs BEFORE removing usage data
            try {
                saveAiCostsForSession(sessionId, callSid);
            } catch (Exception e) {
                log.error("❌ [Gemini] Error saving costs for session: {}", sessionId, e);
            }
            sessionUsage.remove(sessionId);
        }
    }

    /**
     * Calculate and save AI costs to admin database
     */
    private void saveAiCostsForSession(String sessionId, String callSid) {
        try {
            UsageData usage = sessionUsage.get(sessionId);
            if (usage == null) {
                log.warn("⚠️ [Gemini] No usage data found for session: {}", sessionId);
                return;
            }

            usage.endTime = LocalDateTime.now();

            // Check if any tokens were used
            long totalTokens = usage.audioInputTokens + usage.audioOutputTokens + 
                              usage.textInputTokens + usage.textOutputTokens;
            
            if (totalTokens == 0) {
                log.info("💰 [Gemini] No usage detected for session: {}, skipping cost tracking", sessionId);
                return;
            }

            // Prepare usage metrics for calculator
            Map<String, Object> usageMetrics = new HashMap<>();
            usageMetrics.put("audio_input_tokens", usage.audioInputTokens);
            usageMetrics.put("audio_output_tokens", usage.audioOutputTokens);
            usageMetrics.put("text_input_tokens", usage.textInputTokens);
            usageMetrics.put("text_output_tokens", usage.textOutputTokens);
            usageMetrics.put("model", geminiModel);

            // Calculate cost using GeminiCostCalculator
            AiCostCalculationResult costResult = geminiCostCalculator.calculateCost(usageMetrics);

            if (costResult != null && costResult.isValid()) {
                log.info("💰 [Gemini] Cost calculated - ${} USD for call {}", costResult.getCost(), callSid);

                // Save to admin database
                aiCostTrackingService.saveAiCost(
                    callSid,
                    usage.tenantId,
                    costResult,
                    usage.startTime,
                    usage.endTime,
                    null, // fromNumber
                    usage.phoneNumber
                );

                log.info("✅ [Gemini] AI costs saved to admin database for call {}", callSid);
            } else {
                log.warn("⚠️ [Gemini] Invalid cost calculation result for session: {}", sessionId);
            }

        } catch (Exception e) {
            log.error("❌ [Gemini] Error saving AI costs for session: {}", sessionId, e);
        }
    }

    /**
     * Convert mulaw 8kHz to PCM 16kHz
     * Twilio format → Gemini input format
     * 
     * CRITICAL: Gemini expects LITTLE-ENDIAN PCM (low byte first)
     */
    private byte[] convertMulawToPcm16k(byte[] mulawData) {
        try {
            // Step 1: Decode mulaw to PCM 8kHz (LITTLE-ENDIAN)
            byte[] pcm8k = new byte[mulawData.length * 2]; // 16-bit PCM
            for (int i = 0; i < mulawData.length; i++) {
                short pcmSample = mulawToPcm(mulawData[i]);
                // Write LITTLE-ENDIAN (low byte first, then high byte)
                pcm8k[i * 2] = (byte) (pcmSample & 0xFF);        // Low byte
                pcm8k[i * 2 + 1] = (byte) ((pcmSample >> 8) & 0xFF);  // High byte
            }
            
            // Step 2: Resample 8kHz → 16kHz
            return resampleAudio(pcm8k, TWILIO_SAMPLE_RATE, GEMINI_INPUT_SAMPLE_RATE);
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Error converting mulaw to PCM 16k", e);
            return new byte[0];
        }
    }

    /**
     * Convert PCM 24kHz to mulaw 8kHz
     * Gemini output format → Twilio format
     * 
     * TESTING: Try BIG-ENDIAN byte swap (high byte first) to test endianness hypothesis
     */
    private byte[] convertPcm24kToMulaw(byte[] pcm24kData) {
        try {
            // Parse PCM samples (testing BIG-ENDIAN vs LITTLE-ENDIAN)
            int sampleCount = pcm24kData.length / 2;
            
            // Downsample 24kHz → 8kHz by averaging every 3 consecutive samples
            int outputSampleCount = sampleCount / 3;
            byte[] mulaw = new byte[outputSampleCount];
            
            for (int i = 0; i < outputSampleCount; i++) {
                // Read 3 consecutive PCM samples and average them
                int sum = 0;
                for (int j = 0; j < 3; j++) {
                    int idx = (i * 3 + j) * 2;
                    if (idx + 1 < pcm24kData.length) {
                        // TEST: Try BIG-ENDIAN (high byte first) - SWAP from previous attempt
                        short sample = (short) (
                            ((pcm24kData[idx] & 0xFF) << 8) |    // High byte (swapped)
                            (pcm24kData[idx + 1] & 0xFF)          // Low byte (swapped)
                        );
                        sum += sample;
                    }
                }
                
                // Average the 3 samples
                short avgSample = (short) (sum / 3);
                
                // Convert averaged sample to mulaw
                mulaw[i] = pcmToMulaw(avgSample);
            }
            
            return mulaw;
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Error converting PCM 24k to mulaw", e);
            return new byte[0];
        }
    }

    /**
     * Resample audio using Java AudioSystem
     */
    private byte[] resampleAudio(byte[] audioData, int sourceRate, int targetRate) throws Exception {
        AudioFormat sourceFormat = new AudioFormat(sourceRate, 16, 1, true, false);
        AudioFormat targetFormat = new AudioFormat(targetRate, 16, 1, true, false);
        
        ByteArrayInputStream bais = new ByteArrayInputStream(audioData);
        AudioInputStream sourceStream = new AudioInputStream(bais, sourceFormat, audioData.length / sourceFormat.getFrameSize());
        AudioInputStream targetStream = AudioSystem.getAudioInputStream(targetFormat, sourceStream);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = targetStream.read(buffer)) != -1) {
            baos.write(buffer, 0, bytesRead);
        }
        
        return baos.toByteArray();
    }

    /**
     * Mulaw to PCM conversion (ITU-T G.711)
     */
    private short mulawToPcm(byte mulaw) {
        int sign = (mulaw & 0x80);
        int exponent = (mulaw & 0x70) >> 4;
        int mantissa = mulaw & 0x0F;
        
        int sample = ((mantissa << 3) + 132) << exponent;
        sample -= 132;
        
        if (sign != 0) {
            sample = -sample;
        }
        
        return (short) sample;
    }

    /**
     * PCM to mulaw conversion (ITU-T G.711)
     * CRITICAL: Must invert bits (~) at the end per μ-law specification
     */
    private byte pcmToMulaw(short pcm) {
        int sign = (pcm < 0) ? 0x80 : 0;
        if (sign != 0) pcm = (short) -pcm;
        
        // Clamp to avoid overflow
        if (pcm > 32635) pcm = 32635;
        
        // Add bias
        pcm += 132;
        
        // Find exponent
        int exponent = 7;
        for (int mask = 0x4000; (pcm & mask) == 0 && exponent > 0; exponent--, mask >>= 1);
        
        // Extract mantissa
        int mantissa = (pcm >> (exponent + 3)) & 0x0F;
        
        // CRITICAL FIX: Invert all bits per ITU-T G.711 μ-law standard
        // Without this, audio sounds like static/noise instead of speech
        return (byte) ~(sign | (exponent << 4) | mantissa);
    }

    /**
     * Send audio to Twilio
     * Sends audio from Gemini back to the caller via Twilio Media Stream
     * 
     * @param sessionId Stream session ID
     * @param base64MulawAudio Base64-encoded mulaw audio to send
     */
    private void sendAudioToTwilio(String sessionId, String base64MulawAudio) {
        try {
            WebSocketSession twilioSession = twilioSessions.get(sessionId);
            if (twilioSession == null || !twilioSession.isOpen()) {
                log.warn("⚠️ [Gemini] No active Twilio session for: {}", sessionId);
                return;
            }

            // Build Twilio media message
            // Format: {"event": "media", "streamSid": "...", "media": {"payload": "base64audio"}}
            Map<String, Object> media = new HashMap<>();
            media.put("payload", base64MulawAudio);
            
            Map<String, Object> message = new HashMap<>();
            message.put("event", "media");
            message.put("streamSid", sessionId);
            message.put("media", media);
            
            String json = objectMapper.writeValueAsString(message);
            twilioSession.sendMessage(new TextMessage(json));
            
            log.debug("📤 [Gemini] Audio sent to Twilio for session: {}", sessionId);
            
        } catch (Exception e) {
            log.error("❌ [Gemini] Error sending audio to Twilio for session: {}", sessionId, e);
        }
    }
    
    /**
     * Register Twilio WebSocket session for audio output
     * Called by GeminiSessionHandler when stream starts
     * 
     * @param streamSid Stream session ID
     * @param twilioSession Twilio WebSocket session
     */
    public void registerTwilioSession(String streamSid, WebSocketSession twilioSession) {
        twilioSessions.put(streamSid, twilioSession);
        log.info("✅ [Gemini] Registered Twilio session for stream: {}", streamSid);
    }
    
    /**
     * Unregister Twilio WebSocket session
     * Called by GeminiSessionHandler when stream ends
     * 
     * @param streamSid Stream session ID
     */
    public void unregisterTwilioSession(String streamSid) {
        twilioSessions.remove(streamSid);
        log.info("🔌 [Gemini] Unregistered Twilio session for stream: {}", streamSid);
    }

    /**
     * Check if session is connected
     */
    public boolean isConnected(String sessionId) {
        WebSocketSession session = geminiSessions.get(sessionId);
        return session != null && session.isOpen();
    }
}
