package com.saas.voip.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.saas.shared.dto.AiCostCalculationResult;
import com.saas.shared.service.BaseUrlResolver;
import com.saas.shared.service.ai.AiCostTrackingService;
import com.saas.tenant.entity.InboundCallRequest;
import com.saas.tenant.service.InboundCallService;
import com.saas.voip.service.ai.OpenAiRealtimeCostCalculator;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.Data;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;


@Service
public class OpenAIRealtimeService {

    private static final Logger log = LoggerFactory.getLogger(OpenAIRealtimeService.class);

    @Value("${openai.api.key}")
    private String openAiApiKey;
    
    @Value("${openai.realtime.ws-url}")
    private String openaiWsUrl;
    
    @Value("${openai.realtime.voice:alloy}")
    private String openaiVoice;
    
    private final InboundCallService inboundCallService;
    private final SmsService smsService;
    private final OpenAiRealtimeCostCalculator aiCostCalculator;
    private final AiCostTrackingService aiCostTrackingService;
    private final EmmaPromptService emmaPromptService;
    private final BaseUrlResolver baseUrlResolver;

    public OpenAIRealtimeService(
            InboundCallService inboundCallService, 
            SmsService smsService,
            OpenAiRealtimeCostCalculator aiCostCalculator,
            AiCostTrackingService aiCostTrackingService,
            EmmaPromptService emmaPromptService,
            BaseUrlResolver baseUrlResolver) {
        this.inboundCallService = inboundCallService;
        this.smsService = smsService;
        this.aiCostCalculator = aiCostCalculator;
        this.aiCostTrackingService = aiCostTrackingService;
        this.emmaPromptService = emmaPromptService;
        this.baseUrlResolver = baseUrlResolver;
    }
    
    /**
     * Usage data holder for OpenAI Realtime API
     */
    @Data
    private static class UsageData {
        private long inputAudioTokens = 0;
        private long outputAudioTokens = 0;
        private LocalDateTime callStartTime;
        private LocalDateTime callEndTime;
        private String tenantId;
        private String fromNumber;
        private String toNumber;
        
        public void addUsage(long inputTokens, long outputTokens) {
            this.inputAudioTokens += inputTokens;
            this.outputAudioTokens += outputTokens;
        }
    }

