package com.petshop.backend.service;

import com.petshop.backend.dto.auth.ForgotPasswordResponse;
import com.petshop.backend.dto.auth.ResetPasswordResponse;
import com.petshop.backend.entity.PasswordResetToken;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.repository.PasswordResetTokenRepository;
import com.petshop.backend.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.Optional;

@Service
public class PasswordResetService {

    private static final int RESET_TOKEN_BYTES = 32;
    private static final long RESET_TOKEN_MINUTES = 30;

    private final PasswordResetTokenRepository passwordResetTokenRepository;
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final EmailService emailService;
    private final SecureRandom secureRandom = new SecureRandom();

    public PasswordResetService(PasswordResetTokenRepository passwordResetTokenRepository,
                                UserRepository userRepository,
                                PasswordEncoder passwordEncoder,
                                EmailService emailService) {
        this.passwordResetTokenRepository = passwordResetTokenRepository;
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.emailService = emailService;
    }

    @Transactional
    public ForgotPasswordResponse createResetToken(String usernameOrEmail) {
        String normalized = trimToNull(usernameOrEmail);
        if (normalized == null) {
            throw new BusinessException("Username or email is required");
        }

        Optional<User> user = userRepository.findByEmail(normalized)
                .or(() -> userRepository.findByUsername(normalized));

        if (user.isEmpty() || !Boolean.TRUE.equals(user.get().getActive())) {
            return new ForgotPasswordResponse(
                    "If an account matches that username or email, a reset token has been generated.",
                    null
            );
        }

        User managedUser = user.get();
        LocalDateTime now = LocalDateTime.now();
        invalidateOutstandingTokens(managedUser.getId(), now);

        String rawToken = generateRawToken();
        PasswordResetToken resetToken = new PasswordResetToken();
        resetToken.setUser(managedUser);
        resetToken.setTokenHash(hashToken(rawToken));
        resetToken.setExpiresAt(now.plusMinutes(RESET_TOKEN_MINUTES));
        passwordResetTokenRepository.save(resetToken);

        emailService.sendPasswordResetLink(managedUser, rawToken);

        return new ForgotPasswordResponse(
                "If an account matches that username or email, a reset link has been sent.",
                null
        );
    }

    @Transactional
    public ResetPasswordResponse resetPassword(String rawToken, String newPassword) {
        String normalizedToken = trimToNull(rawToken);
        if (normalizedToken == null) {
            throw new BusinessException("Reset token is required");
        }

        PasswordResetToken token = passwordResetTokenRepository
                .findByTokenHashForUpdate(hashToken(normalizedToken), LocalDateTime.now())
                .orElseThrow(() -> new BusinessException("Reset token is invalid or has expired"));

        User user = token.getUser();
        if (!Boolean.TRUE.equals(user.getActive())) {
            throw new BusinessException("User account is inactive");
        }

        LocalDateTime now = LocalDateTime.now();
        user.setPassword(passwordEncoder.encode(newPassword));
        user.setTokenVersion(user.getTokenVersion() + 1);
        userRepository.save(user);

        token.setUsedAt(now);
        passwordResetTokenRepository.save(token);
        invalidateOutstandingTokens(user.getId(), now);

        return new ResetPasswordResponse("Password reset successfully");
    }

    private void invalidateOutstandingTokens(Long userId, LocalDateTime usedAt) {
        for (PasswordResetToken token : passwordResetTokenRepository.findByUser_IdAndUsedAtIsNull(userId)) {
            token.setUsedAt(usedAt);
        }
    }

    private String generateRawToken() {
        byte[] bytes = new byte[RESET_TOKEN_BYTES];
        secureRandom.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }

    private String hashToken(String rawToken) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hashed = digest.digest(rawToken.getBytes(StandardCharsets.UTF_8));
            StringBuilder builder = new StringBuilder(hashed.length * 2);
            for (byte value : hashed) {
                builder.append(String.format("%02x", value));
            }
            return builder.toString();
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException("SHA-256 is not available", ex);
        }
    }

    private String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
