package com.saas.voip.controller;

import com.saas.admin.entity.PhoneNumber;
import com.saas.admin.entity.Tenant;
import com.saas.admin.repository.PhoneNumberRepository;
import com.saas.admin.repository.TenantRepository;
import com.saas.shared.core.TenantContext;
import com.saas.shared.dto.VoipConfigDTO;
import com.saas.shared.enums.Provider;
import com.saas.shared.service.BaseUrlResolver;
import com.saas.shared.service.TenantVoipConfigRuntimeService;
import com.saas.tenant.entity.InboundCallData;
import com.saas.tenant.entity.InboundCallRequest;
import com.saas.tenant.service.InboundCallService;
import com.saas.voip.service.TelnyxVoiceAIService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * TeXML Controller for Telnyx Voice API
 * TeXML is 100% compatible with Twilio's TwiML
 * Configure this in Telnyx Portal: Voice → TeXML Applications
 */
@RestController
@RequestMapping("/api/voip/telnyx")
@Slf4j
@RequiredArgsConstructor
public class TelnyxTeXMLController {

    private final PhoneNumberRepository phoneNumberRepository;
    private final TenantRepository tenantRepository;
    private final InboundCallService inboundCallService;
    private final TenantVoipConfigRuntimeService voipRuntimeService;
    private final TelnyxVoiceAIService telnyxVoiceAIService;
    private final ObjectMapper objectMapper;
    private final BaseUrlResolver baseUrlResolver;
    private final com.saas.shared.service.AsyncCallService asyncCallService;
    private final RestTemplate restTemplate = new RestTemplate();

    @Value("${telnyx.api.key:#{null}}")
    private String telnyxApiKey;

    /**
     * TeXML Response endpoint - Returns XML like Twilio TwiML
     * Configure in Telnyx Portal: Voice → TeXML Applications → Webhook URL
     * 
     * Example URL: https://your-domain.com/api/voip/telnyx/texml-response
     */
    @PostMapping(value = "/texml-response", produces = MediaType.APPLICATION_XML_VALUE)
    public String handleTeXMLRequest(
            HttpServletRequest request,
            @RequestParam(value = "From", required = false) String from,
            @RequestParam(value = "To", required = false) String to,
            @RequestParam(value = "CallSid", required = false) String callSid,
            @RequestParam(value = "CallStatus", required = false) String callStatus) {

        log.info("=== TELNYX TeXML REQUEST ===");
        log.info("📞 From: {}, To: {}, CallSid: {}, Status: {}", from, to, callSid, callStatus);

        // CRITICAL: Get tenant info from request attributes (set by
        // TelnyxTenantResolverFilter)
        // The filter already resolved tenant and kept TenantContext as 'saas_db' for
        // EntityManager
        String tenantId = (String) request.getAttribute("RESOLVED_TENANT_ID");
        String tenantSchema = (String) request.getAttribute("RESOLVED_TENANT_SCHEMA");

        if (tenantId != null && tenantSchema != null) {
            log.info("✅ Using tenant from filter: {} (schema: {})", tenantId, tenantSchema);
        } else {
            log.warn("⚠️ No tenant info in request attributes - filter may not have resolved tenant");
        }

        // Save inbound call data to BOTH admin (saas_db) and tenant databases
        boolean callDataSaved = false;
        if (callSid != null && from != null && to != null && tenantId != null && tenantSchema != null) {
            try {
                InboundCallData callData = InboundCallData.builder()
                        .callSid(callSid)
                        .fromNumber(from)
                        .toNumber(to)
                        .callStatus(callStatus != null ? callStatus : "initiated")
                        .direction("inbound")
                        .provider(Provider.TELNYX.name()) // ✅ CRITICAL: Set provider to TELNYX
                        .startTime(LocalDateTime.now())
                        .build();

                // Save to BOTH admin and tenant databases asynchronously
                asyncCallService.saveInBoundCallAsync(callData, tenantId, tenantSchema);

                callDataSaved = true;
                log.info("✅ [Async] Triggered saving of Telnyx call data");
                log.info("   ├─ Admin DB (saas_db): ⏳");
                log.info("   ├─ Tenant DB ({}): ⏳", tenantSchema);
                log.info("   └─ CallSid: {}", callSid);
            } catch (Exception e) {
                log.error("╔═══════════════════════════════════════════════════════╗");
                log.error("║     CRITICAL ERROR: FAILED TO SAVE CALL DATA         ║");
                log.error("╚═══════════════════════════════════════════════════════╝");
                log.error("❌ Error saving Telnyx call data to databases");
                log.error("   ├─ TenantId: {}", tenantId);
                log.error("   ├─ Schema: {}", tenantSchema);
                log.error("   ├─ CallSid: {}", callSid);
                log.error("   └─ Error: {}", e.getMessage(), e);

                // Return error TeXML to signal failure to Telnyx
                return generateErrorTeXML("Database error - please try again");
            }
        }

        // If call data save failed, return error instead of proceeding
        if (!callDataSaved && callSid != null) {
            log.error("❌ Call data was NOT saved - returning error TeXML");
            return generateErrorTeXML("Failed to save call data");
        }

        // Resolve VoIP configuration (DB first, then fallback to environment)
        // TenantContext is already 'saas_db', so tenant_voip_configs will be read
        // correctly
        Optional<VoipConfigDTO> voipConfig = Optional.empty();
        if (tenantId != null) {
            voipConfig = voipRuntimeService.resolveVoipConfig(tenantId, Provider.TELNYX);
            log.info("📋 VoIP Config loaded for tenant: {} - Type: {}, AssistantID: {}",
                    tenantId, voipConfig.map(VoipConfigDTO::getAiType).orElse("N/A"),
                    voipConfig.map(VoipConfigDTO::getAiAssistantId).orElse("N/A"));
        }

        // Generate TeXML response (compatible with TwiML)
        return generateTeXMLResponse(request, voipConfig, tenantId, callSid);
    }