    // NOTE: SYSTEM_RAG is now managed by EmmaPromptService (emma_rag.md)
    // This is kept only for backward compatibility during migration
    private static final String SYSTEM_RAG = """
        [A] SPÉCIALITÉS MÉDICALES ET DOMAINES DE COMPÉTENCE
        [A1] Médecine Générale et Interne
            Domaine de compétence : Point d entrée du système de santé. Prise en charge globale, diagnostic initial, traitement des maladies courantes (grippe, infections), suivi des maladies chroniques, prévention, vaccination et orientation. Idéal pour les symptômes multiples ou non spécifiques.
            Médecins :
              - Docteur Baumann, Médecin Généraliste (Langues: Français, Allemand, Anglais)
              - Docteure Fournier, Médecin Généraliste (Langues: Français, Italien, Anglais)
              - Docteur Wenger, Médecin Généraliste (Langues: Français, Allemand, Espagnol)
              - Docteure Haddad, Médecin Généraliste (Langues: Français, Anglais, Arabe, Espagnol)
              - Docteur Chen, Médecin Généraliste (Langues: Français, Anglais, Mandarin)
        [A2] Cardiologie et Angiologie
            Domaine de compétence : Diagnostic et traitement des maladies du cœur et des vaisseaux. Prise en charge de l hypertension artérielle, des douleurs thoraciques, des palpitations, de l insuffisance cardiaque, et des problèmes de circulation (varices, phlébites).
            Médecins :
              - Docteure Rochat, Cardiologue (Langues: Français, Anglais)
              - Docteur Dubois, Cardiologue (Langues: Français, Allemand, Anglais)
              - Docteure Fischer, Cardiologue (Langues: Français, Italien)
              - Docteur Kohli, Angiologue (Langues: Français, Allemand)
        [A3] Oncologie et Hématologie
            Domaine de compétence : Diagnostic et traitement des cancers (oncologie) et des maladies du sang (hématologie, ex: leucémies, lymphomes). Administration des chimiothérapies et thérapies ciblées.
            Médecins :
              - Professeur Elbaz, Oncologue (Langues: Français, Anglais)
              - Docteure Benali, Oncologue (Langues: Français, Arabe, Anglais)
              - Docteur Müller, Hématologue (Langues: Français, Allemand, Anglais)
        [A4] Neurologie et Neurochirurgie
            Domaine de compétence : Diagnostic et traitement des maladies du système nerveux (cerveau, moelle épinière, nerfs). Prise en charge des maux de tête (céphalées, migraines), vertiges, troubles de la mémoire, épilepsie, tremblements (Parkinson), sclérose en plaques, et chirurgie de la colonne vertébrale ou du cerveau.
            Médecins :
              - Docteure Keller, Neurologue (Langues: Français, Anglais, Allemand)
              - Docteur Simon, Neurologue (Langues: Français, Anglais)
              - Professeur Lejeune, Neurochirurgien (Langues: Français, Anglais)
        [A5] Gynécologie et Maternité
            Domaine de compétence : Santé de la femme, contraception, ménopause, dépistage (frottis), maladies du sein (sénologie), suivi de grossesse et accouchements.
            Médecins :
              - Docteur Gerber, Gynécologue (Langues: Français, Allemand, Allemand)
              - Docteure Perrin, Gynécologue (Langues: Français, Anglais)
            Service : Maternité (Équipe francophone et anglophone 24/7).
        [A6] Orthopédie et Traumatologie
            Domaine de compétence : Traitement des maladies et accidents de l appareil locomoteur (os, articulations, ligaments, muscles). Prise en charge des fractures, entorses, douleurs articulaires (arthrose), mal de dos, et chirurgie prothétique (hanche, genou).
            Médecins :
              - Docteure Meyer, Orthopédiste (Langues: Français, Anglais)
              - Docteur Zbinden, Orthopédiste (Langues: Français, Allemand)
        [A7] Gastro-entérologie
            Domaine de compétence : Diagnostic et traitement des maladies du tube digestif (œsophage, estomac, intestins), du foie et du pancréas. Prise en charge des maux d estomac, brûlures, reflux, diarrhées, constipation, et réalisation d endoscopies (gastroscopie, coloscopie).
            Médecins :
              - Docteure Favre, Gastro-entérologue (Langues: Français, Anglais)
        [A8] Pédiatrie et Néonatalogie
            Domaine de compétence : Santé des enfants et adolescents (de la naissance à 16 ans). Prise en charge de toutes les maladies infantiles, suivi du développement, vaccination, et urgences pédiatriques.
            Médecins :
              - Docteur Huber, Pédiatre (Langues: Français, Anglais, Allemand)
              - Docteure Jacquet, Pédiatre (Langues: Français, Anglais)
              - Docteur Sousa, Pédiatre (Langues: Français, Anglais, Portugais)
        [A9] Dermatologie
            Domaine de compétence : Diagnostic et traitement des maladies de la peau, des cheveux et des ongles. Prise en charge de l acné, l eczéma, le psoriasis, le dépistage des cancers de la peau (dermoscopie) et la petite chirurgie dermatologique.
            Médecins :
              - Docteure de Vries, Dermatologue (Langues: Français, Anglais, Allemand, Néerlandais)
        [B] SERVICES, PROCÉDURES ET TARIFS (Base TARMED, point 1.20 CHF)
        [B1] Consultations
            [B1.1] Médecine générale (20 min): 135 CHF
            [B1.2] Spécialiste (20 min): 195 CHF
            [B1.3] Consultation d urgence (sans rdv): Supplément de 70 CHF
            [B1.4] Physiothérapie (séance 30 min): 55 CHF
            [B1.5] Consultation diététique (45 min): 90 CHF
        [B2] Centre de Check-up & Prévention
            [B2.1] Bilan  Essentiel  (Consultation, prise de sang complète, ECG, rapport): 650 CHF
            [B2.2] Bilan  Exécutif  (Essentiel + échographie abdo, ECG d effort): 1500 CHF
            [B2.3] Bilan  Sportif  (Exécutif + consultation cardiologie du sport, test VO2max): 2200 CHF
            [B2.4] Note: Ces bilans ne sont généralement pas couverts par la LAMal.
        [B3] Interventions et Soins
            [B3.1] Chimiothérapie (séance ambulatoire): Tarif variable selon le protocole (800 - 5000 CHF).
            [B3.2] Dialyse (séance): 600 CHF.
            [B3.3] Endoscopie digestive (gastroscopie): 800 CHF.
            [B3.4] Endoscopie digestive (coloscopie): 1100 CHF.
            [B3.5] Accouchement (forfait voie basse, 3 jours en semi-privé): 7500 CHF.
        [C] PLATEAU TECHNIQUE: IMAGERIE, LABORATOIRE, BLOCS OPÉRATOIRES
        [C1] Imagerie Médicale
            [C1.1] Radiographie standard: 170 CHF
            [C1.2] Échographie: 290 CHF
            [C1.3] Scanner (CT-Scan) sans contraste: 680 CHF
            [C1.4] Scanner (CT-Scan) avec contraste: 950 CHF
            [C1.5] IRM (Imagerie par Résonance Magnétique) sans contraste: 1000 CHF
            [C1.6] IRM avec contraste: 1350 CHF
            [C1.7] PET-Scan (Tomographie par Émission de Positons): 2500 CHF
        [C2] Laboratoire d Analyses
            [C2.1] Prise de sang (acte infirmier): 40 CHF
            [C2.2] Bilan sanguin de base (FSC, CRP, Glycémie, Rein, Foie): 150 CHF
            [C2.3] Test PCR (respiratoire): 165 CHF
            [C2.4] Analyse génétique (sur devis uniquement).
        [C3] Blocs Opératoires
            [C3.1] Description: 8 salles d opération modernes, incluant une salle hybride avec imagerie intégrée et un robot chirurgical Da Vinci.
        [D] ASSURANCES ET FACTURATION
        [D1] Assurance de base Suisse (LAMal)
            [D1.1] Acceptation: Toutes les assurances de base suisses sont acceptées.
            [D1.2] Modèle Médecin de famille: Le patient doit avoir un bon de délégation de son médecin pour voir nos spécialistes (sauf urgences, gynéco, pédiatrie, ophtalmo).
            [D1.3] Modèle Telmed: Le patient doit avoir appelé sa hotline d assurance avant de nous contacter.
            [D1.4] Remboursement: 90% des coûts après déduction de la franchise annuelle. La quote-part est de 10%.
        [D2] Assurances Complémentaires (LCA)
            [D2.1] Division privée: Permet le choix du médecin et une chambre individuelle. Coûts couverts selon le contrat.
            [D2.2] Division semi-privée: Permet une chambre à deux lits. Coûts couverts selon le contrat.
        [D3] Assurances Internationales et Auto-payeurs
            [D3.1] Procédure: Le paiement complet est requis le jour du traitement. Une facture détaillée est fournie pour le remboursement.
            [D3.2] Garantie de Paiement (GOP): Une prise en charge directe est possible si l assurance envoie une confirmation écrite (GOP) 48h avant.
        [E] INFORMATIONS PRATIQUES ET CONTACTS
        [E1] Horaires
            [E1.1] Accueil téléphonique: Lu-Ve, 08:00 - 18:00.
            [E1.2] Urgences Adultes & Pédiatriques: Ouvert 24 heures sur 24, 7 jours sur 7.
            [E1.3] Laboratoire (prises de sang): Lu-Ve, 07:30 - 12:00.
            [E1.4] Heures de visite (hospitalisation): Tous les jours, 11:00 - 20:00.
        [E2] Accès
            [E2.1] Adresse: Avenue du Léman 25, 1815 Clarens, Suisse.
            [E2.2] Parking: Parking souterrain pour les patients, 1ère heure gratuite (valider ticket).
            [E2.3] Transports publics: Bus 201 (arrêt  Hôpital Rive Bleue ), Gare CFF de Clarens (10 min à pied).
        [E3] Contacts Internes
            [E3.1] Service de facturation: 021 987 65 44 (Lu-Ve, 10h-12h).
            [E3.2] Service des admissions (hospitalisation): 021 987 65 50.
            [E3.3] Pharmacie de l hôpital: 021 987 65 90 (ouverte au public).
        [E4] Autres Services
            [E4.1] Cafétéria  Le Panorama : Ouverte de 08:00 à 19:00, vue sur le lac.
            [E4.2] WiFi: Réseau  RiveBleue-Patient  gratuit disponible dans tout l établissement.
            [E4.3] Aumônerie: Service disponible pour un soutien spirituel sur demande.
        [F] WORKFLOWS ET PROCÉDURES OPÉRATIONNELLES (SCRIPT IMPÉRATIF)
        [F1] Workflow de Prise de Rendez-vous 
            ÉTAPE 1: PRISE DE PAROLE ET TRIAGE.
                 1a. Votre toute première phrase à l appelant est : Je suis Emma, votre assistant virtuel. Quel est le motif de votre appel ?
                 1b. Appliquer le protocole de triage avancé pour identifier la ou les spécialités pertinentes. Ne pas réciter les symptômes à l appelant.
            ÉTAPE 2: PROPOSITION ET CHOIX DE LA SPÉCIALITÉ.
                 2a. Proposer la ou les 2 spécialités identifiées. Attendre que l appelant choisisse une spécialité.
                 2b. Si l appelant est incertain, proposer directement un Médecin Généraliste.
            ÉTAPE 3: PROPOSITION MÉDECIN. Une fois la spécialité choisie, proposer jusqu à 2 médecins de cette spécialité, en y ajoutant toujours un Médecin Généraliste comme option finale. Utiliser le format concis : Docteur/Docteure [Nom de famille]. Attendre le choix de l appelant.
            ÉTAPE 4: RECHERCHE ET PROPOSITION DE CRÉNEAU.
                 4a. Une fois le médecin choisi, dire : Parfait. Je recherche les prochaines disponibilités pour le/la [Titre du médecin].
                 4b. Consulter les sections [H] (horaires) et [G] (indisponibilités) pour trouver les 2 ou 3 premiers créneaux valides à partir de J+1.
                 4c. Proposer les créneaux trouvés.
            ÉTAPE 5: CONFIRMATION DU CRÉNEAU.
                 5a. Attendre que l appelant confirme son choix de créneau.
                 5b. Si l appelant accepte, dire : Très bien. Pour finaliser ce rendez-vous du [jour] [date] à [heure], j ai besoin de quelques informations.
                 NE PAS PASSER À L ÉTAPE 6 TANT QUE LE CRÉNEAU N EST PAS ACCEPTÉ.
            ÉTAPE 6: COLLECTE ET VALIDATION DU NOM ET DU PRENOM. Suivre la séquence stricte (demande, épellation OBLIGATOIRE, confirmation).
            ÉTAPE 7: COLLECTE ET VALIDATION DATE DE NAISSANCE (CHIFFRE PAR CHIFFRE).
                 7a. Demander : Quelle est votre date de naissance ? Veuillez me donner le jour, puis le mois, puis l année.
                 7b. Répéter chaque partie pour confirmation. Exemple : Le jour [jour], correct ? ... Le mois [mois], correct ? ... Et l année [année], correct ?
               ÉTAPE 8: RÉCAPITULATIF FINAL. Faire le récapitulatif complet et demander une dernière confirmation globale en ajoutant la phrase : Le numéro de téléphone avec lequel vous nous appelez sera ajouté à votre fiche de rendez-vous.
        [F2] Procédure de Gestion d Urgence Vitale
            [F2.1] Déclencheur : L appelant décrit des symptômes potentiellement graves (ex: douleur thoracique intense, difficulté à respirer soudaine, perte de connaissance, paralysie faciale, confusion aiguë).
            [F2.2] Action Immédiate : Interrompre tout autre script. Adopter un ton calme mais direct.
            [F2.3] Message Obligatoire : Dire la phrase exacte :  Pour ce type de symptômes, il est impératif d appeler immédiatement les services d urgence au 144. Veuillez raccrocher et composer le 144. Pour votre sécurité, je ne peux pas continuer cet appel. 
        [F3] Procédure de Transfert d Appel
            [F3.1] Déclencheur : L appelant demande à parler à un service spécifique (ex:  facturation ,  admissions ) ou à une personne non listée comme médecin consultable.
            [F3.2] Action : Rechercher le numéro de contact du service dans la section [E3].
            [F3.3] Message : Informer l appelant :  Je vous mets en relation avec le service [Nom du service]. Veuillez rester en ligne. 
        [G] CALENDRIER DES INDISPONIBILITÉS (Rendez-vous DÉJÀ PRIS)
        (La sémantique de cette section a changé : ce sont les créneaux OCCUPÉS)
            [A1.1] Docteur Baumann, Médecin Généraliste:
              - Nov 2025: 25(14:00, 14:20), 26(09:40, 10:00, 10:20), 28(15:00, 16:00)
              - Dec 2025: 1(10:00), 3(11:00, 11:20), 5(09:00, 09:20), 8(14:40, 15:00)
              - Jan 2026: 12(09:00), 14(10:20, 10:40), 16(16:00, 16:20, 16:40)
            [A1.2] Docteure Fournier, Médecin Généraliste:
              - Nov 2025: 25(16:00), 27(09:00, 09:30, 10:00)
              - Dec 2025: 2(14:00, 14:30), 4(09:00), 9(15:00, 15:30), 11(10:30)
              - Jan 2026: 13(14:00), 15(09:00, 09:30), 19(11:00, 11:30)
            [A2.1] Docteure Rochat, Cardiologue:
              - Nov 2025: 26(11:00), 28(14:30)
              - Dec 2025: 3(09:30, 10:00), 10(14:00, 14:30), 12(11:30)
              - Jan 2026: 14(09:00), 21(15:00, 15:30), 23(10:00, 10:30)
            [A6.1] Docteure Meyer, Orthopédiste:
              - Nov 2025: 25(10:00, 10:30)
              - Dec 2025: 1(14:00), 8(09:00, 09:30), 15(11:00, 11:30)
              - Jan 2026: 12(14:30), 19(10:00), 26(09:30, 10:00, 10:30)
            [A7.1] Docteure Favre, Gastro-entérologue:
              - Nov 2025: 27(10:00, 11:00)
              - Dec 2025: 4(14:00), 11(09:00, 10:00), 18(15:00)
              - Jan 2026: 15(11:00), 22(09:00, 10:00), 29(14:00, 15:00)
            [A8.1] Docteur Huber, Pédiatre:
              - Nov 2025: 25(09:00, 09:20, 09:40), 27(14:00, 14:20)
              - Dec 2025: 2(10:00), 4(15:00, 15:20), 9(09:00, 09:20), 11(14:40)
              - Jan 2026: 13(09:40), 15(14:00), 20(10:00, 10:20), 22(15:00, 15:20)
        [G2] Calendriers des Équipements d Imagerie
            [C1.1] Radiographie standard:
              - Nov 2025: Disponible tous les jours ouvrables toutes les 15 minutes entre 09:00-11:45 et 14:00-16:45.
              - Dec 2025: Disponible tous les jours ouvrables toutes les 15 minutes.
              - Jan 2026: Disponible tous les jours ouvrables toutes les 15 minutes.
            [C1.3] Scanner (CT-Scan):
              - Nov 2025: 25(10:00, 10:30, 11:00), 26(14:00, 14:30, 15:00), 28(09:00, 09:30)
              - Dec 2025: 1(11:00), 3(15:30), 5(09:30, 10:00), 8(14:00, 14:30)
              - Jan 2026: 12(10:00), 14(15:00), 16(09:00, 09:30, 10:00, 10:30)
            [C1.5] IRM (Imagerie par Résonance Magnétique):
              - Nov 2025: 26(16:00), 28(11:00)
              - Dec 2025: 2(10:00, 11:00), 9(14:00, 15:00), 16(16:00)
              - Jan 2026: 13(09:00, 10:00), 20(14:00), 27(15:00, 16:00)6:00)
        [H] HORAIRES DE CONSULTATION STANDARDS
        (Définit les créneaux potentiellement DISPONIBLES. La durée des créneaux est indicative.)
        [H1] Médecins
            [A1] Médecine Générale et Interne
              - Docteur Baumann: Lundi, Mercredi, Vendredi (09:00-12:00, 14:00-17:00). Créneaux de 20 min.
              - Docteure Fournier: Mardi, Jeudi (08:30-12:30, 14:00-16:00). Créneaux de 20 min.
              - Docteur Wenger: Lundi, Mardi, Jeudi (08:00-12:00, 13:30-17:30). Créneaux de 20 min.
              - Docteure Haddad: Mercredi, Vendredi (09:00-13:00, 14:00-18:00). Créneaux de 30 min.
              - Docteur Chen: Mardi, Mercredi, Jeudi (09:30-12:30, 14:00-17:00). Créneaux de 30 min.
            [A2] Cardiologie et Angiologie
              - Docteure Rochat: Mercredi, Vendredi (09:00-12:00, 14:00-15:00). Créneaux de 30 min.
              - Docteur Dubois: Lundi, Mardi (08:30-12:30, 14:00-17:00). Créneaux de 30 min.
              - Docteure Fischer: Jeudi (09:00-12:00, 13:30-16:30). Créneaux de 30 min.
              - Docteur Kohli: Mardi, Vendredi (09:00-12:00). Créneaux de 30 min.
            [A3] Oncologie et Hématologie
              - Professeur Elbaz: Lundi, Jeudi (09:00-12:30, 14:00-17:00). Créneaux de 30 min.
              - Docteure Benali: Mardi, Mercredi (09:00-12:00, 13:30-16:00). Créneaux de 30 min.
              - Docteur Müller: Vendredi (08:30-12:30, 14:00-17:00). Créneaux de 30 min.
            [A4] Neurologie et Neurochirurgie
              - Docteure Keller: Mardi, Jeudi (09:00-12:00, 14:00-16:30). Créneaux de 30 min.
              - Docteur Simon: Lundi, Mercredi (09:30-12:30, 14:00-17:00). Créneaux de 30 min.
              - Professeur Lejeune: Vendredi (Consultations 09:00-12:00). Créneaux de 30 min. (Autres jours en chirurgie).
            [A5] Gynécologie
              - Docteur Gerber: Lundi, Mercredi, Jeudi (08:30-12:30, 14:00-17:00). Créneaux de 30 min.
              - Docteure Perrin: Mardi, Vendredi (09:00-12:00, 13:30-16:30). Créneaux de 30 min.
            [A6] Orthopédie et Traumatologie
              - Docteure Meyer: Lundi, Jeudi (09:00-12:00, 14:00-17:00). Créneaux de 30 min.
              - Docteur Zbinden: Mardi, Vendredi (08:30-12:30, 14:00-16:30). Créneaux de 30 min.
            [A7] Gastro-entérologie
              - Docteure Favre: Jeudi, Vendredi (09:00-12:00, 14:00-16:00). Créneaux de 30-60 min.
            [A8] Pédiatrie
              - Docteur Huber: Lundi, Mardi, Jeudi (08:30-12:00, 14:00-17:30). Créneaux de 20 min.
              - Docteure Jacquet: Mercredi, Vendredi (09:00-12:30, 14:00-17:00). Créneaux de 20 min.
              - Docteur Sousa: Lundi, Vendredi (09:00-12:00, 14:00-18:00). Créneaux de 20 min.
            [A9] Dermatologie
              - Docteure de Vries: Lundi, Mercredi (09:00-12:00, 13:30-17:00). Créneaux de 20 min.
        [H2] Équipements
            [C1.1] Radiographie standard: Lundi au Vendredi (08:00-12:00, 13:30-17:00). Créneaux de 15 min.
            [C1.2] Échographie: Lundi au Vendredi (08:30-12:00, 14:00-16:30). Créneaux de 30 min.
            [C1.3] & [C1.4] Scanner (CT-Scan): Lundi au Vendredi (08:00-12:00, 13:30-17:30). Créneaux de 30 min.
            [C1.5] & [C1.6] IRM: Lundi, Mercredi, Vendredi (08:00-12:00, 14:00-18:00). Créneaux de 60 min.
            [C1.7] PET-Scan: Mardi, Jeudi (08:00-12:00). Créneaux de 90 min.
    """;

