package de.votian.services.service;

import de.votian.services.config.ParameterKeys;
import de.votian.services.dto.AppointmentCategoryOptionDto;
import de.votian.services.dto.AppointmentGroupOptionDto;
import de.votian.services.dto.AppointmentParticipantOptionDto;
import de.votian.services.dto.AppointmentParticipantStateDto;
import de.votian.services.dto.CustomerListItemDto;
import de.votian.services.dto.GroupwareAppointmentCommandDto;
import de.votian.services.dto.GroupwareAppointmentDto;
import de.votian.services.dto.GroupwareCancellationCommandDto;
import de.votian.services.dto.GroupwareOptionsDto;
import de.votian.services.dto.GroupwareResubmissionCommandDto;
import de.votian.services.dto.UserSessionDto;
import de.votian.services.entity.AppGroup;
import de.votian.services.entity.Company;
import de.votian.services.entity.Customer;
import de.votian.services.entity.Employee;
import de.votian.services.entity.GroupwareAppointment;
import de.votian.services.entity.Headquarters;
import de.votian.services.entity.MetaType;
import de.votian.services.entity.User;
import de.votian.services.repository.AppGroupRepository;
import de.votian.services.repository.CompanyRepository;
import de.votian.services.repository.CustomerRepository;
import de.votian.services.repository.EmployeeRepository;
import de.votian.services.repository.GroupwareAppointmentRepository;
import de.votian.services.repository.HeadquartersRepository;
import de.votian.services.repository.MetaTypeRepository;
import de.votian.services.repository.UserRepository;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@Transactional
public class GroupwareService {

    private static final Logger log = LoggerFactory.getLogger(GroupwareService.class);
    private static final DateTimeFormatter MAIL_TIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
    private static final String META_CATEGORY_1 = "appointment_category_1";
    private static final String META_CATEGORY_2 = "appointment_category_2";
    private static final String META_CATEGORY_3 = "appointment_category_3";
    private static final String META_CATEGORY_4 = "appointment_category_4";

    private final GroupwareAppointmentRepository appointmentRepository;
    private final UserRepository userRepository;
    private final EmployeeRepository employeeRepository;
    private final HeadquartersRepository headquartersRepository;
    private final AppGroupRepository appGroupRepository;
    private final MetaTypeRepository metaTypeRepository;
    private final CustomerRepository customerRepository;
    private final CompanyRepository companyRepository;
    private final ParameterService parameterService;
    private final JdbcTemplate jdbcTemplate;
    private final ObjectProvider<JavaMailSender> mailSenderProvider;

    public GroupwareService(GroupwareAppointmentRepository appointmentRepository,
                            UserRepository userRepository,
                            EmployeeRepository employeeRepository,
                            HeadquartersRepository headquartersRepository,
                            AppGroupRepository appGroupRepository,
                            MetaTypeRepository metaTypeRepository,
                            CustomerRepository customerRepository,
                            CompanyRepository companyRepository,
                            ParameterService parameterService,
                            JdbcTemplate jdbcTemplate,
                            ObjectProvider<JavaMailSender> mailSenderProvider) {
        this.appointmentRepository = appointmentRepository;
        this.userRepository = userRepository;
        this.employeeRepository = employeeRepository;
        this.headquartersRepository = headquartersRepository;
        this.appGroupRepository = appGroupRepository;
        this.metaTypeRepository = metaTypeRepository;
        this.customerRepository = customerRepository;
        this.companyRepository = companyRepository;
        this.parameterService = parameterService;
        this.jdbcTemplate = jdbcTemplate;
        this.mailSenderProvider = mailSenderProvider;
    }

