package de.cannamanage.api.service;

import de.cannamanage.api.dto.auth.LoginRequest;
import de.cannamanage.api.dto.auth.LoginResponse;
import de.cannamanage.api.dto.auth.RefreshRequest;
import de.cannamanage.api.dto.auth.SetPasswordRequest;
import de.cannamanage.api.security.JwtService;
import de.cannamanage.domain.entity.InviteToken;
import de.cannamanage.domain.entity.StaffAccount;
import de.cannamanage.domain.entity.User;
import de.cannamanage.service.repository.InviteTokenRepository;
import de.cannamanage.service.repository.StaffAccountRepository;
import de.cannamanage.service.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.time.Instant;
import java.util.HexFormat;
import java.util.UUID;

/**
 * Authentication service — handles login, token refresh, and invite-based password setup.
 * Stateless JWT approach: no UserDetailsService needed.
 * Refresh tokens are hashed and stored on the User entity for revocation support.
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository userRepository;
    private final JwtService jwtService;
    private final PasswordEncoder passwordEncoder;
    private final InviteTokenRepository inviteTokenRepository;
    private final StaffAccountRepository staffAccountRepository;

    @Transactional
    public LoginResponse login(LoginRequest request) {
        User user = userRepository.findByEmail(request.email())
                .orElseThrow(() -> new AuthenticationException("Invalid credentials"));

        if (!user.isActive()) {
            throw new AuthenticationException("Account not activated");
        }

        if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) {
            throw new AuthenticationException("Invalid credentials");
        }

        // Generate tokens
        String roleName = user.getRole().name().replace("ROLE_", "");
        String accessToken = jwtService.generateAccessToken(
                user.getId(), user.getTenantId(), roleName, user.getEmail());
        String refreshToken = jwtService.generateRefreshToken(user.getId(), user.getTenantId());

        // Store SHA-256 hashed refresh token for revocation (BCrypt can't handle >72 bytes)
        user.setRefreshTokenHash(sha256(refreshToken));
        user.setLastLogin(Instant.now());
        userRepository.save(user);

        log.info("User {} logged in for tenant {}", user.getEmail(), user.getTenantId());

        return new LoginResponse(accessToken, refreshToken, 3600L, roleName);
    }

    @Transactional
    public LoginResponse refresh(RefreshRequest request) {
        String token = request.refreshToken();

        if (!jwtService.isTokenValid(token)) {
            throw new AuthenticationException("Invalid or expired refresh token");
        }

        UUID userId = jwtService.extractUserId(token);
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new AuthenticationException("User not found"));

        if (!user.isActive()) {
            throw new AuthenticationException("Account not activated");
        }

        // Verify the refresh token matches stored hash (revocation check)
        if (user.getRefreshTokenHash() == null ||
                !sha256(token).equals(user.getRefreshTokenHash())) {
            throw new AuthenticationException("Refresh token has been revoked");
        }

        // Rotate refresh token
        String roleName = user.getRole().name().replace("ROLE_", "");
        String newAccessToken = jwtService.generateAccessToken(
                user.getId(), user.getTenantId(), roleName, user.getEmail());
        String newRefreshToken = jwtService.generateRefreshToken(user.getId(), user.getTenantId());

        user.setRefreshTokenHash(sha256(newRefreshToken));
        userRepository.save(user);

        return new LoginResponse(newAccessToken, newRefreshToken, 3600L, roleName);
    }

    /**
     * Sets the password for a user via invite token.
     * Validates the token, sets the password hash, marks user active, marks token as used.
     * Security: generic error message for invalid/expired tokens (don't reveal state).
     */
    @Transactional
    public void setPassword(SetPasswordRequest request) {
        // Find valid (unused + not expired) token — security: generic error message
        InviteToken inviteToken = inviteTokenRepository
                .findByTokenAndUsedAtIsNullAndExpiresAtAfter(request.token(), Instant.now())
                .orElseThrow(() -> new AuthenticationException("Invalid or expired token"));

        User user = inviteToken.getUser();

        // Set password and activate user
        user.setPasswordHash(passwordEncoder.encode(request.password()));
        user.setActive(true);
        userRepository.save(user);

        // Mark token as used
        inviteToken.setUsedAt(Instant.now());
        inviteTokenRepository.save(inviteToken);

        // Update staff account activation timestamp
        staffAccountRepository.findByUserId(user.getId())
                .ifPresent(staff -> {
                    staff.setActivatedAt(Instant.now());
                    staffAccountRepository.save(staff);
                });

        log.info("Password set for user {} via invite token", user.getEmail());
    }

    /**
     * SHA-256 hash for refresh token storage.
     * JWTs exceed BCrypt's 72-byte limit (enforced in Spring Security 7+).
     * SHA-256 is appropriate here: refresh tokens are already high-entropy random strings.
     */
    private String sha256(String input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(hash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("SHA-256 not available", e);
        }
    }

    /**
     * Custom authentication exception — caught by GlobalExceptionHandler.
     */
    public static class AuthenticationException extends RuntimeException {
        public AuthenticationException(String message) {
            super(message);
        }
    }
}