    private static final String SYSTEM_MESSAGE = """
    Vous êtes un assistant médical  EMMA virtuel professionnel et bienveillant de la Clinique « La Rive Bleue ». Vos fonctions principales a respecté SVP sont :\n" +
        "1. *Prise de rendez-vous.*\n" +
        "2. *Transfert d’appel* vers un service ou une personne. \n" +
        "3. *Conseil de consultation et prise de rendez-vous* pour un malaise ou un mal donné. \n" +
        "4. *Fournir des informations* sur les services, moyens (équipements) et spécialités disponibles lorsque demandé par l'appelant. \n\n" +
        "*Principes de communication et comportement :*\n" +
        "• *Écoute active et réactivité :* Attendez toujours que la personne ait fini de parler avant de répondre. Soyez vigilant à chaque mot. *Arrêtez de parler immédiatement si vous entendez le patient parler.* Demandez-lui poliment de répéter ce que vous n'avez pas entendu afin de bien comprendre. \n" +
        "• *Clarté et concision :* Répondez en une seule phrase claire et polie. Posez une seule question à la fois. \n" +
        "• *Numéros de téléphone et dates :* Assurez-vous de bien comprendre et répéter chaque chiffre ou élément de la date. Épelez lettre par lettre les informations critiques (nom, prénom) et vérifiez chaque chiffre du numéro de téléphone. \n" +
        "• *Ton :* Gardez toujours un ton calme, naturel, professionnel et empathique.\n" +
        "• *Gestion de la parole simultanée :* Si l'appelant commence à parler alors que vous parlez, interrompez immédiatement vos phrases et écoutez attentivement. Ne continuez que lorsque le patient a terminé.\n\n" +
        "*Déroulement de l'interaction :*\n" +
        "1.  *Accueil et motif :* Commencez toujours par demander le motif précis de l'appel ou de la maladie du patient. Par exemple : 'Bonjour et bienvenue à la Clinique La Rive Bleue. Quel est le motif de votre appel aujourd'hui ?'\n\n" +

        "2.  *Gestion des rendez-vous :*\n" +
        "    •   *Disponibilité :* Les rendez-vous sont uniquement du lundi au vendredi. Ils sont pris à partir de J+1 (le lendemain de l'appel). Privilégiez toujours les dates les plus proches. \n" +
        "    •   *Refus week-end :* Refusez poliment toute demande de rendez-vous le week-end et proposez un jour en semaine. Ex: 'Nous ne prenons pas de rendez-vous le week-end, mais je peux vous proposer le plus tôt possible un jour en semaine, par exemple [proposez la date J+1 si c'est un jour de semaine, sinon le lundi suivant]. Cela vous conviendrait-il ?'\n" +
        "    •   *Choix du médecin :* Après avoir identifié la spécialité requise ou le besoin, proposez *toujours un spécialiste et un médecin généraliste* si pertinent, en laissant le choix au patient. \n" +
        "        - *Liste des médecins et spécialités :*\n" +
        "          1. Dre. Élodie Rochat - Cardiologie\n" +
        "          2. Dr. Noah Müller - Dermatologie\n" +
        "          3. Dre. Léa Favre - Gastro-entérologie\n" +
        "          4. Dr. Liam Schmid - Pneumologie\n" +
        "          5. Dre. Sofia Keller - Neurologie\n" +
        "          6. Dr. Gabriel Weber - Endocrinologie\n" +
        "          7. Dre. Clara Meyer - Orthopédie\n" +
        "          8. Dr. Arthur Gerber - Gynécologie\n" +
        "          9. Dre. Alice Fournier - Urologie\n" +
        "          10. Dr. Louis Huber - Pédiatrie\n" +
        "          11. Dre. Zoé Graf - Ophtalmologie\n" +
        "          12. Dr. Samuel Schneider - ORL\n" +
        "          13. Dr. Nathan Baumann - Généraliste\n" +
        "          14. Dre. Léa Fournier - Généraliste\n" +
        "    •   *Collecte des informations patient (après confirmation du rendez-vous) :* Une fois le rendez-vous (date, heure, médecin) confirmé, demandez successivement :\n" +
        "        a.  *Nom complet :* 'Pourriez-vous me donner votre nom et prénom, s'il vous plaît ?'\n" +
        "        b.  *Épellation et confirmation :* Épelez lettre par lettre le nom et le prénom du patient, puis demandez confirmation. Ex: 'Je note [Nom, épelez N-O-M] [Prénom, épelez P-R-E-N-O-M]. Est-ce bien cela ?'\n" +
        "        c.  *Date de naissance :* 'Et quelle est votre date de naissance ?'\n" +
        "        d.  *Confirmation :* Répétez la date de naissance et demandez confirmation. \n" +
        "        e.  *Numéro de téléphone :* 'Enfin, quel est votre numéro de téléphone pour vous joindre ?' *Attendez que le patient ait entièrement prononcé son numéro de téléphone avant de le répéter ou de demander confirmation.*\n" +
        "        f.  *Confirmation du numéro :* Une fois le numéro complet donné, répétez-le et demandez confirmation. Ex: 'J'ai noté le [Numéro de téléphone]. Est-ce exact ?'\n" +
        "        g.  *Récapitulatif final :* 'Je vous confirme donc votre rendez-vous avec [Nom du Médecin] le [Date] à [Heure]. Vos coordonnées sont [Nom Prénom], né(e) le [Date de naissance], numéro de téléphone [Numéro]. Est-ce tout correct ?'\n\n" +

        "3.  *Gestion des transferts d'appel :*\n" +
        "    •   Si le patient demande à parler à un service spécifique ou à une personne, proposez de le rediriger. Ex: 'Je vous mets en relation avec le service concerné / [Nom de la personne], restez en ligne s'il vous plaît.'\n\n" +

        "4.  *Conseil de consultation pour malaises / maux :*\n" +
        "    •   Selon le malaise ou le mal décrit par le patient, proposez un médecin (spécialiste ou généraliste selon pertinence) et prenez rendez-vous. Ex: 'Pour ces symptômes, je vous recommande de consulter le Dr. [Nom du spécialiste ou généraliste]. Souhaitez-vous prendre rendez-vous ?'\n\n" +

        "    •   *Réponse aux demandes d'informations :* Si un patient demande des informations sur un service, un équipement ou une spécialité, fournissez la description pertinente de manière claire et concise. Ex: 'Notre clinique dispose d'un échographe portable qui permet de visualiser en temps réel certains organes...' \n\n" +

        "N'oubliez jamais de rester concentré sur les fonctions définies et de guider le patient avec professionnalisme et empathie.
    """;


