package com.expensetracker.api.security.service;

import com.expensetracker.api.auth.model.RefreshToken;
import com.expensetracker.api.auth.repository.RefreshTokenRepository;
import com.expensetracker.api.user.model.User;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.OffsetDateTime;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

@Service
@RequiredArgsConstructor
public class RefreshTokenService {

  private static final int TOKEN_BYTES = 32;

  private final RefreshTokenRepository refreshTokens;
  private final SecureRandom secureRandom = new SecureRandom();

  @Value("${app.jwt.refresh-expires-days:30}")
  private long refreshExpiresDays;

  @Transactional
  public String createToken(User user) {
    String plainToken = generateToken();
    refreshTokens.save(RefreshToken.builder()
        .user(user)
        .tokenHash(hashToken(plainToken))
        .expiresAt(OffsetDateTime.now().plusDays(refreshExpiresDays))
        .build());
    return plainToken;
  }

  @Transactional
  public RotationResult rotate(String plainToken) {
    RefreshToken existingToken = getActiveToken(plainToken);
    existingToken.setRevokedAt(OffsetDateTime.now());

    String replacementPlainToken = generateToken();
    String replacementHash = hashToken(replacementPlainToken);
    existingToken.setReplacedByTokenHash(replacementHash);

    refreshTokens.save(existingToken);
    refreshTokens.save(RefreshToken.builder()
        .user(existingToken.getUser())
        .tokenHash(replacementHash)
        .expiresAt(OffsetDateTime.now().plusDays(refreshExpiresDays))
        .build());

    return new RotationResult(existingToken.getUser(), replacementPlainToken);
  }

  @Transactional
  public void revoke(String plainToken) {
    RefreshToken existingToken = getActiveToken(plainToken);
    existingToken.setRevokedAt(OffsetDateTime.now());
    refreshTokens.save(existingToken);
  }

  private RefreshToken getActiveToken(String plainToken) {
    RefreshToken existingToken = refreshTokens.findByTokenHash(hashToken(plainToken))
        .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid refresh token"));

    if (!existingToken.isActive()) {
      throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Refresh token expired or revoked");
    }

    return existingToken;
  }

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

  private String hashToken(String token) {
    if (token == null || token.isBlank()) {
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Refresh token is required");
    }

    try {
      MessageDigest digest = MessageDigest.getInstance("SHA-256");
      byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
      StringBuilder hex = new StringBuilder(hash.length * 2);
      for (byte b : hash) {
        hex.append(String.format("%02x", b));
      }
      return hex.toString();
    } catch (NoSuchAlgorithmException e) {
      throw new IllegalStateException("SHA-256 not available", e);
    }
  }

  public record RotationResult(User user, String refreshToken) {}
}
