package org.raddatz.familienarchiv.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.raddatz.familienarchiv.user.CreateInviteRequest;
import org.raddatz.familienarchiv.user.InviteListItemDTO;
import org.raddatz.familienarchiv.user.RegisterRequest;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.InviteToken;
import org.raddatz.familienarchiv.user.UserGroup;
import org.raddatz.familienarchiv.user.InviteTokenRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.SecureRandom;
import java.util.*;

@Service
@RequiredArgsConstructor
@Slf4j
public class InviteService {

    static final int MIN_PASSWORD_LENGTH = 8;
    private static final String CODE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    private static final int CODE_LENGTH = 10;
    private static final int MAX_CODE_ATTEMPTS = 10;
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();

    private final InviteTokenRepository inviteTokenRepository;
    private final UserService userService;

    public String generateCode() {
        for (int attempt = 0; attempt < MAX_CODE_ATTEMPTS; attempt++) {
            String code = buildRandomCode();
            if (inviteTokenRepository.findByCode(code).isEmpty()) {
                return code;
            }
        }
        throw DomainException.internal(ErrorCode.INTERNAL_ERROR, "Failed to generate unique invite code after " + MAX_CODE_ATTEMPTS + " attempts");
    }

    public InviteToken validateCode(String code) {
        InviteToken token = inviteTokenRepository.findByCode(code)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.INVITE_NOT_FOUND, "Invite not found: " + code));
        checkTokenState(token);
        return token;
    }

    @Transactional
    public InviteToken createInvite(CreateInviteRequest dto, AppUser creator) {
        Set<UUID> groupIds = new HashSet<>();
        if (dto.getGroupIds() != null && !dto.getGroupIds().isEmpty()) {
            Set<UUID> uniqueIds = new HashSet<>(dto.getGroupIds());
            List<UserGroup> groups = userService.findGroupsByIds(new ArrayList<>(uniqueIds));
            if (groups.size() != uniqueIds.size()) {
                throw DomainException.notFound(ErrorCode.GROUP_NOT_FOUND, "One or more group IDs do not exist");
            }
            groups.forEach(g -> groupIds.add(g.getId()));
        }

        InviteToken token = InviteToken.builder()
                .code(generateCode())
                .label(dto.getLabel())
                .maxUses(dto.getMaxUses())
                .prefillFirstName(dto.getPrefillFirstName())
                .prefillLastName(dto.getPrefillLastName())
                .prefillEmail(dto.getPrefillEmail())
                .groupIds(groupIds)
                .expiresAt(dto.getExpiresAt())
                .createdBy(creator)
                .build();

        return inviteTokenRepository.save(token);
    }

    @Transactional
    public AppUser redeemInvite(RegisterRequest dto) {
        InviteToken token = inviteTokenRepository.findByCodeForUpdate(dto.getCode())
                .orElseThrow(() -> DomainException.notFound(ErrorCode.INVITE_NOT_FOUND, "Invite not found: " + dto.getCode()));

        checkTokenState(token);

        if (dto.getPassword() == null || dto.getPassword().length() < MIN_PASSWORD_LENGTH) {
            throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR,
                    "Password must be at least " + MIN_PASSWORD_LENGTH + " characters");
        }

        AppUser user = userService.createUser(
                dto.getEmail(),
                dto.getPassword(),
                dto.getFirstName(),
                dto.getLastName(),
                token.getGroupIds()
        );

        userService.updateNotificationPreferences(user.getId(), dto.isNotifyOnMention(), dto.isNotifyOnMention());

        token.setUseCount(token.getUseCount() + 1);
        inviteTokenRepository.save(token);

        log.info("User {} registered via invite code {}", dto.getEmail(), dto.getCode());
        return user;
    }

    @Transactional
    public void revokeInvite(UUID id) {
        InviteToken token = inviteTokenRepository.findById(id)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.INVITE_NOT_FOUND, "Invite not found: " + id));
        token.setRevoked(true);
        inviteTokenRepository.save(token);
    }

    public List<InviteListItemDTO> listInvites(boolean activeOnly, String appBaseUrl) {
        List<InviteToken> tokens = activeOnly
                ? inviteTokenRepository.findActive()
                : inviteTokenRepository.findAllOrderedByCreatedAt();
        return tokens.stream().map(t -> toListItemDTO(t, appBaseUrl)).toList();
    }

    public InviteListItemDTO toListItemDTO(InviteToken token, String appBaseUrl) {
        String status;
        if (token.isRevoked()) status = "revoked";
        else if (token.isExpired()) status = "expired";
        else if (token.isExhausted()) status = "exhausted";
        else status = "active";

        return InviteListItemDTO.builder()
                .id(token.getId())
                .code(token.getCode())
                .displayCode(formatDisplayCode(token.getCode()))
                .label(token.getLabel())
                .useCount(token.getUseCount())
                .maxUses(token.getMaxUses())
                .expiresAt(token.getExpiresAt())
                .revoked(token.isRevoked())
                .status(status)
                .createdAt(token.getCreatedAt())
                .shareableUrl(appBaseUrl + "/register?code=" + token.getCode())
                .build();
    }

    private void checkTokenState(InviteToken token) {
        if (token.isRevoked()) {
            throw DomainException.conflict(ErrorCode.INVITE_REVOKED, "Invite has been revoked");
        }
        if (token.isExpired()) {
            throw new DomainException(ErrorCode.INVITE_EXPIRED, org.springframework.http.HttpStatus.GONE,
                    "Invite has expired");
        }
        if (token.isExhausted()) {
            throw DomainException.conflict(ErrorCode.INVITE_EXHAUSTED, "Invite use limit reached");
        }
    }

    private String buildRandomCode() {
        StringBuilder sb = new StringBuilder(CODE_LENGTH);
        for (int i = 0; i < CODE_LENGTH; i++) {
            sb.append(CODE_ALPHABET.charAt(SECURE_RANDOM.nextInt(CODE_ALPHABET.length())));
        }
        return sb.toString();
    }

    public static String formatDisplayCode(String code) {
        if (code == null || code.length() != CODE_LENGTH) return code;
        return code.substring(0, 5) + "-" + code.substring(5);
    }
}