    // Voice configuration now externalized to application.yml (openai.realtime.voice)
    
    private static final List<String> LOG_EVENT_TYPES = List.of(
        "response.content.done",
        "rate_limits.updated", 
        "response.done",
        "input_audio_buffer.committed",
        "input_audio_buffer.speech_stopped",
        "input_audio_buffer.speech_started",
        "session.created"
    );

    private final ObjectMapper objectMapper = new ObjectMapper();
    private final Map<String, WebSocketClient> openAIClients = new ConcurrentHashMap<>();
    private final Map<String, String> sessionStreamIds = new ConcurrentHashMap<>();
    private final Map<String, String> sessionCallSids = new ConcurrentHashMap<>();
    private final Map<String, StringBuilder> sessionTranscripts = new ConcurrentHashMap<>();
    private final Map<String, UsageData> sessionUsage = new ConcurrentHashMap<>();

    public void connectToOpenAI(WebSocketSession twilioSession) {
        try {
            WebSocketClient openAIClient = new WebSocketClient(new URI(openaiWsUrl)) {
                @Override
                public void onOpen(ServerHandshake handshake) {
                    log.info("Connected to OpenAI Realtime API for session: {}", twilioSession.getId());
                    sendSessionUpdate(this);
                }

                @Override
                public void onMessage(String message) {
                    handleOpenAIMessage(twilioSession, message);
                }

                @Override
                public void onClose(int code, String reason, boolean remote) {
                    log.info("Disconnected from OpenAI: {} - {}", code, reason);
                }

                @Override
                public void onError(Exception ex) {
                    log.error("OpenAI WebSocket error", ex);
                }
            };

            openAIClient.addHeader("Authorization", "Bearer " + openAiApiKey);
            openAIClient.addHeader("OpenAI-Beta", "realtime=v1");
            openAIClient.connect();

            openAIClients.put(twilioSession.getId(), openAIClient);
            sessionTranscripts.put(twilioSession.getId(), new StringBuilder("["));
            
            // Initialize usage tracking
            UsageData usage = new UsageData();
            usage.setCallStartTime(LocalDateTime.now());
            sessionUsage.put(twilioSession.getId(), usage);

        } catch (Exception e) {
            log.error("Failed to connect to OpenAI", e);
        }
    }

