package com.gnx.telemedicine.service;

import com.gnx.telemedicine.dto.user.*;
import com.gnx.telemedicine.dto.admin.UserManagementDto;
import com.gnx.telemedicine.exception.DuplicateUserException;
import com.gnx.telemedicine.exception.PasswordMismatchException;
import com.gnx.telemedicine.mappers.UserMapper;
import com.gnx.telemedicine.model.Doctor;
import com.gnx.telemedicine.model.Patient;
import com.gnx.telemedicine.model.UserModel;
import com.gnx.telemedicine.model.enums.Role;
import com.gnx.telemedicine.repository.DoctorRepository;
import com.gnx.telemedicine.repository.PatientRepository;
import com.gnx.telemedicine.repository.UserRepository;
import com.gnx.telemedicine.util.InputSanitizer;
import com.gnx.telemedicine.util.PasswordValidator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

    private final UserMapper userMapper;

    private final UserRepository userRepository;
    private final PatientRepository patientRepository;
    private final DoctorRepository doctorRepository;

    private final PasswordEncoder passwordEncoder;
    private final PasswordValidator passwordValidator;
    private final PasswordPolicyService passwordPolicyService;

    @Value("${spring.mail.username}")
    private String adminEmail;



    public DoctorResponseDto registerDoctor(DoctorRegistrationDto doctorRegistrationDto) {
        Doctor doctor = userMapper.toDoctor(doctorRegistrationDto);
        UserModel savedUser = registerUser(doctorRegistrationDto.user(), Role.DOCTOR);
        doctor.setUser(savedUser);
        Doctor savedDoctor = doctorRepository.save(doctor);
        return userMapper.toDoctorResponse(savedDoctor);
    }

    public PatientResponseDto registerPatient(PatientRegistrationDto patientRegistrationDto) {
        Patient patient = userMapper.toPatient(patientRegistrationDto);
        UserModel savedUser = registerUser(patientRegistrationDto.user(), Role.PATIENT);
        patient.setUser(savedUser);
        Patient savedPatieht = patientRepository.save(patient);
        return userMapper.toPatientResponse(savedPatieht);
    }

    public void registerAdmin(UserRegistrationDto userRegistrationDto) {
        if (!userRegistrationDto.email().equals(adminEmail)) {
            throw new BadCredentialsException("Only admin can register");
        }
        registerUser(userRegistrationDto, Role.ADMIN);
    }

    //Helper methods

    @Transactional(rollbackFor = {Exception.class})
    private UserModel registerUser(UserRegistrationDto userRegistrationDto, Role role) {
        UserModel userModel = userMapper.toUser(userRegistrationDto);
        validateUser(userRegistrationDto);
        userModel.setRole(role);
        userModel.setIsActive(true);

        String plainPassword = userModel.getPassword();
        if (plainPassword == null) {
            throw new IllegalArgumentException("Password cannot be null");
        }

        // Persist the user to obtain an ID while avoiding storing the raw password
        String placeholderPassword = passwordEncoder.encode(java.util.UUID.randomUUID().toString());
        userModel.setPassword(placeholderPassword);
        userModel = userRepository.save(userModel);

        // Clear placeholder so password history logic does not treat it as a real password
        userModel.setPassword(null);

        passwordPolicyService.updatePassword(userModel, plainPassword);

        return userModel;
    }

    private void validateUser(UserRegistrationDto userRegistrationDto) {
        // Sanitize email
        String email = InputSanitizer.sanitizeEmail(userRegistrationDto.email());
        if (email == null) {
            throw new IllegalArgumentException("Invalid email format");
        }
        
        // Check for duplicate email
        if (userRepository.existsByEmail(email)) {
            throw new DuplicateUserException("User with email " + email + " already exists");
        }
        
        // Validate password
        Optional<String> password = Optional.ofNullable(userRegistrationDto.password().password());
        Optional<String> confirmPassword = Optional.ofNullable(userRegistrationDto.password().confirmPassword());
        if (password.isEmpty() || confirmPassword.isEmpty()) {
            throw new IllegalArgumentException("Password cannot be null");
        }
        
        String passwordValue = password.get();
        String confirmPasswordValue = confirmPassword.get();
        
        // Check password match
        if (!passwordValue.equals(confirmPasswordValue)) {
            throw new PasswordMismatchException("Passwords do not match");
        }
        
        // Validate password complexity
        PasswordValidator.ValidationResult passwordValidation = passwordValidator.validate(passwordValue);
        if (!passwordValidation.isValid()) {
            log.warn("Password validation failed for user {}: {}", email, passwordValidation.getMessage());
            throw new IllegalArgumentException("Password does not meet requirements: " + passwordValidation.getMessage());
        }
        
        // Check for SQL injection and XSS in email
        if (InputSanitizer.containsSqlInjection(email) || InputSanitizer.containsXss(email)) {
            log.warn("Potential security threat detected in email: {}", email);
            throw new IllegalArgumentException("Invalid input detected");
        }
    }

    @Cacheable(value = "userByEmail", key = "#email")
    public UserManagementDto getCurrentUser(String email) {
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new RuntimeException("User not found"));
        return userMapper.toUserManagementDto(user);
    }

    @Transactional
    @CacheEvict(value = {"userByEmail", "userProfile"}, key = "#email")
    public UserManagementDto updateUserProfile(String email, UserUpdateDto updateDto) {
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new RuntimeException("User not found"));

        // Sanitize and validate input
        if (updateDto.firstName() != null && !updateDto.firstName().isBlank()) {
            String sanitized = InputSanitizer.sanitizeText(updateDto.firstName());
            if (sanitized == null || sanitized.length() > 100) {
                throw new IllegalArgumentException("Invalid first name");
            }
            if (InputSanitizer.containsSqlInjection(sanitized) || InputSanitizer.containsXss(sanitized)) {
                log.warn("Potential security threat detected in firstName for user: {}", email);
                throw new IllegalArgumentException("Invalid input detected");
            }
            user.setFirstName(sanitized);
        }
        if (updateDto.lastName() != null && !updateDto.lastName().isBlank()) {
            String sanitized = InputSanitizer.sanitizeText(updateDto.lastName());
            if (sanitized == null || sanitized.length() > 100) {
                throw new IllegalArgumentException("Invalid last name");
            }
            if (InputSanitizer.containsSqlInjection(sanitized) || InputSanitizer.containsXss(sanitized)) {
                log.warn("Potential security threat detected in lastName for user: {}", email);
                throw new IllegalArgumentException("Invalid input detected");
            }
            user.setLastName(sanitized);
        }
        if (updateDto.phoneNumber() != null && !updateDto.phoneNumber().isBlank()) {
            String sanitized = InputSanitizer.sanitizePhone(updateDto.phoneNumber());
            if (sanitized == null || sanitized.length() > 20) {
                throw new IllegalArgumentException("Invalid phone number");
            }
            user.setPhoneNumber(sanitized);
        }

        UserModel saved = userRepository.save(user);
        return userMapper.toUserManagementDto(saved);
    }

    @Transactional
    public DoctorResponseDto updateDoctorProfile(String email, DoctorUpdateDto updateDto) {
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new RuntimeException("User not found"));

        Doctor doctor = doctorRepository.findByUser(user)
                .orElseThrow(() -> new RuntimeException("Doctor profile not found"));

        if (updateDto.medicalLicenseNumber() != null && !updateDto.medicalLicenseNumber().isBlank()) {
            doctor.setMedicalLicenseNumber(updateDto.medicalLicenseNumber());
        }
        if (updateDto.specialization() != null && !updateDto.specialization().isBlank()) {
            doctor.setSpecialization(updateDto.specialization());
        }
        if (updateDto.yearsOfExperience() != null) {
            doctor.setYearsOfExperience(updateDto.yearsOfExperience());
        }
        if (updateDto.biography() != null) {
            doctor.setBiography(updateDto.biography());
        }
        if (updateDto.consultationFee() != null) {
            doctor.setConsultationFee(updateDto.consultationFee());
        }
        if (updateDto.defaultDurationMinutes() != null) {
            doctor.setDefaultDurationMinutes(updateDto.defaultDurationMinutes());
        }
        // Enterprise fields
        if (updateDto.streetAddress() != null) {
            doctor.setStreetAddress(updateDto.streetAddress());
        }
        if (updateDto.city() != null) {
            doctor.setCity(updateDto.city());
        }
        if (updateDto.state() != null) {
            doctor.setState(updateDto.state());
        }
        if (updateDto.zipCode() != null) {
            doctor.setZipCode(updateDto.zipCode());
        }
        if (updateDto.country() != null) {
            doctor.setCountry(updateDto.country());
        }
        if (updateDto.educationDegree() != null) {
            doctor.setEducationDegree(updateDto.educationDegree());
        }
        if (updateDto.educationUniversity() != null) {
            doctor.setEducationUniversity(updateDto.educationUniversity());
        }
        if (updateDto.educationGraduationYear() != null) {
            doctor.setEducationGraduationYear(updateDto.educationGraduationYear());
        }
        if (updateDto.certifications() != null) {
            doctor.setCertifications(updateDto.certifications());
        }
        if (updateDto.languagesSpoken() != null) {
            doctor.setLanguagesSpoken(updateDto.languagesSpoken());
        }
        if (updateDto.hospitalAffiliations() != null) {
            doctor.setHospitalAffiliations(updateDto.hospitalAffiliations());
        }
        if (updateDto.insuranceAccepted() != null) {
            doctor.setInsuranceAccepted(updateDto.insuranceAccepted());
        }
        if (updateDto.professionalMemberships() != null) {
            doctor.setProfessionalMemberships(updateDto.professionalMemberships());
        }

        Doctor saved = doctorRepository.save(doctor);
        return userMapper.toDoctorResponse(saved);
    }

    @Transactional
    public PatientResponseDto updatePatientProfile(String email, PatientUpdateDto updateDto) {
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new RuntimeException("User not found"));

        Patient patient = patientRepository.findByUser(user)
                .orElseThrow(() -> new RuntimeException("Patient profile not found"));

        if (updateDto.emergencyContactName() != null && !updateDto.emergencyContactName().isBlank()) {
            patient.setEmergencyContactName(updateDto.emergencyContactName());
        }
        if (updateDto.emergencyContactPhone() != null && !updateDto.emergencyContactPhone().isBlank()) {
            patient.setEmergencyContactPhone(updateDto.emergencyContactPhone());
        }
        if (updateDto.bloodType() != null && !updateDto.bloodType().isBlank()) {
            patient.setBloodType(updateDto.bloodType());
        }
        if (updateDto.allergies() != null) {
            patient.setAllergies(updateDto.allergies());
        }
        // Enterprise fields
        if (updateDto.dateOfBirth() != null) {
            patient.setDateOfBirth(updateDto.dateOfBirth());
        }
        if (updateDto.gender() != null) {
            patient.setGender(updateDto.gender());
        }
        if (updateDto.streetAddress() != null) {
            patient.setStreetAddress(updateDto.streetAddress());
        }
        if (updateDto.city() != null) {
            patient.setCity(updateDto.city());
        }
        if (updateDto.state() != null) {
            patient.setState(updateDto.state());
        }
        if (updateDto.zipCode() != null) {
            patient.setZipCode(updateDto.zipCode());
        }
        if (updateDto.country() != null) {
            patient.setCountry(updateDto.country());
        }
        if (updateDto.insuranceProvider() != null) {
            patient.setInsuranceProvider(updateDto.insuranceProvider());
        }
        if (updateDto.insurancePolicyNumber() != null) {
            patient.setInsurancePolicyNumber(updateDto.insurancePolicyNumber());
        }
        if (updateDto.medicalHistorySummary() != null) {
            patient.setMedicalHistorySummary(updateDto.medicalHistorySummary());
        }
        if (updateDto.currentMedications() != null) {
            patient.setCurrentMedications(updateDto.currentMedications());
        }
        if (updateDto.primaryCarePhysicianName() != null) {
            patient.setPrimaryCarePhysicianName(updateDto.primaryCarePhysicianName());
        }
        if (updateDto.primaryCarePhysicianPhone() != null) {
            patient.setPrimaryCarePhysicianPhone(updateDto.primaryCarePhysicianPhone());
        }
        if (updateDto.preferredLanguage() != null) {
            patient.setPreferredLanguage(updateDto.preferredLanguage());
        }
        if (updateDto.occupation() != null) {
            patient.setOccupation(updateDto.occupation());
        }
        if (updateDto.maritalStatus() != null) {
            patient.setMaritalStatus(updateDto.maritalStatus());
        }

        Patient saved = patientRepository.save(patient);
        return userMapper.toPatientResponse(saved);
    }

    @Transactional
    public void changePassword(String email, String currentPassword, String newPassword, String confirmNewPassword) {
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new RuntimeException("User not found"));
        
        // Verify current password
        if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
            log.warn("Password change failed - incorrect current password for user: {}", email);
            throw new BadCredentialsException("Current password is incorrect");
        }
        
        // Check password match
        if (!newPassword.equals(confirmNewPassword)) {
            throw new PasswordMismatchException("New passwords do not match");
        }
        
        // Validate new password complexity
        PasswordValidator.ValidationResult passwordValidation = passwordValidator.validate(newPassword);
        if (!passwordValidation.isValid()) {
            log.warn("Password change failed - validation failed for user {}: {}", email, passwordValidation.getMessage());
            throw new IllegalArgumentException("New password does not meet requirements: " + passwordValidation.getMessage());
        }
        
        // Check if new password is same as current password
        if (passwordEncoder.matches(newPassword, user.getPassword())) {
            throw new IllegalArgumentException("New password must be different from current password");
        }
        
        // Check password history (prevent reuse)
        passwordPolicyService.validatePasswordPolicy(user, newPassword);
        
        // Update password with history tracking and expiration
        passwordPolicyService.updatePassword(user, newPassword);
        
        log.info("Password changed successfully for user: {}", email);
    }

    @Transactional(readOnly = true)
    public DoctorResponseDto getCurrentDoctorProfile(String email) {
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new RuntimeException("User not found"));

        Doctor doctor = doctorRepository.findByUser(user)
                .orElseThrow(() -> new RuntimeException("Doctor profile not found"));

        return userMapper.toDoctorResponse(doctor);
    }

    @Transactional(readOnly = true)
    public PatientResponseDto getCurrentPatientProfile(String email) {
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new RuntimeException("User not found"));

        Patient patient = patientRepository.findByUser(user)
                .orElseThrow(() -> new RuntimeException("Patient profile not found"));

        return userMapper.toPatientResponse(patient);
    }

    @Transactional(readOnly = true)
    public List<DoctorResponseDto> getAllDoctors() {
        return doctorRepository.findAllWithActiveUser().stream()
                .map(userMapper::toDoctorResponse)
                .toList();
    }

    @Transactional(readOnly = true)
    public List<PatientResponseDto> getAllPatients() {
        return patientRepository.findAllWithActiveUser().stream()
                .map(userMapper::toPatientResponse)
                .toList();
    }

    @Transactional(readOnly = true)
    public DoctorResponseDto getDoctorProfileById(java.util.UUID doctorId) {
        Doctor doctor = doctorRepository.findById(doctorId)
                .orElseThrow(() -> new RuntimeException("Doctor not found"));
        return userMapper.toDoctorResponse(doctor);
    }

    @Transactional(readOnly = true)
    public PatientResponseDto getPatientProfileById(java.util.UUID patientId) {
        Patient patient = patientRepository.findById(patientId)
                .orElseThrow(() -> new RuntimeException("Patient not found"));
        return userMapper.toPatientResponse(patient);
    }
}