    /**
     * Generate TeXML/TwiML XML response using tenant VoIP configuration
     * TeXML supports all TwiML verbs: <Say>, <Dial>, <Start>, <Stream>, etc.
     */
    private String generateTeXMLResponse(HttpServletRequest request, Optional<VoipConfigDTO> configOpt, String tenantId,
            String callSid) {
        StringBuilder xml = new StringBuilder();
        xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        xml.append("<Response>\n");

        // If no config found or invalid, return error message
        if (configOpt.isEmpty()) {
            log.warn("⚠️ No VoIP configuration found for tenant: {}", tenantId);
            xml.append("  <Say language=\"fr-FR\">Configuration VoIP non trouvée.</Say>\n");
            xml.append("  <Hangup/>\n");
            xml.append("</Response>");
            return xml.toString();
        }

        VoipConfigDTO config = configOpt.get();

        if (!config.isValid()) {
            log.warn("⚠️ Invalid VoIP configuration for tenant: {}", tenantId);
            xml.append("  <Say language=\"fr-FR\">Configuration VoIP invalide.</Say>\n");
            xml.append("  <Hangup/>\n");
            xml.append("</Response>");
            return xml.toString();
        }

        String aiType = config.getAiType();
        String aiAssistantId = config.getAiAssistantId();
        String streamUrl = config.getStreamUrl();

        log.info("🔧 VoIP Config - Type: {}, AssistantID: {}, StreamURL: {}, Source: {}",
                aiType, aiAssistantId, streamUrl,
                config.isFromDatabase() ? "DATABASE" : "ENVIRONMENT");

        // Option 1: Use Telnyx Native Voice AI with Connect + AIAssistant
        if ("TELNYX_NATIVE_AI".equals(aiType)) {
            if (aiAssistantId != null && !aiAssistantId.isEmpty()) {
                log.info("🤖 Starting Telnyx Native AI Assistant with Connect: {}", aiAssistantId);

                // Construct statusCallback URL using BaseUrlResolver
                String statusCallbackUrl = baseUrlResolver.buildCallbackUrl(request,
                        "/api/voip/telnyx/status-callback");

                // Use Connect + AIAssistant with statusCallback for call details after hangup
                // Per Telnyx documentation: https://support.telnyx.com/en/articles/4374050
                xml.append("  <!-- Telnyx AI Assistant - Full Conversation Mode -->\n");
                xml.append("  <Connect statusCallback=\"").append(statusCallbackUrl).append("\" ");
                xml.append("statusCallbackEvent=\"initiated ringing answered completed\">\n");
                xml.append("    <AIAssistant id=\"").append(aiAssistantId).append("\"/>\n");
                xml.append("  </Connect>\n");

                log.info("📞 StatusCallback configured: {}", statusCallbackUrl);
            } else {
                log.warn("⚠️ AI Assistant ID not configured");
                xml.append("  <Say language=\"fr-FR\">Identifiant de l'assistant vocal non configuré.</Say>\n");
                xml.append("  <Hangup/>\n");
            }
        }
        // Option 2: Use WebSocket streaming (for OpenAI, ElevenLabs, etc.)
        else if ("WEBSOCKET_STREAM".equals(aiType)) {
            if (streamUrl != null && !streamUrl.isEmpty()) {
                log.info("🌐 Starting WebSocket stream to: {}", streamUrl);

                xml.append("  <!-- Stream audio to WebSocket for AI processing -->\n");
                xml.append("  <Start>\n");
                xml.append("    <Stream url=\"").append(streamUrl).append("\">\n");
                xml.append("      <Parameter name=\"tenantId\" value=\"")
                        .append(tenantId != null ? tenantId : "unknown").append("\"/>\n");
                xml.append("      <Parameter name=\"callSid\" value=\"").append(callSid != null ? callSid : "unknown")
                        .append("\"/>\n");
                xml.append("    </Stream>\n");
                xml.append("  </Start>\n");

                // Keep call alive while streaming AI audio (max 1 hour)
                xml.append("  <Pause length=\"3600\"/>\n");
            } else {
                log.warn("⚠️ Stream URL not configured");
                xml.append("  <Say language=\"fr-FR\">URL de streaming non configurée.</Say>\n");
                xml.append("  <Hangup/>\n");
            }
        }
        // Default: Simple greeting (for testing)
        else {
            log.info("📢 Using default greeting (no specific AI type configured)");
            xml.append("  <Say language=\"fr-FR\">Bonjour, vous êtes connecté au système Telnyx.</Say>\n");
            xml.append("  <Pause length=\"1\"/>\n");
            xml.append("  <Say language=\"fr-FR\">Veuillez configurer l'assistant vocal pour ce tenant.</Say>\n");
            xml.append("  <Hangup/>\n");
        }

        xml.append("</Response>");

        String response = xml.toString();
        log.info("📄 TeXML Response:\n{}", response);

        return response;
    }