    @Transactional(readOnly = true)
    public GroupwareOptionsDto loadOptions(UserSessionDto sessionUser) {
        long headquartersId = requireHeadquartersId(sessionUser);
        long employeeId = sessionUser.getEmployeeId() != null ? sessionUser.getEmployeeId() : 0L;

        List<User> participantUsers = userRepository.findByTypeAndHeadquartersIdOrderByNameAscFirstnameAsc(1, headquartersId);
        List<Employee> employees = employeeRepository.findByHeadquartersIdOrderByIdAsc(headquartersId);
        Map<Long, Employee> employeesByUserId = employees.stream()
                .filter(employee -> employee.getUserId() != null)
                .collect(Collectors.toMap(Employee::getUserId, employee -> employee, (left, right) -> left, LinkedHashMap::new));

        Map<Long, Headquarters> headquartersById = mapById(headquartersRepository.findAllById(
                Objects.requireNonNull(participantUsers.stream()
                        .map(User::getHeadquartersId)
                        .filter(Objects::nonNull)
                        .collect(Collectors.toCollection(LinkedHashSet::new)))
        ));

        List<AppointmentParticipantOptionDto> participants = participantUsers.stream()
                .map(user -> toParticipantOption(user, employeesByUserId.get(user.getId()), headquartersById.get(user.getHeadquartersId())))
                .toList();

        List<AppGroup> groups = appGroupRepository.findVisibleForHeadquarters(headquartersId);
        List<AppointmentGroupOptionDto> groupOptions = groups.stream()
                .map(group -> toGroupOption(group, participants))
                .filter(group -> !group.getMemberUserIds().isEmpty())
                .toList();

        GroupwareOptionsDto dto = new GroupwareOptionsDto();
        dto.setStatusOptions(loadCategoryOptions(META_CATEGORY_1, 1, employeeId, headquartersId));
        dto.setVisibilityOptions(loadCategoryOptions(META_CATEGORY_2, 2, employeeId, headquartersId));
        dto.setTypeOptions(loadCategoryOptions(META_CATEGORY_3, 3, employeeId, headquartersId));
        dto.setReminderOptions(loadCategoryOptions(META_CATEGORY_4, 4, employeeId, headquartersId));
        dto.setParticipants(participants);
        dto.setGroups(groupOptions);
        dto.setCustomers(loadCustomers(headquartersId));
        dto.setDefaultTypeCategory(1);
        dto.setDefaultReminderCategory(resolveDefaultReminderCategory(employeeId, headquartersId));
        return dto;
    }

    @Transactional(readOnly = true)
    public List<GroupwareAppointmentDto> findAppointments(UserSessionDto sessionUser,
                                                          LocalDate fromDate,
                                                          LocalDate toDate,
                                                          Long selectedUserId,
                                                          Integer category1,
                                                          Integer category3,
                                                          Integer category4,
                                                          Long customerId) {
        long headquartersId = requireHeadquartersId(sessionUser);
        if (fromDate == null || toDate == null) {
            throw badRequest("Von- und Bis-Datum sind erforderlich.");
        }
        if (toDate.isBefore(fromDate)) {
            throw badRequest("Der Zeitraum ist ungueltig.");
        }

        LocalDateTime rangeStart = fromDate.atStartOfDay();
        LocalDateTime rangeEnd = toDate.atTime(LocalTime.MAX);

        List<GroupwareAppointment> appointments = appointmentRepository.findOverlapping(headquartersId, rangeStart, rangeEnd).stream()
                .filter(appointment -> matchesSelectedUser(appointment, selectedUserId))
                .filter(appointment -> category1 == null || Objects.equals(category1, appointment.getCategory1()))
                .filter(appointment -> category3 == null || Objects.equals(category3, appointment.getCategory3()))
                .filter(appointment -> category4 == null || Objects.equals(category4, appointment.getCategory4()))
                .filter(appointment -> customerId == null || Objects.equals(customerId, appointment.getCustomerId()))
                .toList();

        long employeeId = sessionUser.getEmployeeId() != null ? sessionUser.getEmployeeId() : 0L;
        return toAppointmentDtos(appointments, employeeId, headquartersId);
    }

    public GroupwareAppointmentDto createAppointment(UserSessionDto sessionUser, GroupwareAppointmentCommandDto command) {
        long headquartersId = requireHeadquartersId(sessionUser);
        GroupwareAppointment appointment = new GroupwareAppointment();
        appointment.setType(0);
        applyCommand(sessionUser, appointment, command, headquartersId);
        GroupwareAppointment saved = appointmentRepository.save(appointment);
        sendAppointmentMail(sessionUser, saved, command, "Neuer Termin", "Mit freundlichen Gruessen");
        return toAppointmentDto(saved, currentEmployeeId(sessionUser), headquartersId);
    }

    public GroupwareAppointmentDto updateAppointment(UserSessionDto sessionUser,
                                                     Long appointmentId,
                                                     GroupwareAppointmentCommandDto command) {
        long headquartersId = requireHeadquartersId(sessionUser);
        GroupwareAppointment appointment = requireAppointment(headquartersId, appointmentId);
        applyCommand(sessionUser, appointment, command, headquartersId);
        GroupwareAppointment saved = appointmentRepository.save(Objects.requireNonNull(appointment));
        sendAppointmentMail(sessionUser, saved, command, "Termin aktualisiert", "Mit freundlichen Gruessen");
        return toAppointmentDto(saved, currentEmployeeId(sessionUser), headquartersId);
    }

