package com.saas.tenant.service;

import com.saas.admin.entity.AdminInboundCallData;
import com.saas.admin.repository.AdminInboundCallDataRepository;
import com.saas.tenant.entity.InboundCallData;
import com.saas.tenant.entity.InboundCallRequest;
import com.saas.tenant.repository.InboundCallDataRepository;
import com.saas.tenant.repository.InboundCallRequestRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@Slf4j
@RequiredArgsConstructor
public class InboundCallService {

    private final InboundCallDataRepository callDataRepository;
    private final InboundCallRequestRepository callRequestRepository;
    private final AdminInboundCallDataRepository adminCallDataRepository;
    private final EntityManagerFactory entityManagerFactory;

    @Transactional
    public InboundCallData saveCallData(InboundCallData callData) {
        try {
            InboundCallData saved = callDataRepository.save(callData);
            log.info("💾 Call data saved - CallSid: {}, From: {}, To: {}", 
                    saved.getCallSid(), saved.getFromNumber(), saved.getToNumber());
            return saved;
        } catch (Exception e) {
            log.error("Error saving call data for CallSid: {}", callData.getCallSid(), e);
            throw e;
        }
    }

    /**
     * Save call data in BOTH admin (saas_db) and tenant databases.
     * 
     * This method fulfills the requirement: "Inbound call data saved to BOTH admin and tenant databases"
     * 
     * CRITICAL: This method ensures BOTH saves succeed. If either fails, the entire operation fails.
     * 
     * @param callData The call data to save
     * @param tenantId The tenant ID (for admin DB tracking)
     * @param tenantSchema The tenant schema (e.g., "tenant_1")
     * @throws RuntimeException if either admin or tenant save fails
     */
    public InboundCallData saveInBothDatabases(InboundCallData callData, String tenantId, String tenantSchema) {
        // 1. Save to ADMIN database (saas_db) FIRST
        AdminInboundCallData adminCallData;
        try {
            adminCallData = AdminInboundCallData.builder()
                    .tenantId(tenantId)
                    .callSid(callData.getCallSid())
                    .parentCallSid(callData.getParentCallSid())
                    .fromNumber(callData.getFromNumber())
                    .toNumber(callData.getToNumber())
                    .caller(callData.getCaller())
                    .called(callData.getCalled())
                    .forwardedFrom(callData.getForwardedFrom())
                    .provider(callData.getProvider())
                    .direction(callData.getDirection())
                    .callStatus(callData.getCallStatus())
                    .callToken(callData.getCallToken())
                    .fromCity(callData.getFromCity())
                    .fromState(callData.getFromState())
                    .fromZip(callData.getFromZip())
                    .fromCountry(callData.getFromCountry())
                    .toCity(callData.getToCity())
                    .toState(callData.getToState())
                    .toZip(callData.getToZip())
                    .toCountry(callData.getToCountry())
                    .callerCity(callData.getCallerCity())
                    .callerState(callData.getCallerState())
                    .callerZip(callData.getCallerZip())
                    .callerCountry(callData.getCallerCountry())
                    .calledCity(callData.getCalledCity())
                    .calledState(callData.getCalledState())
                    .calledZip(callData.getCalledZip())
                    .calledCountry(callData.getCalledCountry())
                    .startTime(callData.getStartTime())
                    .endTime(callData.getEndTime())
                    .duration(callData.getDuration())
                    .accountSid(callData.getAccountSid())
                    .apiVersion(callData.getApiVersion())
                    .recordingSid(callData.getRecordingSid())
                    .recordingUrl(callData.getRecordingUrl())
                    .stirVerstat(callData.getStirVerstat())
                    .conversation(callData.getConversation())  // CRITICAL: Copy conversation to admin DB
                    .build();
            
            adminCallData = adminCallDataRepository.save(adminCallData);
            log.info("✅ [1/2] Call data saved to ADMIN database (saas_db) - CallSid: {}, Tenant: {}", 
                    callData.getCallSid(), tenantId);
        } catch (Exception e) {
            log.error("❌ CRITICAL: Failed to save call data to ADMIN database - CallSid: {}", callData.getCallSid(), e);
            throw new RuntimeException("Failed to save call data to admin database (saas_db): " + e.getMessage(), e);
        }
        
        // 2. Save to TENANT database - If this fails, rollback admin record
        try {
            InboundCallData tenantCallData = saveInTenantSchema(callData, tenantSchema);
            log.info("✅ [2/2] Call data saved to TENANT database ({}) - CallSid: {}", 
                    tenantSchema, callData.getCallSid());
            log.info("🎯 SUCCESS: Call data saved to BOTH admin and tenant databases");
            return tenantCallData;
        } catch (Exception e) {
            log.error("❌ CRITICAL: Failed to save call data to TENANT database - CallSid: {}", callData.getCallSid(), e);
            log.error("🔄 ROLLBACK: Deleting orphaned admin record for CallSid: {}", callData.getCallSid());
            
            // Compensating transaction: Delete the admin record to maintain consistency
            try {
                adminCallDataRepository.delete(adminCallData);
                log.info("✅ ROLLBACK SUCCESS: Admin record deleted for CallSid: {}", callData.getCallSid());
            } catch (Exception deleteEx) {
                log.error("❌ ROLLBACK FAILED: Could not delete admin record - CallSid: {}", callData.getCallSid(), deleteEx);
                log.error("⚠️ CRITICAL: Orphaned admin record remains in saas_db - manual cleanup required!");
            }
            
            throw new RuntimeException("Failed to save call data to tenant database (" + tenantSchema + "): " + e.getMessage(), e);
        }
    }

