package org.raddatz.familienarchiv.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.user.AdminUpdateUserRequest;
import org.raddatz.familienarchiv.user.ChangePasswordDTO;
import org.raddatz.familienarchiv.user.CreateUserRequest;
import org.raddatz.familienarchiv.user.UpdateProfileDTO;
import org.raddatz.familienarchiv.user.GroupDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.UserGroup;
import org.raddatz.familienarchiv.user.AppUserRepository;
import org.raddatz.familienarchiv.user.UserGroupRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import static java.util.stream.Collectors.toSet;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

    private final AppUserRepository userRepository;
    private final UserGroupRepository groupRepository;
    // Injected directly (not via InviteService) to avoid a constructor injection cycle:
    // InviteService → UserService → InviteService. Spring Framework 7 forbids such cycles.
    private final InviteTokenRepository inviteTokenRepository;
    private final PasswordEncoder passwordEncoder;
    private final AuditService auditService;

    @Transactional
    public AppUser createUserOrUpdate(UUID actorId, CreateUserRequest request) {
        log.info("Creating or updating user: {}", request.getEmail());

        Set<UserGroup> groups = new HashSet<>();
        if (request.getGroupIds() != null && !request.getGroupIds().isEmpty()) {
            groups.addAll(groupRepository.findAllById(request.getGroupIds()));
        }

        Optional<AppUser> existingUser = userRepository.findByEmail(request.getEmail());
        AppUser user;
        boolean isNew;

        if (existingUser.isPresent()) {
            log.info("User exists, updating: {}", request.getEmail());
            user = existingUser.get().updateFromRequest(request, passwordEncoder, groups);
            isNew = false;
        } else {
            log.info("Creating new user: {}", request.getEmail());
            user = AppUser.builder()
                    .email(request.getEmail())
                    .password(passwordEncoder.encode(request.getInitialPassword()))
                    .groups(groups)
                    .firstName(request.getFirstName())
                    .lastName(request.getLastName())
                    .birthDate(request.getBirthDate())
                    .contact(request.getContact())
                    .enabled(true)
                    .build();
            isNew = true;
        }

        AppUser saved = userRepository.save(user);
        if (isNew) {
            auditService.logAfterCommit(AuditKind.USER_CREATED, actorId, null,
                    Map.of("userId", saved.getId().toString(), "email", saved.getEmail()));
        }
        return saved;
    }

    @Transactional
    public AppUser createUserForBootstrap(CreateUserRequest request) {
        log.info("Bootstrap user creation (no audit): {}", request.getEmail());

        Set<UserGroup> groups = new HashSet<>();
        if (request.getGroupIds() != null && !request.getGroupIds().isEmpty()) {
            groups.addAll(groupRepository.findAllById(request.getGroupIds()));
        }

        Optional<AppUser> existingUser = userRepository.findByEmail(request.getEmail());
        if (existingUser.isPresent()) {
            AppUser updated = existingUser.get().updateFromRequest(request, passwordEncoder, groups);
            return userRepository.save(updated);
        }

        AppUser user = AppUser.builder()
                .email(request.getEmail())
                .password(passwordEncoder.encode(request.getInitialPassword()))
                .groups(groups)
                .firstName(request.getFirstName())
                .lastName(request.getLastName())
                .birthDate(request.getBirthDate())
                .contact(request.getContact())
                .enabled(true)
                .build();
        return userRepository.save(user);
    }

    @Transactional
    public AppUser createUser(String email, String rawPassword, String firstName, String lastName, Set<UUID> groupIds) {
        userRepository.findByEmail(email).ifPresent(existing -> {
            throw DomainException.conflict(ErrorCode.EMAIL_ALREADY_IN_USE, "Email already registered: " + email);
        });

        Set<UserGroup> groups = new HashSet<>();
        if (groupIds != null && !groupIds.isEmpty()) {
            groups.addAll(groupRepository.findAllById(groupIds));
        }

        AppUser user = AppUser.builder()
                .email(email)
                .password(passwordEncoder.encode(rawPassword))
                .firstName(firstName)
                .lastName(lastName)
                .groups(groups)
                .enabled(true)
                .build();

        return userRepository.save(user);
    }

    public List<UserGroup> findGroupsByIds(Collection<UUID> ids) {
        return groupRepository.findAllById(ids);
    }

    @Transactional
    public void deleteUser(UUID actorId, UUID userId) {
        AppUser user = userRepository.findById(userId)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, "No user found for id: " + userId));
        String email = user.getEmail();
        userRepository.delete(user);
        auditService.logAfterCommit(AuditKind.USER_DELETED, actorId, null,
                Map.of("userId", userId.toString(), "email", email));
    }

    public AppUser getById(UUID id) {
        return userRepository.findById(id)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, "No user found for id: " + id));
    }

    public List<AppUser> findAllById(Collection<UUID> ids) {
        return userRepository.findAllById(ids);
    }

    @Transactional
    public AppUser updateNotificationPreferences(UUID userId, boolean notifyOnReply, boolean notifyOnMention) {
        AppUser user = getById(userId);
        user.setNotifyOnReply(notifyOnReply);
        user.setNotifyOnMention(notifyOnMention);
        return userRepository.save(user);
    }

    @Transactional
    public AppUser updateProfile(UUID userId, UpdateProfileDTO dto) {
        AppUser user = getById(userId);

        if (dto.getEmail() != null && !dto.getEmail().isBlank()) {
            userRepository.findByEmail(dto.getEmail()).ifPresent(existing -> {
                if (!existing.getId().equals(userId)) {
                    throw DomainException.conflict(ErrorCode.EMAIL_ALREADY_IN_USE,
                            "E-Mail wird bereits von einem anderen Konto verwendet");
                }
            });
            user.setEmail(dto.getEmail().trim());
        } else if (dto.getEmail() != null) {
            throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR, "Email must not be blank");
        }

        user.setFirstName(dto.getFirstName());
        user.setLastName(dto.getLastName());
        user.setBirthDate(dto.getBirthDate());
        user.setContact(dto.getContact() == null || dto.getContact().isBlank() ? null : dto.getContact().trim());
        return userRepository.save(user);
    }

    @Transactional
    public AppUser adminUpdateUser(UUID actorId, UUID id, AdminUpdateUserRequest dto) {
        AppUser user = getById(id);

        if (dto.getEmail() != null && !dto.getEmail().isBlank()) {
            userRepository.findByEmail(dto.getEmail()).ifPresent(existing -> {
                if (!existing.getId().equals(id)) {
                    throw DomainException.conflict(ErrorCode.EMAIL_ALREADY_IN_USE,
                            "E-Mail wird bereits von einem anderen Konto verwendet");
                }
            });
            user.setEmail(dto.getEmail().trim());
        } else if (dto.getEmail() != null) {
            throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR, "Email must not be blank");
        }

        user.setFirstName(dto.getFirstName());
        user.setLastName(dto.getLastName());
        user.setBirthDate(dto.getBirthDate());
        user.setContact(dto.getContact() == null || dto.getContact().isBlank() ? null : dto.getContact().trim());

        if (dto.getNewPassword() != null && !dto.getNewPassword().isBlank()) {
            user.setPassword(passwordEncoder.encode(dto.getNewPassword()));
        }

        if (dto.getGroupIds() != null) {
            Set<UserGroup> before = new HashSet<>(user.getGroups());
            Set<UserGroup> after = new HashSet<>(groupRepository.findAllById(dto.getGroupIds()));
            user.setGroups(after);
            groupChangePayload(before, after, id, user.getEmail())
                    .ifPresent(payload -> auditService.logAfterCommit(AuditKind.GROUP_MEMBERSHIP_CHANGED, actorId, null, payload));
        }

        return userRepository.save(user);
    }

    private Optional<Map<String, Object>> groupChangePayload(
            Set<UserGroup> before, Set<UserGroup> after, UUID userId, String email) {
        Set<UUID> beforeIds = before.stream().map(UserGroup::getId).collect(toSet());
        Set<UUID> afterIds = after.stream().map(UserGroup::getId).collect(toSet());
        if (beforeIds.equals(afterIds)) return Optional.empty();
        List<String> added = after.stream().filter(g -> !beforeIds.contains(g.getId())).map(UserGroup::getName).toList();
        List<String> removed = before.stream().filter(g -> !afterIds.contains(g.getId())).map(UserGroup::getName).toList();
        return Optional.of(Map.of("userId", userId.toString(), "email", email,
                "addedGroups", added, "removedGroups", removed));
    }

    @Transactional
    public void changePassword(UUID userId, ChangePasswordDTO dto) {
        AppUser user = getById(userId);
        if (!passwordEncoder.matches(dto.getCurrentPassword(), user.getPassword())) {
            throw DomainException.badRequest(ErrorCode.WRONG_CURRENT_PASSWORD,
                    "Das aktuelle Passwort ist falsch");
        }
        user.setPassword(passwordEncoder.encode(dto.getNewPassword()));
        userRepository.save(user);
    }

    public AppUser findByEmail(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, "No user found for email: " + email));
    }

    public Optional<AppUser> findByEmailOptional(String email) {
        return userRepository.findByEmail(email);
    }

    public AppUser save(AppUser user) {
        return userRepository.save(user);
    }

    public List<AppUser> getAllUsers() {
        return userRepository.findAll();
    }

    public List<UserGroup> getAllGroups() {
        return groupRepository.findAll();
    }

    public UserGroup getGroupById(UUID id) {
        return groupRepository.findById(id)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.INTERNAL_ERROR, "Group not found: " + id));
    }

    @Transactional
    public UserGroup createGroup(GroupDTO dto) {
        UserGroup group = UserGroup.builder()
                .name(dto.getName())
                .permissions(dto.getPermissions() != null ? dto.getPermissions() : new HashSet<>())
                .build();
        return groupRepository.save(group);
    }

    @Transactional
    public UserGroup updateGroup(UUID id, GroupDTO dto) {
        UserGroup group = getGroupById(id);
        if (dto.getName() != null) group.setName(dto.getName());
        if (dto.getPermissions() != null) group.setPermissions(dto.getPermissions());
        return groupRepository.save(group);
    }

    @Transactional
    public void deleteGroup(UUID id) {
        if (inviteTokenRepository.existsActiveWithGroupId(id)) {
            throw DomainException.conflict(ErrorCode.GROUP_HAS_ACTIVE_INVITES,
                    "Cannot delete group " + id + " — referenced by one or more active invites");
        }
        groupRepository.deleteById(id);
    }
}