    public void cancelAppointment(UserSessionDto sessionUser,
                                  Long appointmentId,
                                  GroupwareCancellationCommandDto command) {
        long headquartersId = requireHeadquartersId(sessionUser);
        GroupwareAppointment appointment = requireAppointment(headquartersId, appointmentId);
        if (command != null && (Boolean.TRUE.equals(command.getNotifyParticipants())
                || (command.getExternalEmails() != null && !command.getExternalEmails().isEmpty()))) {
            sendCancellationMail(sessionUser, appointment, command);
        }
        appointmentRepository.delete(Objects.requireNonNull(appointment));
    }

    public GroupwareAppointmentDto confirmAppointment(UserSessionDto sessionUser, Long appointmentId) {
        long headquartersId = requireHeadquartersId(sessionUser);
        long currentUserId = requireUserId(sessionUser);
        GroupwareAppointment appointment = requireAppointment(headquartersId, appointmentId);
        List<Long> confirmedIds = parseUserIds(appointment.getConfirmed());
        if (!confirmedIds.contains(currentUserId)) {
            List<Long> updated = new ArrayList<>(confirmedIds);
            updated.add(currentUserId);
            appointment.setConfirmed(storeUserIds(updated));
            appointment.setModifyTime(LocalDateTime.now());
            appointment = appointmentRepository.save(appointment);
        }
        return toAppointmentDto(appointment, currentEmployeeId(sessionUser), headquartersId);
    }

    public GroupwareAppointmentDto finishAppointment(UserSessionDto sessionUser, Long appointmentId) {
        long headquartersId = requireHeadquartersId(sessionUser);
        GroupwareAppointment appointment = requireAppointment(headquartersId, appointmentId);
        if (requiresReportBeforeFinish(sessionUser, appointment) && !hasSameDayReport(appointment.getCustomerId())) {
            throw badRequest("Legen Sie bitte erst einen Bericht zu diesem Termin an.");
        }
        appointment.setCategory4(2);
        appointment.setModifyTime(LocalDateTime.now());
        GroupwareAppointment saved = appointmentRepository.save(appointment);
        return toAppointmentDto(saved, currentEmployeeId(sessionUser), headquartersId);
    }

    public GroupwareAppointmentDto resubmitAppointment(UserSessionDto sessionUser,
                                                       Long appointmentId,
                                                       GroupwareResubmissionCommandDto command) {
        long headquartersId = requireHeadquartersId(sessionUser);
        GroupwareAppointment source = requireAppointment(headquartersId, appointmentId);
        validateResubmission(command);

        GroupwareAppointment resubmission = new GroupwareAppointment();
        resubmission.setUserId(requireUserId(sessionUser));
        resubmission.setHeadquartersId(headquartersId);
        resubmission.setCustomerId(source.getCustomerId());
        resubmission.setCostCenterId(source.getCostCenterId());
        resubmission.setProspectId(source.getProspectId());
        resubmission.setType(source.getType());
        resubmission.setParticipants(source.getParticipants());
        resubmission.setText(source.getText());
        resubmission.setCategory1(defaultCategory(command.getCategory1(), source.getCategory1(), 1));
        resubmission.setCategory2(defaultCategory(command.getCategory2(), source.getCategory2(), 1));
        resubmission.setCategory3(defaultCategory(command.getCategory3(), source.getCategory3(), 1));
        resubmission.setCategory4(defaultCategory(command.getCategory4(), source.getCategory4(), 0));
        resubmission.setExecDate(trimSeconds(command.getStartTime()));
        resubmission.setEndDate(trimSeconds(command.getEndTime()));
        resubmission.setModifyTime(LocalDateTime.now());
        GroupwareAppointment saved = appointmentRepository.save(resubmission);
        return toAppointmentDto(saved, currentEmployeeId(sessionUser), headquartersId);
    }

    @Transactional(readOnly = true)
    public GroupwareAppointmentDto toAppointmentDto(GroupwareAppointment appointment, long employeeId, long headquartersId) {
        return toAppointmentDtos(List.of(appointment), employeeId, headquartersId).stream()
                .findFirst()
                .orElseThrow(() -> notFound("Termin nicht gefunden."));
    }