    /**
     * Save call data in tenant schema using MANUAL Hibernate Session creation.
     * 
     * CRITICAL: This method creates a NEW Hibernate Session with EXPLICIT tenant identifier,
     * bypassing the thread-bound EntityManager from OpenEntityManagerInViewInterceptor.
     * 
     * This approach is safer than relying on ThreadLocal TenantContext because the tenant
     * is passed explicitly to the Session at creation time.
     * 
     * @param callData The call data to save
     * @param tenantSchema The tenant schema (e.g., "tenant_1")
     */
    public InboundCallData saveInTenantSchema(InboundCallData callData, String tenantSchema) {
        if (tenantSchema == null || tenantSchema.isBlank()) {
            throw new IllegalArgumentException("tenantSchema cannot be null or blank");
        }
        
        log.info("🆕 Creating MANUAL Hibernate Session for tenant schema: {}", tenantSchema);
        
        // Create a NEW Session with EXPLICIT tenant identifier
        // This bypasses ThreadLocal TenantContext and guarantees correct tenant routing
        org.hibernate.Session session = entityManagerFactory.unwrap(org.hibernate.SessionFactory.class)
                .withOptions()
                .tenantIdentifier(tenantSchema)
                .openSession();
        
        try {
            // Start transaction on the new Session
            session.beginTransaction();
            log.debug("✅ New Hibernate Session created with explicit tenantIdentifier: {}", tenantSchema);
            
            // Persist using the new Session
            session.persist(callData);
            session.flush();
            
            // Commit transaction
            session.getTransaction().commit();
            
            log.info("💾 Call data saved to TENANT schema '{}' - CallSid: {}, From: {}, To: {}", 
                    tenantSchema, callData.getCallSid(), callData.getFromNumber(), callData.getToNumber());
            return callData;
        } catch (Exception e) {
            // Rollback on error
            if (session.getTransaction() != null && session.getTransaction().isActive()) {
                session.getTransaction().rollback();
            }
            log.error("Error saving call data to tenant schema '{}' for CallSid: {}", 
                    tenantSchema, callData.getCallSid(), e);
            throw new RuntimeException("Failed to save call data to tenant schema: " + tenantSchema, e);
        } finally {
            // Always close the Session
            session.close();
            log.debug("🔒 Closed manual Hibernate Session for schema: {}", tenantSchema);
        }
    }

    @Transactional
    public InboundCallRequest savePatientRequest(InboundCallRequest request) {
        try {
            InboundCallRequest saved = callRequestRepository.save(request);
            log.info("📋 Patient request saved - CallSid: {}, Nom: {}, Téléphone: {}", 
                    saved.getCallSid(), saved.getNom(), saved.getTelephone());
            return saved;
        } catch (Exception e) {
            log.error("Error saving patient request for CallSid: {}", request.getCallSid(), e);
            throw e;
        }
    }

    @Transactional
    public void updateCallStatus(String callSid, String status, LocalDateTime endTime, Integer duration) {
        try {
            Optional<InboundCallData> callDataOpt = callDataRepository.findByCallSid(callSid);
            if (callDataOpt.isPresent()) {
                InboundCallData callData = callDataOpt.get();
                callData.setCallStatus(status);
                if (endTime != null) {
                    callData.setEndTime(endTime);
                }
                if (duration != null) {
                    callData.setDuration(duration);
                }
                callDataRepository.save(callData);
                log.info("✅ Call status updated - CallSid: {}, Status: {}", callSid, status);
            }
        } catch (Exception e) {
            log.error("Error updating call status for CallSid: {}", callSid, e);
        }
    }

