package com.gnx.telemedicine.service;

import com.gnx.telemedicine.dto.auth.ForgotPasswordRequestDto;
import com.gnx.telemedicine.dto.auth.JwtResponseDto;
import com.gnx.telemedicine.dto.auth.PasswordResetResponseDto;
import com.gnx.telemedicine.dto.auth.RefreshTokenRequestDto;
import com.gnx.telemedicine.dto.auth.RefreshTokenResponseDto;
import com.gnx.telemedicine.dto.auth.ResetPasswordRequestDto;
import com.gnx.telemedicine.dto.auth.UserLoginDto;
import com.gnx.telemedicine.exception.PasswordMismatchException;
import com.gnx.telemedicine.exception.ResourceNotFoundException;
import com.gnx.telemedicine.exception.TwoFactorAuthenticationRequiredException;
import com.gnx.telemedicine.exception.UnauthorizedException;
import com.gnx.telemedicine.model.RefreshToken;
import com.gnx.telemedicine.model.UserModel;
import com.gnx.telemedicine.repository.RefreshTokenRepository;
import com.gnx.telemedicine.repository.UserRepository;
import com.gnx.telemedicine.security.JwtUtils;
import com.gnx.telemedicine.service.PasswordPolicyService;
import com.gnx.telemedicine.util.PasswordValidator;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.SecureRandom;
import java.time.Instant;
import java.util.Base64;
import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {

    private final AuthenticationManager authenticationManager;
    private final JwtUtils jwtUtils;
    private final UserRepository userRepository;
    private final RefreshTokenRepository refreshTokenRepository;
    private final LoginAttemptService loginAttemptService;
    private final TwoFactorAuthService twoFactorAuthService;
    private final EmailNotificationService emailNotificationService;
    private final PasswordEncoder passwordEncoder;
    private final PasswordPolicyService passwordPolicyService;
    private final PasswordValidator passwordValidator;
    private final com.gnx.telemedicine.metrics.TelemedicineMetrics telemedicineMetrics;
    
    @Value("${jwt.refresh-expiration:604800}")
    private int refreshTokenExpiration;
    
    @Value("${jwt.refresh-token.max-tokens-per-user:5}")
    private int maxTokensPerUser;
    
    private static final int TOKEN_EXPIRY_HOURS = 1;
    private static final SecureRandom secureRandom = new SecureRandom();

    @Transactional(rollbackFor = {Exception.class})
    public JwtResponseDto login(UserLoginDto userLoginDto, HttpServletRequest request) {
        // Record login attempt
        telemedicineMetrics.recordLoginAttempt();
        
        // Start authentication timer
        io.micrometer.core.instrument.Timer.Sample authTimer = telemedicineMetrics.startAuthenticationTimer();
        
        try {
            // Authenticate credentials first
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            userLoginDto.email(),
                            userLoginDto.password()
                    )
            );

            // Explicitly check if user is active before generating token
            UserModel user = userRepository.findByEmail(userLoginDto.email())
                    .orElseThrow(() -> new ResourceNotFoundException("User", userLoginDto.email()));
            
            if (!Boolean.TRUE.equals(user.getIsActive())) {
                loginAttemptService.recordFailedLogin(userLoginDto.email(), "Account deactivated", request);
                throw new DisabledException("Account is deactivated");
            }
            
            // Check if password is expired
            if (passwordPolicyService.isPasswordExpired(user)) {
                loginAttemptService.recordFailedLogin(userLoginDto.email(), "Password expired", request);
                throw new DisabledException("Your password has expired. Please reset your password.");
            }

            // Check if 2FA is enabled
            boolean is2FAEnabled = twoFactorAuthService.is2FAEnabled(userLoginDto.email());
            log.debug("Login attempt for {} - 2FA enabled: {}", userLoginDto.email(), is2FAEnabled);
            
            if (is2FAEnabled) {
                // If 2FA code is not provided, throw specific exception indicating 2FA is required
                if (userLoginDto.code() == null || userLoginDto.code().isEmpty()) {
                    log.debug("2FA enabled but no code provided for user: {}", userLoginDto.email());
                    // Don't record as failed - user needs to provide 2FA code
                    throw new TwoFactorAuthenticationRequiredException("2FA code required. Please provide your authentication code.");
                }

                // Verify 2FA code
                boolean isValidCode = twoFactorAuthService.verifyCode(
                    twoFactorAuthService.get2FASetup(userLoginDto.email())
                        .orElseThrow(() -> new RuntimeException("2FA not configured"))
                        .getSecretKey(),
                    userLoginDto.code()
                ) || twoFactorAuthService.verifyBackupCode(userLoginDto.email(), userLoginDto.code());

                if (!isValidCode) {
                    loginAttemptService.recordFailedLogin(userLoginDto.email(), "Invalid 2FA code", request);
                    throw new BadCredentialsException("Invalid 2FA code");
                }
            }

            // Record successful login
            loginAttemptService.recordSuccessfulLogin(userLoginDto.email(), request);

            // Restore user's last status on login
            if (user.getUserStatus() == null || user.getUserStatus() == com.gnx.telemedicine.model.enums.UserStatus.OFFLINE) {
                user.setUserStatus(com.gnx.telemedicine.model.enums.UserStatus.ONLINE);
                user.setIsOnline(true);
                userRepository.save(user);
            } else if (user.getUserStatus() == com.gnx.telemedicine.model.enums.UserStatus.ONLINE) {
                user.setIsOnline(true);
                userRepository.save(user);
            } else if (user.getUserStatus() == com.gnx.telemedicine.model.enums.UserStatus.BUSY) {
                user.setIsOnline(true);
                userRepository.save(user);
            }

            // Generate access token
            String accessToken = jwtUtils.generateToken(userLoginDto.email());
            
                // Generate and save refresh token
                String refreshTokenValue = jwtUtils.generateRefreshToken(userLoginDto.email());
                RefreshToken refreshToken = createRefreshToken(user, refreshTokenValue, request);
                refreshTokenRepository.save(refreshToken);

                // Record successful login
                telemedicineMetrics.recordLoginSuccess();
                telemedicineMetrics.recordAuthenticationTime(authTimer);
                telemedicineMetrics.incrementActiveUsers();

                log.debug("Login successful for user: {} - Access token and refresh token generated", userLoginDto.email());

                return new JwtResponseDto(accessToken, refreshTokenValue);

        } catch (TwoFactorAuthenticationRequiredException e) {
            // Don't record as failed login - this is expected when 2FA is enabled
            // Re-throw to be handled by GlobalExceptionHandler
            throw e;
        } catch (IllegalArgumentException e) {
            // Record other IllegalArgumentException errors as failed login
            loginAttemptService.recordFailedLogin(userLoginDto.email(), e.getMessage(), request);
            throw e;
        } catch (BadCredentialsException | DisabledException e) {
            loginAttemptService.recordFailedLogin(userLoginDto.email(), e.getMessage(), request);
            throw e;
        } catch (Exception e) {
            log.error("Unexpected error during login for {}: {}", userLoginDto.email(), e.getMessage(), e);
            loginAttemptService.recordFailedLogin(userLoginDto.email(), "Authentication failed", request);
            throw new BadCredentialsException("Invalid credentials");
        }
    }

    @Transactional(rollbackFor = {Exception.class})
    public void logout(String email) {
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new ResourceNotFoundException("User", email));

        // Revoke all refresh tokens for the user
        refreshTokenRepository.revokeAllTokensForUser(user, Instant.now(), "User logout");
        
        // Decrement active users
        telemedicineMetrics.decrementActiveUsers();
        
        // Set user to OFFLINE when logging out
        user.setUserStatus(com.gnx.telemedicine.model.enums.UserStatus.OFFLINE);
        user.setIsOnline(false);
        user.setLastSeen(java.time.LocalDateTime.now());
        userRepository.save(user);
        
        log.debug("Logout successful for user: {} - All refresh tokens revoked", email);
    }
    
    /**
     * Refresh access token using refresh token.
     */
    @Transactional(rollbackFor = {Exception.class})
    public RefreshTokenResponseDto refreshToken(RefreshTokenRequestDto request, HttpServletRequest httpRequest) {
        String refreshTokenValue = request.refreshToken();
        
        // Validate refresh token JWT
        if (!jwtUtils.validateRefreshToken(refreshTokenValue)) {
            throw new UnauthorizedException("Invalid or expired refresh token");
        }
        
        // Get email from refresh token
        String email = jwtUtils.getEmailFromToken(refreshTokenValue);
        
        // Find user
        UserModel user = userRepository.findByEmail(email)
                .orElseThrow(() -> new ResourceNotFoundException("User", email));
        
        // Check if user is active
        if (!Boolean.TRUE.equals(user.getIsActive())) {
            throw new DisabledException("Account is deactivated");
        }
        
        // Find refresh token in database
        RefreshToken refreshToken = refreshTokenRepository.findByTokenAndUser(refreshTokenValue, user)
                .orElseThrow(() -> new UnauthorizedException("Refresh token not found"));
        
        // Check if refresh token is valid (not revoked and not expired)
        if (!refreshToken.isValid()) {
            if (refreshToken.isExpired()) {
                refreshTokenRepository.delete(refreshToken);
                throw new UnauthorizedException("Refresh token has expired");
            }
            throw new UnauthorizedException("Refresh token has been revoked");
        }
        
        // Update last used timestamp
        refreshToken.updateLastUsed();
        refreshTokenRepository.save(refreshToken);
        
        // Generate new access token
        String newAccessToken = jwtUtils.generateToken(email);
        
        log.debug("Token refreshed for user: {}", email);
        
        return new RefreshTokenResponseDto(newAccessToken, refreshTokenValue);
    }
    
    /**
     * Create a new refresh token.
     */
    private RefreshToken createRefreshToken(UserModel user, String tokenValue, HttpServletRequest request) {
        // Check if user has too many active tokens
        long activeTokenCount = refreshTokenRepository.countActiveTokensByUser(user, Instant.now());
        if (activeTokenCount >= maxTokensPerUser) {
            // Revoke oldest tokens to make room
            List<RefreshToken> validTokens = refreshTokenRepository.findValidTokensByUser(user, Instant.now());
            int tokensToRevoke = (int) (activeTokenCount - maxTokensPerUser + 1);
            for (int i = 0; i < tokensToRevoke && i < validTokens.size(); i++) {
                RefreshToken oldToken = validTokens.get(i);
                oldToken.revoke("Maximum token limit reached");
                refreshTokenRepository.save(oldToken);
            }
            log.debug("Revoked {} old refresh tokens for user: {} to maintain limit", tokensToRevoke, user.getEmail());
        }
        
        // Extract device fingerprint and IP address
        String deviceFingerprint = request.getHeader("X-Device-Fingerprint");
        String ipAddress = getClientIpAddress(request);
        String userAgent = request.getHeader("User-Agent");
        
        // Create new refresh token
        Instant expiryDate = Instant.now().plusSeconds(refreshTokenExpiration);
        
        return RefreshToken.builder()
                .token(tokenValue)
                .user(user)
                .expiryDate(expiryDate)
                .deviceFingerprint(deviceFingerprint)
                .ipAddress(ipAddress)
                .userAgent(userAgent)
                .revoked(false)
                .build();
    }
    
    /**
     * Get client IP address from request.
     */
    private String getClientIpAddress(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        String xRealIp = request.getHeader("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty()) {
            return xRealIp;
        }
        return request.getRemoteAddr();
    }

    @Transactional
    public PasswordResetResponseDto forgotPassword(ForgotPasswordRequestDto request) {
        // Always return success message to prevent email enumeration
        String successMessage = "If an account with that email exists, a password reset link has been sent.";
        
        userRepository.findByEmail(request.email()).ifPresent(user -> {
            // Generate secure reset token
            String resetToken = generateResetToken();
            
            // Set token and expiry (1 hour from now)
            user.setPasswordResetToken(resetToken);
            user.setPasswordResetTokenExpiry(Instant.now().plusSeconds(TOKEN_EXPIRY_HOURS * 3600));
            userRepository.save(user);
            
            // Send password reset email
            emailNotificationService.sendPasswordResetEmail(
                    user.getEmail(),
                    resetToken,
                    user.getFirstName()
            );
            
            log.info("Password reset token generated for user: {}", user.getEmail());
        });
        
        // Record password reset request
        telemedicineMetrics.recordPasswordResetRequest();
        
        return new PasswordResetResponseDto(successMessage);
    }

    @Transactional(rollbackFor = {Exception.class})
    public PasswordResetResponseDto resetPassword(ResetPasswordRequestDto request) {
        // Find user by reset token
        UserModel user = userRepository.findByPasswordResetToken(request.token())
                .orElseThrow(() -> new BadCredentialsException("Invalid or expired reset token"));
        
        // Check if token is expired
        if (user.getPasswordResetTokenExpiry() == null || 
            user.getPasswordResetTokenExpiry().isBefore(Instant.now())) {
            // Clear expired token
            user.setPasswordResetToken(null);
            user.setPasswordResetTokenExpiry(null);
            userRepository.save(user);
            throw new BadCredentialsException("Reset token has expired. Please request a new one.");
        }
        
        // Validate passwords match
        if (!request.newPassword().equals(request.confirmPassword())) {
            throw new PasswordMismatchException("Passwords do not match");
        }
        
        // Validate password complexity
        PasswordValidator.ValidationResult passwordValidation = passwordValidator.validate(request.newPassword());
        if (!passwordValidation.isValid()) {
            throw new IllegalArgumentException("Password does not meet requirements: " + passwordValidation.getMessage());
        }
        
        // Check password history (prevent reuse)
        passwordPolicyService.validatePasswordPolicy(user, request.newPassword());
        
        // Update password with history tracking and expiration
        passwordPolicyService.updatePassword(user, request.newPassword());
        
        // Clear reset token
        user.setPasswordResetToken(null);
        user.setPasswordResetTokenExpiry(null);
        
        userRepository.save(user);
        
        // Record successful password reset
        telemedicineMetrics.recordPasswordResetSuccess();
        
        log.info("Password reset successful for user: {}", user.getEmail());
        
        return new PasswordResetResponseDto("Password has been reset successfully. You can now login with your new password.");
    }

    private String generateResetToken() {
        byte[] tokenBytes = new byte[32];
        secureRandom.nextBytes(tokenBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
    }
}