    @Transactional(readOnly = true)
    List<Long> parseUserIds(String rawValue) {
        if (rawValue == null || rawValue.isBlank()) {
            return List.of();
        }
        String normalized = rawValue.trim();
        if (normalized.startsWith(",")) {
            normalized = normalized.substring(1);
        }
        if (normalized.endsWith(",")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        if (normalized.isBlank()) {
            return List.of();
        }
        LinkedHashSet<Long> ids = new LinkedHashSet<>();
        for (String token : normalized.split(",")) {
            String trimmed = token.trim();
            if (trimmed.isEmpty()) {
                continue;
            }
            try {
                long id = Long.parseLong(trimmed);
                if (id > 0) {
                    ids.add(id);
                }
            } catch (NumberFormatException ignored) {
            }
        }
        return List.copyOf(ids);
    }

    String storeUserIds(Collection<Long> ids) {
        List<Long> normalized = normalizeUserIds(ids);
        if (normalized.isEmpty()) {
            return "";
        }
        return "," + normalized.stream().map(String::valueOf).collect(Collectors.joining(",")) + ",";
    }

    private void applyCommand(UserSessionDto sessionUser,
                              GroupwareAppointment appointment,
                              GroupwareAppointmentCommandDto command,
                              long headquartersId) {
        if (command == null) {
            throw badRequest("Termindaten fehlen.");
        }
        validateCommand(command);

        appointment.setUserId(requireUserId(sessionUser));
        appointment.setHeadquartersId(headquartersId);
        appointment.setCustomerId(resolveCustomerId(headquartersId, command.getCustomerId()));
        appointment.setCostCenterId(null);
        appointment.setProspectId(null);
        appointment.setCategory1(defaultCategory(command.getCategory1(), appointment.getCategory1(), 1));
        appointment.setCategory2(defaultCategory(command.getCategory2(), appointment.getCategory2(), 1));
        appointment.setCategory3(defaultCategory(command.getCategory3(), appointment.getCategory3(), 1));
        appointment.setCategory4(defaultCategory(command.getCategory4(), appointment.getCategory4(),
                resolveDefaultReminderCategory(currentEmployeeId(sessionUser), headquartersId)));
        appointment.setParticipants(storeUserIds(validateParticipantIds(headquartersId, command.getParticipantUserIds())));
        appointment.setText(command.getText().trim());
        appointment.setExecDate(trimSeconds(command.getStartTime()));
        appointment.setEndDate(trimSeconds(command.getEndTime()));
        appointment.setModifyTime(LocalDateTime.now());
        if (appointment.getType() == null) {
            appointment.setType(0);
        }
    }

    private void validateCommand(GroupwareAppointmentCommandDto command) {
        if (command.getText() == null || command.getText().isBlank()) {
            throw badRequest("Bitte erfassen Sie einen Beschreibungstext.");
        }
        if (command.getStartTime() == null || command.getEndTime() == null) {
            throw badRequest("Beginn und Ende sind erforderlich.");
        }
        if (!command.getStartTime().isBefore(command.getEndTime())) {
            throw badRequest("Die Zeitangaben sind nicht korrekt.");
        }
    }

    private void validateResubmission(GroupwareResubmissionCommandDto command) {
        if (command == null || command.getStartTime() == null || command.getEndTime() == null) {
            throw badRequest("Die Wiedervorlage ist unvollstaendig.");
        }
        if (!command.getStartTime().isBefore(command.getEndTime())) {
            throw badRequest("Die Zeitangaben der Wiedervorlage sind ungueltig.");
        }
    }

    private List<Long> validateParticipantIds(long headquartersId, Collection<Long> participantIds) {
        List<Long> normalizedIds = normalizeUserIds(participantIds);
        if (normalizedIds.isEmpty()) {
            return normalizedIds;
        }
        Set<Long> allowedIds = userRepository.findByTypeAndHeadquartersIdOrderByNameAscFirstnameAsc(1, headquartersId).stream()
                .map(User::getId)
                .collect(Collectors.toSet());
        for (Long userId : normalizedIds) {
            if (!allowedIds.contains(userId)) {
                throw badRequest("Mindestens ein Teilnehmer ist fuer die Niederlassung ungueltig.");
            }
        }
        return normalizedIds;
    }

    private List<Long> normalizeUserIds(Collection<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return List.of();
        }
        LinkedHashSet<Long> normalized = new LinkedHashSet<>();
        for (Long id : ids) {
            if (id != null && id > 0) {
                normalized.add(id);
            }
        }
        return List.copyOf(normalized);
    }

    private boolean matchesSelectedUser(GroupwareAppointment appointment, Long selectedUserId) {
        if (selectedUserId == null || selectedUserId <= 0) {
            return true;
        }
        return Objects.equals(selectedUserId, appointment.getUserId())
                || parseUserIds(appointment.getParticipants()).contains(selectedUserId);
    }

    private Integer defaultCategory(Integer requestedValue, Integer fallbackValue, Integer hardDefaultValue) {
        if (requestedValue != null) {
            return requestedValue;
        }
        if (fallbackValue != null) {
            return fallbackValue;
        }
        return hardDefaultValue;
    }

    private LocalDateTime trimSeconds(LocalDateTime value) {
        return value != null ? value.withSecond(0).withNano(0) : null;
    }