    @Transactional(readOnly = true)
    public Optional<InboundCallData> getCallByCallSid(String callSid) {
        return callDataRepository.findByCallSid(callSid);
    }

    @Transactional(readOnly = true)
    public Optional<InboundCallRequest> getPatientRequestByCallSid(String callSid) {
        return callRequestRepository.findByCallSid(callSid);
    }

    @Transactional(readOnly = true)
    public boolean callExists(String callSid) {
        return callDataRepository.existsByCallSid(callSid);
    }

    @Transactional
    public boolean updateSmsStatus(String smsSid, String smsStatus) {
        try {
            Optional<InboundCallRequest> requestOpt = callRequestRepository.findBySmsSid(smsSid);
            if (requestOpt.isPresent()) {
                InboundCallRequest request = requestOpt.get();
                request.setSmsStatus(smsStatus);
                callRequestRepository.save(request);
                log.info("✅ SMS status updated - SID: {}, Status: {}", smsSid, smsStatus);
                return true;
            } else {
                log.warn("⚠️ No InboundCallRequest found with SMS SID: {}", smsSid);
                return false;
            }
        } catch (Exception e) {
            log.error("❌ Error updating SMS status for SID: {}", smsSid, e);
            return false;
        }
    }

    @Transactional
    public boolean updateTranscript(String callSessionId, java.util.List<java.util.Map<String, Object>> transcript) {
        try {
            Optional<InboundCallRequest> requestOpt = callRequestRepository.findByCallSid(callSessionId);
            if (requestOpt.isPresent()) {
                InboundCallRequest request = requestOpt.get();
                // Convert transcript list to JSON string
                com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
                String transcriptJson = mapper.writeValueAsString(transcript);
                request.setConversationTranscript(transcriptJson);
                callRequestRepository.save(request);
                log.info("✅ Transcript updated for CallSid: {}", callSessionId);
                return true;
            } else {
                log.warn("⚠️ No InboundCallRequest found with CallSid: {}", callSessionId);
                return false;
            }
        } catch (Exception e) {
            log.error("❌ Error updating transcript for CallSid: {}", callSessionId, e);
            return false;
        }
    }

    @Transactional(readOnly = true)
    public org.springframework.data.domain.Page<InboundCallData> getAllInboundCallsPaginated(org.springframework.data.domain.Pageable pageable) {
        return callDataRepository.findAll(pageable);
    }

    @Transactional(readOnly = true)
    public Optional<InboundCallData> getInboundCallByCallSid(String callSid) {
        return callDataRepository.findByCallSid(callSid);
    }

    @Transactional(readOnly = true)
    public List<InboundCallRequest> getCallRecordsByProvider(String provider) {
        return callRequestRepository.findAll().stream()
                .filter(req -> provider.equalsIgnoreCase(req.getProvider()))
                .collect(java.util.stream.Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<InboundCallRequest> getCallRecordsByDateRange(LocalDateTime startDate, LocalDateTime endDate) {
        return callRequestRepository.findAll().stream()
                .filter(req -> req.getCreatedAt() != null)
                .filter(req -> !req.getCreatedAt().isBefore(startDate) && !req.getCreatedAt().isAfter(endDate))
                .collect(java.util.stream.Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<InboundCallRequest> getAllCallRecords() {
        String currentTenant = com.saas.shared.core.TenantContext.getTenantId();
        log.debug("🔍 Fetching all call records for tenant schema: {}", currentTenant);
        return callRequestRepository.findAll();
    }

    @Transactional(readOnly = true)
    public Optional<InboundCallRequest> getCallRecordByCallSid(String callSid) {
        return callRequestRepository.findByCallSid(callSid);
    }

    @Transactional(readOnly = true)
    public List<InboundCallRequest> getCallRecordsByPatientPhone(String phoneNumber) {
        return callRequestRepository.findByTelephone(phoneNumber);
    }

    @Transactional(readOnly = true)
    public List<InboundCallRequest> getRecentCallRecords(int limit) {
        return callRequestRepository.findAll().stream()
                .sorted((r1, r2) -> r2.getCreatedAt().compareTo(r1.getCreatedAt()))
                .limit(limit)
                .collect(java.util.stream.Collectors.toList());
    }
}