    private void sendSessionUpdate(WebSocketClient client) {
        try {
            ObjectNode sessionUpdate = objectMapper.createObjectNode();
            sessionUpdate.put("type", "session.update");
            
            ObjectNode session = objectMapper.createObjectNode();
            
            ObjectNode turnDetection = objectMapper.createObjectNode();
            turnDetection.put("type", "server_vad");
            session.set("turn_detection", turnDetection);
            
            session.put("input_audio_format", "g711_ulaw");
            session.put("output_audio_format", "g711_ulaw");
            session.put("voice", openaiVoice);
            session.put("instructions", emmaPromptService.getFullSystemPrompt());
            session.put("temperature", 0.8);
            session.putArray("modalities").add("text").add("audio");
            
            // Define function for extracting patient data
            ArrayNode tools = session.putArray("tools");
            ObjectNode tool = tools.addObject();
            tool.put("type", "function");
            tool.put("name", "enregistrer_patient");
            tool.put("description", "Enregistre les informations du patient et son rendez-vous une fois toutes les données collectées et confirmées");
            
            ObjectNode parameters = tool.putObject("parameters");
            parameters.put("type", "object");
            
            ObjectNode properties = parameters.putObject("properties");
            
            ObjectNode nom = properties.putObject("nom");
            nom.put("type", "string");
            nom.put("description", "Nom complet du patient");
            
            ObjectNode dateNaissance = properties.putObject("date_naissance");
            dateNaissance.put("type", "string");
            dateNaissance.put("description", "Date de naissance du patient au format JJ/MM/AAAA");
            
            ObjectNode telephone = properties.putObject("telephone");
            telephone.put("type", "string");
            telephone.put("description", "Numéro de téléphone du patient");
            
            ObjectNode maladie = properties.putObject("maladie");
            maladie.put("type", "string");
            maladie.put("description", "Motif de consultation ou maladie du patient");
            
            ObjectNode motifVisite = properties.putObject("motif_visite");
            motifVisite.put("type", "string");
            motifVisite.put("description", "Motif détaillé de la visite ou consultation");
            
            ObjectNode appointmentDateTime = properties.putObject("appointment_date_time");
            appointmentDateTime.put("type", "string");
            appointmentDateTime.put("description", "Date et heure du rendez-vous au format ISO 8601 (YYYY-MM-DDTHH:MM:SS)");
            
            ObjectNode doctorName = properties.putObject("doctor_name");
            doctorName.put("type", "string");
            doctorName.put("description", "Nom complet du médecin pour le rendez-vous");
            
            ObjectNode appointmentConfirmed = properties.putObject("appointment_confirmed");
            appointmentConfirmed.put("type", "boolean");
            appointmentConfirmed.put("description", "true si le rendez-vous est confirmé par le patient, false sinon");
            
            ArrayNode required = parameters.putArray("required");
            required.add("nom");
            required.add("date_naissance");
            required.add("telephone");
            required.add("maladie");
            
            sessionUpdate.set("session", session);

            String message = objectMapper.writeValueAsString(sessionUpdate);
            log.debug("Sending session update: {}", message);
            client.send(message);

        } catch (Exception e) {
            log.error("Error sending session update", e);
        }
    }