    private boolean requiresReportBeforeFinish(UserSessionDto sessionUser, GroupwareAppointment appointment) {
        long employeeId = currentEmployeeId(sessionUser);
        long headquartersId = requireHeadquartersId(sessionUser);
        String enabled = parameterService.getParameterValueFullFallback(
                employeeId,
                ParameterKeys.MASK_AP_WARNING_DISABLE_QUESTION_NO_REPORT_OF_SAME_DAY,
                headquartersId
        );
        return "1".equals(enabled)
                && appointment.getCustomerId() != null
                && appointment.getCustomerId() > 0
                && Integer.valueOf(1).equals(appointment.getCategory1());
    }

    private boolean hasSameDayReport(Long customerId) {
        if (customerId == null || customerId <= 0) {
            return false;
        }
        LocalDate today = LocalDate.now();
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM phoenix_group.report_process " +
                        "WHERE rp_objtype = 'cs' AND rp_objid = ? AND rp_createtime >= ? AND rp_createtime <= ?",
                Integer.class,
                customerId,
                today.atStartOfDay(),
                today.atTime(LocalTime.MAX)
        );
        return count != null && count > 0;
    }

    private void sendAppointmentMail(UserSessionDto sessionUser,
                                     GroupwareAppointment appointment,
                                     GroupwareAppointmentCommandDto command,
                                     String subjectPrefix,
                                     String defaultGreeting) {
        boolean notifyParticipants = command != null && Boolean.TRUE.equals(command.getNotifyParticipants());
        List<String> recipients = resolveRecipients(appointment, command != null ? command.getExternalEmails() : List.of(), notifyParticipants);
        sendMail(
                sessionUser,
                recipients,
                subjectPrefix + ": " + formatDateTime(appointment.getExecDate()),
                buildMailBody(
                        appointment,
                        command != null ? command.getMailSalutation() : null,
                        command != null ? command.getMailGreetings() : null,
                        defaultGreeting
                )
        );
    }

    private void sendCancellationMail(UserSessionDto sessionUser,
                                      GroupwareAppointment appointment,
                                      GroupwareCancellationCommandDto command) {
        boolean notifyParticipants = command != null && Boolean.TRUE.equals(command.getNotifyParticipants());
        List<String> recipients = resolveRecipients(appointment, command != null ? command.getExternalEmails() : List.of(), notifyParticipants);
        sendMail(
                sessionUser,
                recipients,
                "Termin abgesagt: " + formatDateTime(appointment.getExecDate()),
                buildMailBody(appointment, "Sehr geehrte Damen und Herren,", "Mit freundlichen Gruessen", "Mit freundlichen Gruessen")
        );
    }

    private List<String> resolveRecipients(GroupwareAppointment appointment,
                                           Collection<String> externalEmails,
                                           boolean includeParticipants) {
        LinkedHashSet<String> recipients = new LinkedHashSet<>();
        if (includeParticipants) {
            for (Long participantId : parseUserIds(appointment.getParticipants())) {
                userRepository.findById(Objects.requireNonNull(participantId))
                        .map(User::getEmail)
                        .filter(this::isValidEmail)
                        .ifPresent(email -> recipients.add(email.toLowerCase(Locale.ROOT)));
            }
        }
        if (externalEmails != null) {
            for (String externalEmail : externalEmails) {
                if (isValidEmail(externalEmail)) {
                    recipients.add(externalEmail.trim().toLowerCase(Locale.ROOT));
                }
            }
        }
        return List.copyOf(recipients);
    }

    private void sendMail(UserSessionDto sessionUser,
                          List<String> recipients,
                          String subject,
                          String body) {
        if (recipients == null || recipients.isEmpty()) {
            return;
        }
        JavaMailSender mailSender = mailSenderProvider.getIfAvailable();
        if (mailSender == null) {
            log.info("Groupware mail skipped because no JavaMailSender is configured.");
            return;
        }

        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(Objects.requireNonNull(recipients.toArray(String[]::new)));
        message.setSubject(subject);
        message.setText(body);

        String senderEmail = userRepository.findById(requireUserId(sessionUser))
                .map(User::getEmail)
                .filter(this::isValidEmail)
                .orElse(null);
        if (senderEmail != null) {
            message.setFrom(senderEmail);
            message.setReplyTo(senderEmail);
        }

        try {
            mailSender.send(message);
        } catch (MailException ex) {
            log.warn("Groupware mail delivery failed: {}", ex.getMessage());
        }
    }

    private String buildMailBody(GroupwareAppointment appointment,
                                 String salutation,
                                 String greeting,
                                 String defaultGreeting) {
        StringBuilder text = new StringBuilder();
        text.append(nonBlankOrDefault(salutation, "Sehr geehrte Damen und Herren,")).append("\n\n");
        if (appointment.getText() != null && !appointment.getText().isBlank()) {
            text.append(appointment.getText().trim()).append("\n\n");
        }
        text.append("Beginn: ").append(formatDateTime(appointment.getExecDate())).append('\n');
        text.append("Ende: ").append(formatDateTime(appointment.getEndDate())).append("\n\n");
        text.append(nonBlankOrDefault(greeting, defaultGreeting));
        return text.toString();
    }

    private String formatDateTime(LocalDateTime value) {
        return value != null ? MAIL_TIME_FORMAT.format(value) : "-";
    }

    private String nonBlankOrDefault(String value, String defaultValue) {
        return value != null && !value.isBlank() ? value.trim() : defaultValue;
    }

    private boolean isValidEmail(String value) {
        if (value == null || value.isBlank()) {
            return false;
        }
        try {
            InternetAddress address = new InternetAddress(value.trim(), true);
            address.validate();
            return true;
        } catch (AddressException ex) {
            return false;
        }
    }

    private List<GroupwareAppointmentDto> toAppointmentDtos(List<GroupwareAppointment> appointments,
                                                            long employeeId,
                                                            long headquartersId) {
        if (appointments.isEmpty()) {
            return List.of();
        }

        Set<Long> authorIds = appointments.stream()
                .map(GroupwareAppointment::getUserId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Set<Long> participantIds = appointments.stream()
                .flatMap(appointment -> parseUserIds(appointment.getParticipants()).stream())
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Set<Long> confirmedIds = appointments.stream()
                .flatMap(appointment -> parseUserIds(appointment.getConfirmed()).stream())
                .collect(Collectors.toCollection(LinkedHashSet::new));

        Set<Long> customerIds = appointments.stream()
                .map(GroupwareAppointment::getCustomerId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        LinkedHashSet<Long> allUserIds = new LinkedHashSet<>();
        allUserIds.addAll(authorIds);
        allUserIds.addAll(participantIds);
        allUserIds.addAll(confirmedIds);

        Map<Long, User> usersById = mapById(userRepository.findAllById(Objects.requireNonNull(allUserIds)));
        Map<Long, Headquarters> headquartersById = mapById(headquartersRepository.findAllById(
                Objects.requireNonNull(usersById.values().stream()
                        .map(User::getHeadquartersId)
                        .filter(Objects::nonNull)
                        .collect(Collectors.toCollection(LinkedHashSet::new)))
        ));
        Map<Long, Customer> customersById = mapById(customerRepository.findAllById(Objects.requireNonNull(customerIds)));
        Map<Long, Company> companiesById = mapById(companyRepository.findAllById(
                Objects.requireNonNull(customersById.values().stream()
                        .map(Customer::getCompanyId)
                        .filter(Objects::nonNull)
                        .collect(Collectors.toCollection(LinkedHashSet::new)))
        ));

        Map<Integer, String> category1Labels = loadCategoryLabelMap(META_CATEGORY_1, 1, employeeId, headquartersId);
        Map<Integer, String> category2Labels = loadCategoryLabelMap(META_CATEGORY_2, 2, employeeId, headquartersId);
        Map<Integer, String> category3Labels = loadCategoryLabelMap(META_CATEGORY_3, 3, employeeId, headquartersId);
        Map<Integer, String> category4Labels = loadCategoryLabelMap(META_CATEGORY_4, 4, employeeId, headquartersId);

        return appointments.stream()
                .map(appointment -> toAppointmentDto(
                        appointment,
                        usersById,
                        headquartersById,
                        customersById,
                        companiesById,
                        category1Labels,
                        category2Labels,
                        category3Labels,
                        category4Labels
                ))
                .sorted(Comparator.comparing(GroupwareAppointmentDto::getStartTime, Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(GroupwareAppointmentDto::getId, Comparator.nullsLast(Comparator.naturalOrder())))
                .toList();
    }

    private GroupwareAppointmentDto toAppointmentDto(GroupwareAppointment appointment,
                                                     Map<Long, User> usersById,
                                                     Map<Long, Headquarters> headquartersById,
                                                     Map<Long, Customer> customersById,
                                                     Map<Long, Company> companiesById,
                                                     Map<Integer, String> category1Labels,
                                                     Map<Integer, String> category2Labels,
                                                     Map<Integer, String> category3Labels,
                                                     Map<Integer, String> category4Labels) {
        GroupwareAppointmentDto dto = new GroupwareAppointmentDto();
        dto.setId(appointment.getId());
        dto.setHeadquartersId(appointment.getHeadquartersId());
        dto.setAuthorUserId(appointment.getUserId());
        dto.setAuthorName(formatUser(usersById.get(appointment.getUserId()), headquartersById));
        dto.setCustomerId(appointment.getCustomerId());
        dto.setCategory1(appointment.getCategory1());
        dto.setCategory2(appointment.getCategory2());
        dto.setCategory3(appointment.getCategory3());
        dto.setCategory4(appointment.getCategory4());
        dto.setCategory1Label(category1Labels.get(appointment.getCategory1()));
        dto.setCategory2Label(category2Labels.get(appointment.getCategory2()));
        dto.setCategory3Label(category3Labels.get(appointment.getCategory3()));
        dto.setCategory4Label(category4Labels.get(appointment.getCategory4()));
        dto.setText(appointment.getText());
        dto.setStartTime(appointment.getExecDate());
        dto.setEndTime(appointment.getEndDate());
        dto.setReminderActive(appointment.getCategory4() != null
                && appointment.getCategory4() > 0
                && !Integer.valueOf(2).equals(appointment.getCategory4()));
        dto.setFinished(Integer.valueOf(2).equals(appointment.getCategory4()));

        Customer customer = customersById.get(appointment.getCustomerId());
        if (customer != null) {
            dto.setCustomerEid(customer.getEid());
            Company company = companiesById.get(customer.getCompanyId());
            dto.setCustomerName(company != null ? company.getComp() : customer.getEid());
        }

        List<Long> participantUserIds = parseUserIds(appointment.getParticipants());
        List<Long> confirmedUserIds = parseUserIds(appointment.getConfirmed());
        dto.setParticipantUserIds(participantUserIds);
        dto.setParticipants(participantUserIds.stream()
                .map(userId -> {
                    AppointmentParticipantStateDto state = new AppointmentParticipantStateDto();
                    state.setUserId(userId);
                    state.setName(formatUser(usersById.get(userId), headquartersById));
                    state.setEmail(usersById.containsKey(userId) ? usersById.get(userId).getEmail() : null);
                    state.setConfirmed(confirmedUserIds.contains(userId));
                    return state;
                })
                .toList());
        return dto;
    }

    private String formatUser(User user, Map<Long, Headquarters> headquartersById) {
        if (user == null) {
            return "-";
        }
        Headquarters headquarters = headquartersById.get(user.getHeadquartersId());
        String base = (user.getName() != null ? user.getName() : "")
                + (user.getFirstname() != null && !user.getFirstname().isBlank()
                ? ", " + user.getFirstname()
                : "");
        if (headquarters != null && headquarters.getMnemonic() != null && !headquarters.getMnemonic().isBlank()) {
            return base + " [" + headquarters.getMnemonic() + "]";
        }
        return base;
    }

    private AppointmentParticipantOptionDto toParticipantOption(User user, Employee employee, Headquarters headquarters) {
        AppointmentParticipantOptionDto dto = new AppointmentParticipantOptionDto();
        dto.setUserId(user.getId());
        dto.setEmployeeId(employee != null ? employee.getId() : null);
        LinkedHashMap<Long, Headquarters> headquartersById = new LinkedHashMap<>();
        if (headquarters != null && headquarters.getId() != null) {
            headquartersById.put(headquarters.getId(), headquarters);
        }
        dto.setName(formatUser(user, headquartersById));
        dto.setEmail(user.getEmail());
        dto.setHeadquartersMnemonic(headquarters != null ? headquarters.getMnemonic() : null);
        dto.setGroupIds(employee != null ? parseUserIds(employee.getGroup()) : List.of());
        return dto;
    }

    private AppointmentGroupOptionDto toGroupOption(AppGroup group,
                                                    List<AppointmentParticipantOptionDto> participants) {
        AppointmentGroupOptionDto dto = new AppointmentGroupOptionDto();
        dto.setId(group.getId());
        dto.setName(group.getName());
        dto.setMemberUserIds(participants.stream()
                .filter(participant -> participant.getGroupIds().contains(group.getId()))
                .map(AppointmentParticipantOptionDto::getUserId)
                .filter(Objects::nonNull)
                .toList());
        return dto;
    }

    private List<AppointmentCategoryOptionDto> loadCategoryOptions(String type,
                                                                   int categoryIndex,
                                                                   long employeeId,
                                                                   long headquartersId) {
        return metaTypeRepository.findByTypeOrderBySort(type).stream()
                .map(metaType -> new AppointmentCategoryOptionDto(
                        metaType.getSort(),
                        resolveCategoryLabel(metaType, categoryIndex, employeeId, headquartersId)
                ))
                .toList();
    }

    private Map<Integer, String> loadCategoryLabelMap(String type,
                                                      int categoryIndex,
                                                      long employeeId,
                                                      long headquartersId) {
        LinkedHashMap<Integer, String> labels = new LinkedHashMap<>();
        for (MetaType metaType : metaTypeRepository.findByTypeOrderBySort(type)) {
            labels.put(metaType.getSort(), resolveCategoryLabel(metaType, categoryIndex, employeeId, headquartersId));
        }
        return labels;
    }

    private String resolveCategoryLabel(MetaType metaType,
                                        int categoryIndex,
                                        long employeeId,
                                        long headquartersId) {
        String override = parameterService.getParameterValueFullFallback(
                employeeId,
                "MASK_AP_CAT_" + categoryIndex + "_" + metaType.getSort(),
                headquartersId
        );
        if (override != null && !override.isBlank()) {
            return override;
        }
        return metaType.getValue();
    }

    private List<CustomerListItemDto> loadCustomers(long headquartersId) {
        List<Customer> customers = customerRepository.findByHeadquartersId(headquartersId);
        Map<Long, Company> companiesById = mapById(companyRepository.findAllById(
                Objects.requireNonNull(customers.stream()
                        .map(Customer::getCompanyId)
                        .filter(Objects::nonNull)
                        .collect(Collectors.toCollection(LinkedHashSet::new)))
        ));

        return customers.stream()
                .map(customer -> {
                    Company company = companiesById.get(customer.getCompanyId());
                    CustomerListItemDto dto = new CustomerListItemDto();
                    dto.setId(customer.getId());
                    dto.setEid(customer.getEid());
                    dto.setCompanyName(company != null ? company.getComp() : customer.getEid());
                    return dto;
                })
                .sorted(Comparator.comparing(CustomerListItemDto::getCompanyName, Comparator.nullsLast(String::compareToIgnoreCase))
                        .thenComparing(CustomerListItemDto::getEid, Comparator.nullsLast(String::compareToIgnoreCase)))
                .toList();
    }

    private Long resolveCustomerId(long headquartersId, Long customerId) {
        if (customerId == null || customerId <= 0) {
            return null;
        }
        Customer customer = customerRepository.findById(customerId)
                .orElseThrow(() -> badRequest("Der Kunde existiert nicht."));
        if (!Objects.equals(customer.getHeadquartersId(), headquartersId)) {
            throw forbidden("Der Kunde gehoert nicht zur aktuellen Niederlassung.");
        }
        return customer.getId();
    }

    private GroupwareAppointment requireAppointment(long headquartersId, Long appointmentId) {
        if (appointmentId == null || appointmentId <= 0) {
            throw badRequest("Termin-ID fehlt.");
        }
        return appointmentRepository.findByIdAndHeadquartersId(appointmentId, headquartersId)
                .orElseThrow(() -> notFound("Termin nicht gefunden."));
    }

    private long requireHeadquartersId(UserSessionDto sessionUser) {
        if (sessionUser == null || sessionUser.getHeadquartersId() == null || sessionUser.getHeadquartersId() <= 0) {
            throw forbidden("Keine gueltige Niederlassung im Kontext.");
        }
        return sessionUser.getHeadquartersId();
    }

    private long requireUserId(UserSessionDto sessionUser) {
        if (sessionUser == null || sessionUser.getUserId() == null || sessionUser.getUserId() <= 0) {
            throw forbidden("Kein gueltiger Benutzer im Kontext.");
        }
        return sessionUser.getUserId();
    }

    private long currentEmployeeId(UserSessionDto sessionUser) {
        return sessionUser != null && sessionUser.getEmployeeId() != null ? sessionUser.getEmployeeId() : 0L;
    }

    private Integer resolveDefaultReminderCategory(long employeeId, long headquartersId) {
        String preset = parameterService.getParameterValueFullFallback(
                employeeId,
                ParameterKeys.MASK_AP_CATEGORY_4_PRESET,
                headquartersId
        );
        return "1".equals(preset) ? 1 : 0;
    }

    private <T> Map<Long, T> mapById(Iterable<T> values) {
        LinkedHashMap<Long, T> map = new LinkedHashMap<>();
        for (T value : values) {
            if (value instanceof User user && user.getId() != null) {
                map.put(user.getId(), value);
            } else if (value instanceof Customer customer && customer.getId() != null) {
                map.put(customer.getId(), value);
            } else if (value instanceof Company company && company.getId() != null) {
                map.put(company.getId(), value);
            } else if (value instanceof Headquarters headquarters && headquarters.getId() != null) {
                map.put(headquarters.getId(), value);
            } else if (value instanceof Employee employee && employee.getId() != null) {
                map.put(employee.getId(), value);
            }
        }
        return map;
    }

    private ResponseStatusException forbidden(String message) {
        return new ResponseStatusException(HttpStatus.FORBIDDEN, message);
    }

    private ResponseStatusException badRequest(String message) {
        return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
    }

    private ResponseStatusException notFound(String message) {
        return new ResponseStatusException(HttpStatus.NOT_FOUND, message);
    }
}