    /**
     * Generate error TeXML response to signal failure to Telnyx
     * This allows Telnyx to retry or handle the failure appropriately
     */
    private String generateErrorTeXML(String errorMessage) {
        StringBuilder xml = new StringBuilder();
        xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        xml.append("<Response>\n");
        xml.append("  <Say language=\"fr-FR\">Une erreur s'est produite. </Say>\n");
        xml.append("  <Say language=\"fr-FR\">").append(errorMessage).append("</Say>\n");
        xml.append("  <Hangup/>\n");
        xml.append("</Response>");

        String response = xml.toString();
        log.error("📄 Error TeXML Response:\n{}", response);

        return response;
    }

    @GetMapping("/texml-test")
    public String testTeXML() {
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<Response>\n" +
                "  <Say language=\"fr-FR\">Test TeXML - Telnyx est correctement configuré.</Say>\n" +
                "</Response>";
    }

    /**
     * Status Callback endpoint - Receives call details after hangup
     * Called by Telnyx for events: initiated, ringing, answered, completed
     * 
     * Per Telnyx documentation: https://support.telnyx.com/en/articles/4374050
     */
    @PostMapping("/status-callback")
    public ResponseEntity<Map<String, Object>> handleStatusCallback(
            @RequestParam Map<String, String> params) {

        log.info("=== TELNYX STATUS CALLBACK ===");
        log.info("📊 Received parameters: {}", params);

        try {
            // Extract call details from Telnyx webhook
            String callSid = params.get("CallSid");
            String to = params.get("To");
            String from = params.get("From");
            String callStatus = params.get("CallStatus");
            String duration = params.get("CallDuration");
            String timestamp = params.get("Timestamp");

            log.info("📞 CallSid: {}, Status: {}, Duration: {}s, From: {}, To: {}",
                    callSid, callStatus, duration, from, to);

            if (callSid == null || to == null) {
                log.warn("⚠️ Missing CallSid or To number in status callback");
                return ResponseEntity.ok(Map.of("status", "ignored", "reason", "missing_data"));
            }

            // Identify tenant via phone number
            Optional<PhoneNumber> phoneOpt = phoneNumberRepository.findByPhoneNumber(to);

            if (phoneOpt.isEmpty() || phoneOpt.get().getProvider() != Provider.TELNYX) {
                log.warn("⚠️ No Telnyx phone number found for: {}", to);
                return ResponseEntity.ok(Map.of("status", "ignored", "reason", "phone_not_found"));
            }

            PhoneNumber phoneNumber = phoneOpt.get();
            String tenantId = phoneNumber.getTenantId();

            Optional<Tenant> tenantOpt = tenantRepository.findByTenantId(tenantId);
            if (tenantOpt.isEmpty()) {
                log.error("❌ Tenant not found for phone: {}", to);
                return ResponseEntity.ok(Map.of("status", "error", "reason", "tenant_not_found"));
            }

            Tenant tenant = tenantOpt.get();
            String schemaName = tenant.getSchemaName();

            TenantContext.setTenantId(schemaName);
            log.info("📊 Set tenant context: {}", schemaName);

            try {
                // Find existing call record by CallSid
                Optional<InboundCallData> existingCall = inboundCallService.getCallByCallSid(callSid);

                if (existingCall.isPresent()) {
                    // Update existing record with call completion details
                    InboundCallData callData = existingCall.get();
                    callData.setCallStatus(callStatus);

                    if (duration != null && !duration.isEmpty()) {
                        try {
                            callData.setDuration(Integer.parseInt(duration));
                        } catch (NumberFormatException e) {
                            log.warn("⚠️ Invalid duration format: {}", duration);
                        }
                    }

                    if ("completed".equalsIgnoreCase(callStatus)) {
                        callData.setEndTime(LocalDateTime.now());
                        log.info("✅ Call completed - Duration: {}s", duration);

                        // ===== RÉCUPÉRER CONVERSATION TELNYX =====
                        try {
                            log.info("📥 Retrieving conversation transcript for CallSid: {}", callSid);
                            Map<String, Object> conversationData = telnyxVoiceAIService
                                    .getConversationTranscript(callSid);

                            if (conversationData != null && !conversationData.isEmpty()) {
                                // Extract messages array from conversation response
                                Object messagesObj = conversationData.get("messages");
                                if (messagesObj != null) {
                                    List<Object> messages = objectMapper.convertValue(messagesObj,
                                            new TypeReference<List<Object>>() {
                                            });
                                    callData.setConversation(messages);
                                    log.info("✅ Conversation transcript stored ({} messages)", messages.size());

                                    // Extraire les données patient/RDV de la conversation
                                    extractPatientDataFromConversation(callSid, messages, schemaName);
                                } else {
                                    log.warn("⚠️ No messages found in conversation data");
                                }
                            } else {
                                log.warn("⚠️ No conversation data available for CallSid: {}", callSid);
                            }
                        } catch (Exception convEx) {
                            log.error("❌ Failed to retrieve conversation for CallSid: {}", callSid, convEx);
                        }
                    }

                    // Save to BOTH admin and tenant databases with conversation
                    inboundCallService.saveInBothDatabases(callData, tenantId, schemaName);
                    log.info("✅ Updated call record with status: {}", callStatus);

                } else {
                    log.warn("⚠️ No existing call record found for CallSid: {}", callSid);

                    // Create new record if this is the first callback
                    InboundCallData newCall = InboundCallData.builder()
                            .callSid(callSid)
                            .fromNumber(from)
                            .toNumber(to)
                            .callStatus(callStatus)
                            .direction("inbound")
                            .startTime(LocalDateTime.now())
                            .build();

                    if (duration != null && !duration.isEmpty()) {
                        try {
                            newCall.setDuration(Integer.parseInt(duration));
                        } catch (NumberFormatException e) {
                            log.warn("⚠️ Invalid duration: {}", duration);
                        }
                    }

                    inboundCallService.saveCallData(newCall);
                    log.info("✅ Created new call record from status callback");
                }

                return ResponseEntity.ok(Map.of(
                        "status", "success",
                        "message", "Status callback processed",
                        "callStatus", callStatus != null ? callStatus : "unknown"));

            } finally {
                TenantContext.clear();
            }

        } catch (Exception e) {
            log.error("❌ Error processing status callback", e);
            TenantContext.clear();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of("status", "error", "message", e.getMessage()));
        }
    }

    /**
     * Start Telnyx AI Assistant via API call
     * This is the CORRECT way to start an AI assistant from a TeXML webhook
     * 
     * Documentation:
     * https://developers.telnyx.com/docs/voice/programmable-voice/gather-using-ai
     */
    private void startTelnyxAIAssistant(String callControlId, String assistantId) {
        if (telnyxApiKey == null || telnyxApiKey.isEmpty()) {
            log.error("❌ TELNYX_API_KEY not configured! Cannot start AI assistant");
            log.error("💡 Add TELNYX_API_KEY to your .env file or application.properties");
            log.error("💡 Example: TELNYX_API_KEY=KEY0123456789ABCDEF...");
            return;
        }

        log.info("✅ Using Telnyx API Key: {}...", telnyxApiKey.substring(0, Math.min(10, telnyxApiKey.length())));

        if (callControlId == null || callControlId.isEmpty()) {
            log.error("❌ Call Control ID is null! Cannot start AI assistant");
            return;
        }

        try {
            String url = "https://api.telnyx.com/v2/calls/" + callControlId + "/actions/ai_assistant_start";

            HttpHeaders headers = new HttpHeaders();
            headers.set("Authorization", "Bearer " + telnyxApiKey);
            headers.setContentType(MediaType.APPLICATION_JSON);

            Map<String, Object> payload = new HashMap<>();
            Map<String, String> assistant = new HashMap<>();
            assistant.put("id", assistantId);
            payload.put("assistant", assistant);

            HttpEntity<Map<String, Object>> request = new HttpEntity<>(payload, headers);

            log.info("🔄 Calling Telnyx API to start AI assistant {} for call {}", assistantId, callControlId);

            ResponseEntity<String> response = restTemplate.exchange(
                    url,
                    HttpMethod.POST,
                    request,
                    String.class);

            if (response.getStatusCode().is2xxSuccessful()) {
                log.info("✅ AI Assistant started successfully! Response: {}", response.getBody());
            } else {
                log.error("❌ Failed to start AI assistant. Status: {}, Body: {}",
                        response.getStatusCode(), response.getBody());
            }

        } catch (Exception e) {
            log.error("❌ Error calling Telnyx API to start AI assistant", e);
        }
    }

    /**
     * Extract patient data and appointment info from conversation messages
     * and save to InboundCallRequest (tenant DB only)
     */
    private void extractPatientDataFromConversation(String callSid, List<Object> messages, String schemaName) {
        if (messages == null || messages.isEmpty()) {
            log.warn("⚠️ No conversation messages to extract patient data from");
            return;
        }

        try {
            log.info("🔍 Extracting patient data from {} conversation messages", messages.size());

            // Parse conversation to extract patient/appointment info
            // Look for assistant messages that contain structured data
            String patientName = null;
            String patientPhone = null;
            String illness = null;
            String doctorName = null;
            LocalDateTime appointmentDateTime = null;

            for (Object msgObj : messages) {
                if (msgObj instanceof Map) {
                    Map<String, Object> message = (Map<String, Object>) msgObj;
                    String role = (String) message.get("role");
                    String content = (String) message.get("content");

                    if (content != null && !content.isEmpty()) {
                        // Simple text parsing to extract patient info
                        // This is a basic implementation - improve based on actual conversation
                        // structure
                        if (patientName == null && content.toLowerCase().contains("nom")) {
                            // Extract name pattern
                            String[] parts = content.split(":");
                            if (parts.length > 1) {
                                patientName = parts[1].trim();
                            }
                        }
                        if (patientPhone == null && content.contains("+") && content.matches(".*\\+\\d{10,15}.*")) {
                            // Extract phone pattern
                            java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\+\\d{10,15}");
                            java.util.regex.Matcher matcher = pattern.matcher(content);
                            if (matcher.find()) {
                                patientPhone = matcher.group();
                            }
                        }
                        // Add more extraction logic as needed
                    }

                    // Check for function calls with structured data
                    if (message.containsKey("function_call")) {
                        Map<String, Object> functionCall = (Map<String, Object>) message.get("function_call");
                        if (functionCall != null && functionCall.containsKey("arguments")) {
                            String argsJson = (String) functionCall.get("arguments");
                            if (argsJson != null) {
                                try {
                                    Map<String, Object> args = objectMapper.readValue(argsJson,
                                            new TypeReference<Map<String, Object>>() {
                                            });

                                    // Extract from function arguments
                                    if (args.containsKey("patient_name"))
                                        patientName = (String) args.get("patient_name");
                                    if (args.containsKey("patient_phone"))
                                        patientPhone = (String) args.get("patient_phone");
                                    if (args.containsKey("illness") || args.containsKey("maladie")) {
                                        illness = (String) args.getOrDefault("illness", args.get("maladie"));
                                    }
                                    if (args.containsKey("doctor_name"))
                                        doctorName = (String) args.get("doctor_name");
                                    if (args.containsKey("appointment_datetime")) {
                                        try {
                                            appointmentDateTime = LocalDateTime
                                                    .parse((String) args.get("appointment_datetime"));
                                        } catch (Exception dtEx) {
                                            log.warn("Failed to parse appointment datetime", dtEx);
                                        }
                                    }
                                } catch (Exception jsonEx) {
                                    log.warn("Failed to parse function arguments JSON", jsonEx);
                                }
                            }
                        }
                    }
                }
            }

            // If we found any patient data, save it to tenant DB
            if (patientName != null || patientPhone != null || illness != null) {
                log.info("📝 Patient data extracted - Name: {}, Phone: {}, Illness: {}",
                        patientName, patientPhone, illness);

                // Set tenant context for saving to tenant DB
                TenantContext.setTenantId(schemaName);

                try {
                    InboundCallRequest request = new InboundCallRequest();
                    request.setCallSid(callSid);
                    request.setNom(patientName);
                    request.setTelephone(patientPhone);
                    request.setMaladie(illness);
                    request.setDoctorName(doctorName);
                    request.setAppointmentDateTime(appointmentDateTime);
                    request.setProvider("TELNYX");

                    // Store conversation as JSON in InboundCallRequest
                    String conversationJson = objectMapper.writeValueAsString(messages);
                    request.setConversationTranscript(conversationJson);

                    inboundCallService.savePatientRequest(request);
                    log.info("✅ Patient data saved to InboundCallRequest (tenant DB only)");
                } finally {
                    TenantContext.clear();
                }
            } else {
                log.info("⏭️ No patient data found in conversation");
            }

        } catch (Exception e) {
            log.error("❌ Error extracting patient data from conversation", e);
        }
    }
}