    private void handleOpenAIMessage(WebSocketSession twilioSession, String message) {
        try {
            JsonNode response = objectMapper.readTree(message);
            String type = response.get("type").asText();

            if (LOG_EVENT_TYPES.contains(type)) {
                log.debug("Received OpenAI event: {}", type);
            }

            if ("session.updated".equals(type)) {
                log.info("✅ OpenAI session updated successfully");
                if (response.has("session")) {
                    JsonNode session = response.get("session");
                    String inputFormat = session.has("input_audio_format") ? 
                        session.get("input_audio_format").asText() : "UNKNOWN";
                    String outputFormat = session.has("output_audio_format") ? 
                        session.get("output_audio_format").asText() : "UNKNOWN";
                    
                    log.info("🎧 Audio formats configured:");
                    log.info("   ├─ Input:  {} (expected: pcm16)", inputFormat);
                    log.info("   └─ Output: {} (expected: pcm16)", outputFormat);
                    
                    // WARN if OpenAI didn't apply pcm16 (known API bug Oct 2024)
                    if (!"pcm16".equals(inputFormat) || !"pcm16".equals(outputFormat)) {
                        log.warn("⚠️ AUDIO FORMAT MISMATCH!");
                        log.warn("⚠️ OpenAI may have ignored pcm16 configuration (known API bug)");
                        log.warn("⚠️ This WILL cause distorted/metallic audio!");
                        log.warn("⚠️ Input: {} (should be pcm16)", inputFormat);
                        log.warn("⚠️ Output: {} (should be pcm16)", outputFormat);
                    }
                }
            }

            // Trigger response generation when speech is committed
            if ("input_audio_buffer.committed".equals(type)) {
                log.info("Speech committed, triggering response generation");
                triggerResponseGeneration(twilioSession);
            }

            if ("response.audio.delta".equals(type) && response.has("delta")) {
                String audioDelta = response.get("delta").asText();
                log.debug("Received audio delta, sending to Twilio");
                sendAudioToTwilio(twilioSession, audioDelta);
            }
            
            // Capture transcript - assistant messages
            if ("response.audio_transcript.done".equals(type) && response.has("transcript")) {
                String transcript = response.get("transcript").asText();
                addToTranscript(twilioSession.getId(), "assistant", transcript);
            }
            
            // Capture transcript - user messages  
            if ("conversation.item.input_audio_transcription.completed".equals(type) && response.has("transcript")) {
                String transcript = response.get("transcript").asText();
                addToTranscript(twilioSession.getId(), "user", transcript);
            }
            
            // Handle function call from OpenAI
            if ("response.function_call_arguments.done".equals(type)) {
                handleFunctionCall(response, twilioSession.getId());
            }
            
            // Track usage from response.done event
            if ("response.done".equals(type) && response.has("response")) {
                captureUsageMetrics(twilioSession.getId(), response.get("response"));
            }

        } catch (Exception e) {
            log.error("Error processing OpenAI message", e);
        }
    }
    
    private void handleFunctionCall(JsonNode response, String sessionId) {
        try {
            String functionName = response.get("name").asText();
            String arguments = response.get("arguments").asText();
            String callSid = response.get("call_sid") != null ? response.get("call_sid").asText() : null;
            
            if ("enregistrer_patient".equals(functionName)) {
                JsonNode patientData = objectMapper.readTree(arguments);
                
                log.info("========================================");
                log.info("📋 DONNÉES PATIENT EXTRAITES :");
                log.info("========================================");
                log.info("👤 Nom : {}", patientData.has("nom") ? patientData.get("nom").asText() : "N/A");
                log.info("🎂 Date de naissance : {}", patientData.has("date_naissance") ? patientData.get("date_naissance").asText() : "N/A");
                log.info("📞 Téléphone : {}", patientData.has("telephone") ? patientData.get("telephone").asText() : "N/A");
                log.info("🏥 Maladie/Motif : {}", patientData.has("maladie") ? patientData.get("maladie").asText() : "N/A");
                log.info("📝 Motif visite : {}", patientData.has("motif_visite") ? patientData.get("motif_visite").asText() : "N/A");
                log.info("📅 RDV Date/Heure : {}", patientData.has("appointment_date_time") ? patientData.get("appointment_date_time").asText() : "N/A");
                log.info("👨‍⚕️ Médecin : {}", patientData.has("doctor_name") ? patientData.get("doctor_name").asText() : "N/A");
                log.info("✅ RDV Confirmé : {}", patientData.has("appointment_confirmed") ? patientData.get("appointment_confirmed").asBoolean() : "N/A");
                log.info("========================================");
                
                // Find callSid from session mapping
                String resolvedCallSid = callSid;
                if (resolvedCallSid == null) {
                    // Try to get from session mapping
                    for (Map.Entry<String, String> entry : sessionCallSids.entrySet()) {
                        resolvedCallSid = entry.getValue();
                        break; // Get the first one (should be only one active)
                    }
                }
                
                if (resolvedCallSid != null) {
                    // Parse appointment date/time if present
                    LocalDateTime appointmentDateTime = null;
                    if (patientData.has("appointment_date_time")) {
                        try {
                            String dateTimeStr = patientData.get("appointment_date_time").asText();
                            appointmentDateTime = LocalDateTime.parse(dateTimeStr);
                        } catch (Exception e) {
                            log.warn("Failed to parse appointment date/time: {}", patientData.get("appointment_date_time").asText());
                        }
                    }
                    
                    // Save patient request to database
                    InboundCallRequest request = InboundCallRequest.builder()
                            .callSid(resolvedCallSid)
                            .nom(patientData.has("nom") ? patientData.get("nom").asText() : null)
                            .dateNaissance(patientData.has("date_naissance") ? patientData.get("date_naissance").asText() : null)
                            .telephone(patientData.has("telephone") ? patientData.get("telephone").asText() : null)
                            .maladie(patientData.has("maladie") ? patientData.get("maladie").asText() : null)
                            .motifVisite(patientData.has("motif_visite") ? patientData.get("motif_visite").asText() : null)
                            .appointmentDateTime(appointmentDateTime)
                            .doctorName(patientData.has("doctor_name") ? patientData.get("doctor_name").asText() : null)
                            .appointmentConfirmed(patientData.has("appointment_confirmed") ? patientData.get("appointment_confirmed").asBoolean() : false)
                            .smsSent(false)
                            .build();
                    
                    InboundCallRequest savedRequest = inboundCallService.savePatientRequest(request);
                    
                    // Get and save conversation transcript
                    String transcript = getConversationTranscript(sessionId);
                    if (transcript != null && !transcript.isEmpty()) {
                        savedRequest.setConversationTranscript(transcript);
                        savedRequest = inboundCallService.savePatientRequest(savedRequest);
                        log.info("📝 Transcription de conversation sauvegardée");
                    }
                    
                    // Send SMS if appointment is confirmed
                    if (Boolean.TRUE.equals(savedRequest.getAppointmentConfirmed()) && 
                        savedRequest.getAppointmentDateTime() != null &&
                        savedRequest.getTelephone() != null &&
                        savedRequest.getDoctorName() != null) {
                        
                        log.info("📱 Envoi du SMS de confirmation de RDV...");
                        
                        // Build status callback URL using BaseUrlResolver
                        String statusCallbackUrl = baseUrlResolver.buildCallbackUrl("/api/voip/sms/status-callback");
                        
                        String smsSid = smsService.sendAppointmentConfirmation(
                            SmsService.Provider.TWILIO,
                            null,
                            savedRequest.getTelephone(),
                            savedRequest.getNom(),
                            savedRequest.getDoctorName(),
                            savedRequest.getAppointmentDateTime(),
                            statusCallbackUrl
                        );
                        
                        if (smsSid != null) {
                            savedRequest.setSmsSent(true);
                            savedRequest.setSmsSid(smsSid);
                            savedRequest.setSmsStatus("queued");
                            inboundCallService.savePatientRequest(savedRequest);
                            log.info("✅ SMS de confirmation envoyé et marqué dans la base de données (SID: {})", smsSid);
                        }
                    }
                } else {
                    log.warn("No callSid found to save patient request");
                }
            }
        } catch (Exception e) {
            log.error("Error handling function call", e);
        }
    }
    
    private void triggerResponseGeneration(WebSocketSession twilioSession) {
        WebSocketClient client = openAIClients.get(twilioSession.getId());
        if (client != null && client.isOpen()) {
            try {
                ObjectNode responseCreate = objectMapper.createObjectNode();
                responseCreate.put("type", "response.create");
                
                ObjectNode responseConfig = objectMapper.createObjectNode();
                responseConfig.putArray("modalities").add("text").add("audio");
                
                responseCreate.set("response", responseConfig);

                String message = objectMapper.writeValueAsString(responseCreate);
                log.debug("Sending response.create: {}", message);
                client.send(message);
            } catch (Exception e) {
                log.error("Error triggering response generation", e);
            }
        }
    }

    public void sendAudioToOpenAI(WebSocketSession twilioSession, String audioPayload) {
        WebSocketClient client = openAIClients.get(twilioSession.getId());
        if (client != null && client.isOpen()) {
            try {
                ObjectNode audioAppend = objectMapper.createObjectNode();
                audioAppend.put("type", "input_audio_buffer.append");
                audioAppend.put("audio", audioPayload);

                client.send(objectMapper.writeValueAsString(audioAppend));
            } catch (Exception e) {
                log.error("Error sending audio to OpenAI", e);
            }
        }
    }

    private void sendAudioToTwilio(WebSocketSession twilioSession, String audioDelta) {
        try {
            if (twilioSession.isOpen()) {
                // Get the real Twilio streamSid
                String streamSid = sessionStreamIds.get(twilioSession.getId());
                if (streamSid == null) {
                    log.warn("No streamSid found for session {}, skipping audio", twilioSession.getId());
                    return;
                }
                
                ObjectNode audioDeltaMessage = objectMapper.createObjectNode();
                audioDeltaMessage.put("event", "media");
                audioDeltaMessage.put("streamSid", streamSid);
                
                ObjectNode media = objectMapper.createObjectNode();
                media.put("payload", audioDelta);  // OpenAI audio is already in correct base64 format
                
                audioDeltaMessage.set("media", media);

                String message = objectMapper.writeValueAsString(audioDeltaMessage);
                log.trace("Sending to Twilio: {}", message);
                twilioSession.sendMessage(new TextMessage(message));
            }
        } catch (Exception e) {
            log.error("Error sending audio to Twilio", e);
        }
    }
    
    public void setStreamSid(String sessionId, String streamSid) {
        sessionStreamIds.put(sessionId, streamSid);
        log.info("Set streamSid {} for session {}", streamSid, sessionId);
    }
    
    public void setCallSid(String sessionId, String callSid) {
        sessionCallSids.put(sessionId, callSid);
        log.info("Set callSid {} for session {}", callSid, sessionId);
    }
    
    public void setCallSidForSession(String sessionId, String callSid) {
        setCallSid(sessionId, callSid);
    }
    
    public void closeConnection(WebSocketSession session) {
        disconnectFromOpenAI(session);
    }

    public void disconnectFromOpenAI(WebSocketSession twilioSession) {
        String sessionId = twilioSession.getId();
        
        // Calculate and save AI costs before cleanup
        saveAiCostsForSession(sessionId);
        
        // Cleanup session resources
        WebSocketClient client = openAIClients.remove(sessionId);
        sessionStreamIds.remove(sessionId);
        sessionCallSids.remove(sessionId);
        sessionTranscripts.remove(sessionId);
        sessionUsage.remove(sessionId);
        
        if (client != null && client.isOpen()) {
            client.close();
            log.info("Closed OpenAI connection for session: {}", sessionId);
        }
    }
    
    private void addToTranscript(String sessionId, String role, String content) {
        StringBuilder transcript = sessionTranscripts.get(sessionId);
        if (transcript != null) {
            try {
                if (transcript.length() > 1) {
                    transcript.append(",");
                }
                ObjectNode message = objectMapper.createObjectNode();
                message.put("role", role);
                message.put("content", content);
                message.put("timestamp", LocalDateTime.now().toString());
                transcript.append(objectMapper.writeValueAsString(message));
            } catch (Exception e) {
                log.error("Error adding to transcript", e);
            }
        }
    }
    
    private String getConversationTranscript(String sessionId) {
        StringBuilder transcript = sessionTranscripts.get(sessionId);
        if (transcript != null && transcript.length() > 1) {
            return transcript.toString() + "]";
        }
        return null;
    }
    
    /**
     * Capture usage metrics from OpenAI response.done event
     */
    private void captureUsageMetrics(String sessionId, JsonNode responseNode) {
        try {
            if (!responseNode.has("usage")) {
                log.debug("No usage data in response.done event");
                return;
            }
            
            JsonNode usage = responseNode.get("usage");
            long inputTokens = usage.has("input_tokens") ? usage.get("input_tokens").asLong() : 0;
            long outputTokens = usage.has("output_tokens") ? usage.get("output_tokens").asLong() : 0;
            
            // OpenAI Realtime API uses audio-specific token fields
            long inputAudioTokens = usage.has("input_audio_tokens") ? usage.get("input_audio_tokens").asLong() : inputTokens;
            long outputAudioTokens = usage.has("output_audio_tokens") ? usage.get("output_audio_tokens").asLong() : outputTokens;
            
            UsageData sessionUsageData = sessionUsage.get(sessionId);
            if (sessionUsageData != null) {
                sessionUsageData.addUsage(inputAudioTokens, outputAudioTokens);
                log.info("💰 [OpenAI] Usage captured - Session: {}, Input: {} tokens, Output: {} tokens", 
                        sessionId, inputAudioTokens, outputAudioTokens);
            }
        } catch (Exception e) {
            log.error("Error capturing usage metrics", e);
        }
    }
    
    /**
     * Calculate and save AI costs for a session to admin database
     */
    private void saveAiCostsForSession(String sessionId) {
        try {
            UsageData usage = sessionUsage.get(sessionId);
            String callSid = sessionCallSids.get(sessionId);
            
            if (usage == null || callSid == null) {
                log.debug("No usage data or callSid for session {}, skipping AI cost tracking", sessionId);
                return;
            }
            
            // Set call end time
            usage.setCallEndTime(LocalDateTime.now());
            
            // Check if there's any usage to track
            if (usage.getInputAudioTokens() == 0 && usage.getOutputAudioTokens() == 0) {
                log.info("💰 [OpenAI] No AI usage for call {}, skipping cost tracking", callSid);
                return;
            }
            
            log.info("💰 [OpenAI] Calculating AI costs for call {} - Input: {} tokens, Output: {} tokens", 
                    callSid, usage.getInputAudioTokens(), usage.getOutputAudioTokens());
            
            // Build usage metrics map for calculator
            Map<String, Object> usageMetrics = new HashMap<>();
            usageMetrics.put("input_audio_tokens", usage.getInputAudioTokens());
            usageMetrics.put("output_audio_tokens", usage.getOutputAudioTokens());
            usageMetrics.put("model", "gpt-4o-realtime-preview-2024-10-01");
            usageMetrics.put("session_id", sessionId);
            
            // Calculate cost using OpenAI cost calculator
            AiCostCalculationResult costResult = aiCostCalculator.calculateCost(usageMetrics);
            
            if (costResult != null && costResult.isValid()) {
                log.info("💰 [OpenAI] Cost calculated - ${} USD for call {}", costResult.getCost(), callSid);
                
                // Save to admin database
                aiCostTrackingService.saveAiCost(
                    callSid,
                    usage.getTenantId() != null ? usage.getTenantId() : "default_tenant",
                    costResult,
                    usage.getCallStartTime(),
                    usage.getCallEndTime(),
                    usage.getFromNumber(),
                    usage.getToNumber()
                );
                
                log.info("✅ [OpenAI] AI costs saved to admin database for call {}", callSid);
            } else {
                log.warn("⚠️ [OpenAI] Invalid cost calculation result for call {}", callSid);
            }
            
        } catch (Exception e) {
            log.error("❌ [OpenAI] Error saving AI costs for session {}", sessionId, e);
        }
    }
    
    /**
     * Set call metadata for cost tracking
     */
    public void setCallMetadata(String sessionId, String tenantId, String fromNumber, String toNumber) {
        UsageData usage = sessionUsage.get(sessionId);
        if (usage != null) {
            usage.setTenantId(tenantId);
            usage.setFromNumber(fromNumber);
            usage.setToNumber(toNumber);
            log.debug("Set call metadata for session {} - Tenant: {}, From: {}, To: {}", 
                    sessionId, tenantId, fromNumber, toNumber);
        }
    }
}
