package de.votian.services.service;

import de.votian.services.dto.AcceptanceProtocolDto;
import de.votian.services.dto.AcceptanceProtocolQuestionDto;
import de.votian.services.dto.CatalogOptionDto;
import de.votian.services.dto.CustomerAcceptanceProtocolConfigDto;
import de.votian.services.dto.CustomerDeliveryReasonConfigDto;
import de.votian.services.dto.CustomerServiceConfigDto;
import de.votian.services.dto.DeliveryReasonItemDto;
import de.votian.services.dto.DeliveryReasonListDto;
import de.votian.services.dto.ServiceRadiusDto;
import de.votian.services.dto.ServiceZoneDto;
import de.votian.services.dto.ServiceZoneMappingDto;
import de.votian.services.entity.Customer;
import de.votian.services.entity.CustomerServiceAssignment;
import de.votian.services.entity.CustomerServiceTypeAssignment;
import de.votian.services.entity.GenericDataContainer;
import de.votian.services.entity.MetaType;
import de.votian.services.entity.ServiceEntity;
import de.votian.services.entity.ServiceRadius;
import de.votian.services.entity.ServiceType;
import de.votian.services.entity.ServiceZipCode;
import de.votian.services.entity.ServiceZone;
import de.votian.services.entity.ServiceZoneMapping;
import de.votian.services.entity.ServiceZoneMappingId;
import de.votian.services.repository.CustomerRepository;
import de.votian.services.repository.CustomerServiceAssignmentRepository;
import de.votian.services.repository.CustomerServiceTypeAssignmentRepository;
import de.votian.services.repository.GenericDataContainerRepository;
import de.votian.services.repository.MetaTypeRepository;
import de.votian.services.repository.ServiceEntityRepository;
import de.votian.services.repository.ServiceRadiusRepository;
import de.votian.services.repository.ServiceTypeRepository;
import de.votian.services.repository.ServiceZipCodeRepository;
import de.votian.services.repository.ServiceZoneMappingRepository;
import de.votian.services.repository.ServiceZoneRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

@Service
@Transactional
public class CustomerServiceConfigService {

    private static final String CUSTOMER_OBJECT_TYPE = "cs";
    private static final String DELIVERY_REASONS_FIELD = "service_delivery_reasons";
    private static final String DELIVERY_REASONS_TEXT_FIELD = "service_delivery_reasons_text";
    private static final String ACCEPTANCE_PROTOCOL_QUESTIONS_FIELD = "service_acceptance_protocol_questions";
    private static final String ACCEPTANCE_PROTOCOL_MAIL_STATE_FIELD = "service_acceptance_protocol_mail_state_question";
    private static final String ACCEPTANCE_PROTOCOL_TEXT_FIELD = "service_acceptance_protocol_text";
    private static final String DELIVERY_REASON_SEPARATOR = "\u00A7";
    private static final int DELIVERY_REASON_LIST_COUNT = 4;

    private final CustomerRepository customerRepository;
    private final ServiceEntityRepository serviceEntityRepository;
    private final ServiceTypeRepository serviceTypeRepository;
    private final CustomerServiceAssignmentRepository customerServiceAssignmentRepository;
    private final CustomerServiceTypeAssignmentRepository customerServiceTypeAssignmentRepository;
    private final ServiceZoneRepository serviceZoneRepository;
    private final ServiceZoneMappingRepository serviceZoneMappingRepository;
    private final ServiceRadiusRepository serviceRadiusRepository;
    private final ServiceZipCodeRepository serviceZipCodeRepository;
    private final GenericDataContainerRepository genericDataContainerRepository;
    private final MetaTypeRepository metaTypeRepository;

    public CustomerServiceConfigService(CustomerRepository customerRepository,
                                        ServiceEntityRepository serviceEntityRepository,
                                        ServiceTypeRepository serviceTypeRepository,
                                        CustomerServiceAssignmentRepository customerServiceAssignmentRepository,
                                        CustomerServiceTypeAssignmentRepository customerServiceTypeAssignmentRepository,
                                        ServiceZoneRepository serviceZoneRepository,
                                        ServiceZoneMappingRepository serviceZoneMappingRepository,
                                        ServiceRadiusRepository serviceRadiusRepository,
                                        ServiceZipCodeRepository serviceZipCodeRepository,
                                        GenericDataContainerRepository genericDataContainerRepository,
                                        MetaTypeRepository metaTypeRepository) {
        this.customerRepository = customerRepository;
        this.serviceEntityRepository = serviceEntityRepository;
        this.serviceTypeRepository = serviceTypeRepository;
        this.customerServiceAssignmentRepository = customerServiceAssignmentRepository;
        this.customerServiceTypeAssignmentRepository = customerServiceTypeAssignmentRepository;
        this.serviceZoneRepository = serviceZoneRepository;
        this.serviceZoneMappingRepository = serviceZoneMappingRepository;
        this.serviceRadiusRepository = serviceRadiusRepository;
        this.serviceZipCodeRepository = serviceZipCodeRepository;
        this.genericDataContainerRepository = genericDataContainerRepository;
        this.metaTypeRepository = metaTypeRepository;
    }

    @Transactional(readOnly = true)
    public CustomerServiceConfigDto getConfiguration(Long customerId) {
        Customer customer = requireCustomer(customerId);

        CustomerServiceConfigDto dto = new CustomerServiceConfigDto();
        dto.setCustomerId(customer.getId());
        dto.setHeadquartersId(customer.getHeadquartersId());
        dto.setAvailableServices(serviceEntityRepository.findAvailableForCustomer(customer.getHeadquartersId(), customerId).stream()
                .map(this::toCatalogOption)
                .toList());
        dto.setSelectedServiceIds(customerServiceAssignmentRepository.findByCustomerIdOrderBySort(customerId).stream()
                .map(CustomerServiceAssignment::getServiceId)
                .toList());
        dto.setAvailableServiceTypes(serviceTypeRepository.findAvailableForCustomer(customer.getHeadquartersId(), customerId).stream()
                .map(this::toCatalogOption)
                .toList());
        dto.setSelectedServiceTypeIds(customerServiceTypeAssignmentRepository.findByCustomerIdOrderBySort(customerId).stream()
                .map(CustomerServiceTypeAssignment::getServiceTypeId)
                .toList());
        return dto;
    }

    public void saveConfiguration(Long customerId, List<Long> serviceIds, List<Long> serviceTypeIds) {
        requireCustomer(customerId);

        customerServiceAssignmentRepository.deleteByCustomerId(customerId);
        List<Long> normalizedServiceIds = distinctIds(serviceIds);
        for (int i = 0; i < normalizedServiceIds.size(); i++) {
            customerServiceAssignmentRepository.save(new CustomerServiceAssignment(customerId, normalizedServiceIds.get(i), i));
        }

        customerServiceTypeAssignmentRepository.deleteByCustomerId(customerId);
        List<Long> normalizedServiceTypeIds = distinctIds(serviceTypeIds);
        for (int i = 0; i < normalizedServiceTypeIds.size(); i++) {
            customerServiceTypeAssignmentRepository.save(new CustomerServiceTypeAssignment(customerId, normalizedServiceTypeIds.get(i), i));
        }
    }

    @Transactional(readOnly = true)
    public List<ServiceZoneDto> getZones(Long customerId) {
        Customer customer = requireCustomer(customerId);
        return serviceZoneRepository.findByHeadquartersIdAndCustomerIdOrderByZoneNo(customer.getHeadquartersId(), customerId).stream()
                .map(this::toZoneDto)
                .toList();
    }

    public ServiceZoneDto createZone(Long customerId, String name, BigDecimal price, BigDecimal price1, BigDecimal price2, BigDecimal price3) {
        Customer customer = requireCustomer(customerId);
        requireNonBlank(name, "Zonenname fehlt");

        Integer maxZoneNo = serviceZoneRepository.findMaxZoneNo(customer.getHeadquartersId(), customerId);
        ServiceZone zone = new ServiceZone();
        zone.setHeadquartersId(customer.getHeadquartersId());
        zone.setCustomerId(customerId);
        zone.setZoneNo(maxZoneNo != null ? maxZoneNo + 1 : 0);
        zone.setName(name.trim());
        zone.setPrice(zeroIfNull(price));
        zone.setPrice1(zeroIfNull(price1));
        zone.setPrice2(zeroIfNull(price2));
        zone.setPrice3(zeroIfNull(price3));
        return toZoneDto(serviceZoneRepository.save(zone));
    }

    public ServiceZoneDto updateZone(Long customerId, Long zoneId, String name, BigDecimal price, BigDecimal price1, BigDecimal price2, BigDecimal price3) {
        requireCustomer(customerId);
        ServiceZone zone = requireZone(customerId, zoneId);
        requireNonBlank(name, "Zonenname fehlt");

        zone.setName(name.trim());
        zone.setPrice(zeroIfNull(price));
        zone.setPrice1(zeroIfNull(price1));
        zone.setPrice2(zeroIfNull(price2));
        zone.setPrice3(zeroIfNull(price3));
        return toZoneDto(serviceZoneRepository.save(zone));
    }

    public void deleteZone(Long customerId, Long zoneId) {
        requireCustomer(customerId);
        requireZone(customerId, zoneId);
        serviceZoneMappingRepository.deleteByZoneId(zoneId);
        serviceZoneRepository.deleteById(Objects.requireNonNull(zoneId));
    }

    @Transactional(readOnly = true)
    public List<ServiceZoneMappingDto> getZoneMappings(Long customerId) {
        Customer customer = requireCustomer(customerId);
        List<ServiceZone> zones = serviceZoneRepository.findByHeadquartersIdAndCustomerIdOrderByZoneNo(customer.getHeadquartersId(), customerId);
        if (zones.isEmpty()) {
            return List.of();
        }

        Map<Long, ServiceZone> zonesById = new HashMap<>();
        for (ServiceZone zone : zones) {
            zonesById.put(zone.getId(), zone);
        }

        List<ServiceZoneMapping> mappings = serviceZoneMappingRepository.findByZoneIdIn(zonesById.keySet());
        Map<Long, ServiceZipCode> zipCodesById = new HashMap<>();
        for (ServiceZipCode zipCode : serviceZipCodeRepository.findAllById(Objects.requireNonNull(mappings.stream().map(ServiceZoneMapping::getZipCodeId).toList()))) {
            zipCodesById.put(zipCode.getId(), zipCode);
        }

        List<ServiceZoneMappingDto> rows = new ArrayList<>();
        for (ServiceZoneMapping mapping : mappings) {
            ServiceZone zone = zonesById.get(mapping.getZoneId());
            ServiceZipCode zipCode = zipCodesById.get(mapping.getZipCodeId());
            if (zone == null || zipCode == null) {
                continue;
            }
            ServiceZoneMappingDto dto = new ServiceZoneMappingDto();
            dto.setZoneId(zone.getId());
            dto.setZoneNo(zone.getZoneNo());
            dto.setZoneName(zone.getName());
            dto.setZipCodeId(zipCode.getId());
            dto.setZipcode(zipCode.getZipcode());
            dto.setMappedValue1(mapping.getMappedValue1());
            dto.setMappedValue2(mapping.getMappedValue2());
            dto.setMappedValue3(mapping.getMappedValue3());
            rows.add(dto);
        }

        rows.sort(Comparator.comparing(ServiceZoneMappingDto::getZipcode, Comparator.nullsLast(String::compareTo))
                .thenComparing(ServiceZoneMappingDto::getZoneNo, Comparator.nullsLast(Integer::compareTo)));
        return rows;
    }

    public ServiceZoneMappingDto createZoneMapping(Long customerId, Long zoneId, String zipcode, BigDecimal mappedValue1, BigDecimal mappedValue2, BigDecimal mappedValue3) {
        requireCustomer(customerId);
        ServiceZone zone = requireZone(customerId, zoneId);
        ServiceZipCode serviceZipCode = requireZipCode(zipcode);
        ensureZipCodeNotMappedToDifferentZone(customerId, serviceZipCode.getId(), null);

        ServiceZoneMapping mapping = new ServiceZoneMapping();
        mapping.setZoneId(zone.getId());
        mapping.setZipCodeId(serviceZipCode.getId());
        mapping.setMappedValue1(zeroIfNull(mappedValue1));
        mapping.setMappedValue2(zeroIfNull(mappedValue2));
        mapping.setMappedValue3(zeroIfNull(mappedValue3));
        serviceZoneMappingRepository.save(mapping);
        return toZoneMappingDto(zone, serviceZipCode, mapping);
    }

    public ServiceZoneMappingDto updateZoneMapping(Long customerId,
                                                   Long currentZoneId,
                                                   Long currentZipCodeId,
                                                   Long newZoneId,
                                                   String zipcode,
                                                   BigDecimal mappedValue1,
                                                   BigDecimal mappedValue2,
                                                   BigDecimal mappedValue3) {
        requireCustomer(customerId);
        ServiceZoneMapping existing = serviceZoneMappingRepository.findById(new ServiceZoneMappingId(currentZoneId, currentZipCodeId))
                .orElseThrow(() -> new RuntimeException("Zonen-Zuordnung nicht gefunden"));
        requireZone(customerId, currentZoneId);

        ServiceZone newZone = requireZone(customerId, newZoneId != null ? newZoneId : currentZoneId);
        ServiceZipCode newZipCode = requireZipCode(zipcode);
        ensureZipCodeNotMappedToDifferentZone(customerId, newZipCode.getId(), existing);

        if (!Objects.equals(existing.getZoneId(), newZone.getId()) || !Objects.equals(existing.getZipCodeId(), newZipCode.getId())) {
            serviceZoneMappingRepository.delete(existing);
            existing = new ServiceZoneMapping();
            existing.setZoneId(newZone.getId());
            existing.setZipCodeId(newZipCode.getId());
        }

        existing.setMappedValue1(zeroIfNull(mappedValue1));
        existing.setMappedValue2(zeroIfNull(mappedValue2));
        existing.setMappedValue3(zeroIfNull(mappedValue3));
        serviceZoneMappingRepository.save(existing);
        return toZoneMappingDto(newZone, newZipCode, existing);
    }

    public void deleteZoneMapping(Long customerId, Long zoneId, Long zipCodeId) {
        requireCustomer(customerId);
        requireZone(customerId, zoneId);
        serviceZoneMappingRepository.deleteById(new ServiceZoneMappingId(zoneId, zipCodeId));
    }

    @Transactional(readOnly = true)
    public List<ServiceRadiusDto> getRadii(Long customerId) {
        Customer customer = requireCustomer(customerId);
        return serviceRadiusRepository.findByHeadquartersIdAndCustomerIdOrderByZipcodeAscCityDistrictAsc(customer.getHeadquartersId(), customerId).stream()
                .map(this::toRadiusDto)
                .toList();
    }

    public ServiceRadiusDto createRadius(Long customerId, String zipcode, String cityDistrict, Integer radiusAreaNo) {
        Customer customer = requireCustomer(customerId);
        ServiceZipCode serviceZipCode = requireZipCode(zipcode);
        requireNonBlank(cityDistrict, "Bezirk fehlt");
        if (radiusAreaNo == null) {
            throw new RuntimeException("Radiusbereich fehlt");
        }
        ensureRadiusCombinationDoesNotExist(customer, null, serviceZipCode.getZipcode(), cityDistrict, radiusAreaNo);

        ServiceRadius radius = new ServiceRadius();
        radius.setHeadquartersId(customer.getHeadquartersId());
        radius.setCustomerId(customerId);
        radius.setZipcode(serviceZipCode.getZipcode());
        radius.setCityDistrict(cityDistrict.trim());
        radius.setRadiusAreaNo(radiusAreaNo);
        return toRadiusDto(serviceRadiusRepository.save(radius));
    }

    public ServiceRadiusDto updateRadius(Long customerId, Long radiusId, String zipcode, String cityDistrict, Integer radiusAreaNo) {
        Customer customer = requireCustomer(customerId);
        ServiceRadius radius = requireRadius(customer, radiusId);
        ServiceZipCode serviceZipCode = requireZipCode(zipcode);
        requireNonBlank(cityDistrict, "Bezirk fehlt");
        if (radiusAreaNo == null) {
            throw new RuntimeException("Radiusbereich fehlt");
        }
        ensureRadiusCombinationDoesNotExist(customer, radiusId, serviceZipCode.getZipcode(), cityDistrict, radiusAreaNo);

        radius.setZipcode(serviceZipCode.getZipcode());
        radius.setCityDistrict(cityDistrict.trim());
        radius.setRadiusAreaNo(radiusAreaNo);
        return toRadiusDto(serviceRadiusRepository.save(radius));
    }

    public void deleteRadius(Long customerId, Long radiusId) {
        Customer customer = requireCustomer(customerId);
        ServiceRadius radius = requireRadius(customer, radiusId);
        serviceRadiusRepository.delete(Objects.requireNonNull(radius));
    }

    @Transactional(readOnly = true)
    public CustomerDeliveryReasonConfigDto getDeliveryReasons(Long customerId) {
        requireCustomer(customerId);

        Map<String, String> reasonsByList = readGenericDataMap(customerId, DELIVERY_REASONS_FIELD);
        Map<String, String> textsByList = readGenericDataMap(customerId, DELIVERY_REASONS_TEXT_FIELD);

        CustomerDeliveryReasonConfigDto dto = new CustomerDeliveryReasonConfigDto();
        dto.setCustomerId(customerId);

        List<DeliveryReasonListDto> lists = new ArrayList<>();
        for (int listNo = 0; listNo < DELIVERY_REASON_LIST_COUNT; listNo++) {
            DeliveryReasonListDto row = new DeliveryReasonListDto();
            row.setListNo(listNo);
            row.setLabel("Liste " + (listNo + 1));
            row.setText(textsByList.getOrDefault(String.valueOf(listNo), ""));
            row.setItems(parseDeliveryReasonItems(reasonsByList.get(String.valueOf(listNo))));
            lists.add(row);
        }
        dto.setLists(lists);
        return dto;
    }

    public void saveDeliveryReasons(Long customerId, List<DeliveryReasonListDto> lists) {
        requireCustomer(customerId);

        Map<String, String> reasonsByList = readGenericDataMap(customerId, DELIVERY_REASONS_FIELD);
        Map<String, String> textsByList = readGenericDataMap(customerId, DELIVERY_REASONS_TEXT_FIELD);

        for (DeliveryReasonListDto list : lists != null ? lists : List.<DeliveryReasonListDto>of()) {
            if (list == null || list.getListNo() == null) {
                continue;
            }
            int listNo = list.getListNo();
            if (listNo < 0 || listNo >= DELIVERY_REASON_LIST_COUNT) {
                continue;
            }

            String key = String.valueOf(listNo);
            String reasons = serializeDeliveryReasonItems(list.getItems());
            if (hasText(reasons)) {
                reasonsByList.put(key, reasons);
            } else {
                reasonsByList.remove(key);
            }

            if (hasText(list.getText())) {
                textsByList.put(key, list.getText().trim());
            } else {
                textsByList.remove(key);
            }
        }

        writeGenericDataMap(customerId, DELIVERY_REASONS_FIELD, reasonsByList);
        writeGenericDataMap(customerId, DELIVERY_REASONS_TEXT_FIELD, textsByList);
    }

    @Transactional(readOnly = true)
    public CustomerAcceptanceProtocolConfigDto getAcceptanceProtocols(Long customerId) {
        requireCustomer(customerId);

        Map<String, String> questionsByService = readGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_QUESTIONS_FIELD);
        Map<String, String> mailStatesByService = readGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_MAIL_STATE_FIELD);
        Map<String, String> textsByService = readGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_TEXT_FIELD);

        Map<Integer, String> serviceLabels = new LinkedHashMap<>();
        for (MetaType metaType : metaTypeRepository.findByTypeOrderBySort("service")) {
            if (metaType.getSort() != null && hasText(metaType.getValue())) {
                serviceLabels.put(metaType.getSort(), metaType.getValue().trim());
            }
        }

        Set<Integer> serviceKeys = new TreeSet<>();
        serviceKeys.addAll(serviceLabels.keySet());
        serviceKeys.addAll(parseIntegerKeys(questionsByService));
        serviceKeys.addAll(parseIntegerKeys(mailStatesByService));
        serviceKeys.addAll(parseIntegerKeys(textsByService));

        CustomerAcceptanceProtocolConfigDto dto = new CustomerAcceptanceProtocolConfigDto();
        dto.setCustomerId(customerId);

        List<AcceptanceProtocolDto> protocols = new ArrayList<>();
        for (Integer serviceKey : serviceKeys) {
            AcceptanceProtocolDto row = new AcceptanceProtocolDto();
            row.setServiceKey(serviceKey);
            row.setServiceLabel(serviceLabels.getOrDefault(serviceKey, "Service " + serviceKey));
            row.setText(textsByService.getOrDefault(String.valueOf(serviceKey), ""));
            row.setQuestions(parseAcceptanceQuestions(
                    questionsByService.get(String.valueOf(serviceKey)),
                    mailStatesByService.get(String.valueOf(serviceKey))
            ));
            protocols.add(row);
        }
        dto.setProtocols(protocols);
        return dto;
    }

    public void saveAcceptanceProtocols(Long customerId, List<AcceptanceProtocolDto> protocols) {
        requireCustomer(customerId);

        Map<String, String> questionsByService = readGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_QUESTIONS_FIELD);
        Map<String, String> mailStatesByService = readGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_MAIL_STATE_FIELD);
        Map<String, String> textsByService = readGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_TEXT_FIELD);

        for (AcceptanceProtocolDto protocol : protocols != null ? protocols : List.<AcceptanceProtocolDto>of()) {
            if (protocol == null || protocol.getServiceKey() == null) {
                continue;
            }

            String key = String.valueOf(protocol.getServiceKey());
            String questions = serializeAcceptanceQuestions(protocol.getQuestions());
            String mailStates = serializeAcceptanceMailStates(protocol.getQuestions());

            if (hasText(questions)) {
                questionsByService.put(key, questions);
            } else {
                questionsByService.remove(key);
            }

            if (hasText(mailStates)) {
                mailStatesByService.put(key, mailStates);
            } else {
                mailStatesByService.remove(key);
            }

            if (hasText(protocol.getText())) {
                textsByService.put(key, protocol.getText().trim());
            } else {
                textsByService.remove(key);
            }
        }

        writeGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_QUESTIONS_FIELD, questionsByService);
        writeGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_MAIL_STATE_FIELD, mailStatesByService);
        writeGenericDataMap(customerId, ACCEPTANCE_PROTOCOL_TEXT_FIELD, textsByService);
    }

    private CatalogOptionDto toCatalogOption(ServiceEntity service) {
        return new CatalogOptionDto(service.getId(), service.getName(), service.getMode());
    }

    private CatalogOptionDto toCatalogOption(ServiceType serviceType) {
        return new CatalogOptionDto(serviceType.getId(), serviceType.getName(), serviceType.getMode());
    }

    private ServiceZoneDto toZoneDto(ServiceZone zone) {
        ServiceZoneDto dto = new ServiceZoneDto();
        dto.setId(zone.getId());
        dto.setZoneNo(zone.getZoneNo());
        dto.setName(zone.getName());
        dto.setPrice(zone.getPrice());
        dto.setPrice1(zone.getPrice1());
        dto.setPrice2(zone.getPrice2());
        dto.setPrice3(zone.getPrice3());
        return dto;
    }

    private ServiceZoneMappingDto toZoneMappingDto(ServiceZone zone, ServiceZipCode zipCode, ServiceZoneMapping mapping) {
        ServiceZoneMappingDto dto = new ServiceZoneMappingDto();
        dto.setZoneId(zone.getId());
        dto.setZoneNo(zone.getZoneNo());
        dto.setZoneName(zone.getName());
        dto.setZipCodeId(zipCode.getId());
        dto.setZipcode(zipCode.getZipcode());
        dto.setMappedValue1(mapping.getMappedValue1());
        dto.setMappedValue2(mapping.getMappedValue2());
        dto.setMappedValue3(mapping.getMappedValue3());
        return dto;
    }

    private ServiceRadiusDto toRadiusDto(ServiceRadius radius) {
        ServiceRadiusDto dto = new ServiceRadiusDto();
        dto.setId(radius.getId());
        dto.setZipcode(radius.getZipcode());
        dto.setCityDistrict(radius.getCityDistrict());
        dto.setRadiusAreaNo(radius.getRadiusAreaNo());
        return dto;
    }

    private List<DeliveryReasonItemDto> parseDeliveryReasonItems(String raw) {
        if (!hasText(raw)) {
            return List.of();
        }

        List<DeliveryReasonItemDto> items = new ArrayList<>();
        String[] tokens = raw.split(java.util.regex.Pattern.quote(DELIVERY_REASON_SEPARATOR), -1);
        for (int i = 0; i < tokens.length; i++) {
            String token = tokens[i];
            if (!hasText(token)) {
                continue;
            }
            int separator = token.indexOf('$');
            DeliveryReasonItemDto item = new DeliveryReasonItemDto();
            item.setSort(i + 1);
            if (separator >= 0) {
                item.setCode(token.substring(0, separator));
                item.setReason(token.substring(separator + 1));
            } else {
                item.setCode("");
                item.setReason(token);
            }
            items.add(item);
        }
        return items;
    }

    private String serializeDeliveryReasonItems(List<DeliveryReasonItemDto> items) {
        return (items != null ? items : List.<DeliveryReasonItemDto>of()).stream()
                .filter(item -> item != null && hasText(item.getReason()))
                .map(item -> safe(item.getCode()) + "$" + item.getReason().trim())
                .collect(Collectors.joining(DELIVERY_REASON_SEPARATOR));
    }

    private List<AcceptanceProtocolQuestionDto> parseAcceptanceQuestions(String rawQuestions, String rawMailStates) {
        List<AcceptanceProtocolQuestionDto> items = new ArrayList<>();
        if (!hasText(rawQuestions)) {
            return items;
        }

        String[] questions = rawQuestions.split(",", -1);
        String[] mailStates = hasText(rawMailStates) ? rawMailStates.split(",", -1) : new String[0];
        for (int i = 0; i < questions.length; i++) {
            if (!hasText(questions[i])) {
                continue;
            }
            AcceptanceProtocolQuestionDto item = new AcceptanceProtocolQuestionDto();
            item.setSort(i + 1);
            item.setQuestion(questions[i]);
            item.setMailState(i < mailStates.length ? safe(mailStates[i]) : "");
            items.add(item);
        }
        return items;
    }

    private String serializeAcceptanceQuestions(List<AcceptanceProtocolQuestionDto> items) {
        return (items != null ? items : List.<AcceptanceProtocolQuestionDto>of()).stream()
                .filter(item -> item != null && hasText(item.getQuestion()))
                .map(item -> item.getQuestion().trim())
                .collect(Collectors.joining(","));
    }

    private String serializeAcceptanceMailStates(List<AcceptanceProtocolQuestionDto> items) {
        return (items != null ? items : List.<AcceptanceProtocolQuestionDto>of()).stream()
                .filter(item -> item != null && hasText(item.getQuestion()))
                .map(item -> normalizeMailState(item.getMailState()))
                .collect(Collectors.joining(","));
    }

    private String normalizeMailState(String value) {
        if (!hasText(value)) {
            return "";
        }
        return switch (value.trim()) {
            case "1", "2" -> value.trim();
            default -> "";
        };
    }

    private Set<Integer> parseIntegerKeys(Map<String, String> values) {
        Set<Integer> keys = new TreeSet<>();
        for (String key : values.keySet()) {
            try {
                keys.add(Integer.parseInt(key));
            } catch (NumberFormatException ignored) {
                // Ignore malformed legacy keys.
            }
        }
        return keys;
    }

    private Map<String, String> readGenericDataMap(Long customerId, String fieldName) {
        String content = genericDataContainerRepository.findByTypeAndIdAndField(CUSTOMER_OBJECT_TYPE, customerId, fieldName)
                .map(GenericDataContainer::getContent)
                .orElse("");
        return parseGenericDataContent(content);
    }

    private Map<String, String> parseGenericDataContent(String content) {
        Map<String, String> values = new LinkedHashMap<>();
        if (!hasText(content)) {
            return values;
        }

        for (String token : content.split("\\|")) {
            if (!hasText(token)) {
                continue;
            }
            int separator = token.indexOf('=');
            if (separator <= 0) {
                continue;
            }
            values.put(token.substring(0, separator), token.substring(separator + 1));
        }
        return values;
    }

    private void writeGenericDataMap(Long customerId, String fieldName, Map<String, String> values) {
        String content = values.entrySet().stream()
                .filter(entry -> hasText(entry.getValue()))
                .sorted(Comparator.comparingInt(entry -> toSortKey(entry.getKey())))
                .map(entry -> entry.getKey() + "=" + entry.getValue())
                .collect(Collectors.joining("|"));

        Optional<GenericDataContainer> existing = genericDataContainerRepository.findByTypeAndIdAndField(
                CUSTOMER_OBJECT_TYPE, customerId, fieldName);
        if (!hasText(content)) {
            existing.ifPresent(genericDataContainerRepository::delete);
            return;
        }

        GenericDataContainer entry = existing.orElseGet(GenericDataContainer::new);
        entry.setObjectType(CUSTOMER_OBJECT_TYPE);
        entry.setObjectId(customerId);
        entry.setFieldName(fieldName);
        entry.setContent(content);
        genericDataContainerRepository.save(entry);
    }

    private int toSortKey(String key) {
        try {
            return Integer.parseInt(key);
        } catch (NumberFormatException ignored) {
            return Integer.MAX_VALUE;
        }
    }

    private Customer requireCustomer(Long customerId) {
        return customerRepository.findById(Objects.requireNonNull(customerId))
                .orElseThrow(() -> new RuntimeException("Kunde nicht gefunden: " + customerId));
    }

    private ServiceZone requireZone(Long customerId, Long zoneId) {
        ServiceZone zone = serviceZoneRepository.findById(Objects.requireNonNull(zoneId))
                .orElseThrow(() -> new RuntimeException("Zone nicht gefunden: " + zoneId));
        if (!Objects.equals(zone.getCustomerId(), customerId)) {
            throw new RuntimeException("Zone gehoert nicht zu Kunde: " + customerId);
        }
        return zone;
    }

    private ServiceRadius requireRadius(Customer customer, Long radiusId) {
        ServiceRadius radius = serviceRadiusRepository.findById(Objects.requireNonNull(radiusId))
                .orElseThrow(() -> new RuntimeException("Radiusbereich nicht gefunden: " + radiusId));
        if (!Objects.equals(radius.getCustomerId(), customer.getId()) || !Objects.equals(radius.getHeadquartersId(), customer.getHeadquartersId())) {
            throw new RuntimeException("Radiusbereich gehoert nicht zu Kunde: " + customer.getId());
        }
        return radius;
    }

    private ServiceZipCode requireZipCode(String zipcode) {
        String normalized = normalizeZipcode(zipcode);
        if (normalized.isBlank()) {
            throw new RuntimeException("PLZ fehlt");
        }
        return serviceZipCodeRepository.findByZipcode(normalized)
                .orElseThrow(() -> new RuntimeException("PLZ nicht im Service-Stamm gefunden: " + normalized));
    }

    private void ensureZipCodeNotMappedToDifferentZone(Long customerId, Long zipCodeId, ServiceZoneMapping current) {
        for (ServiceZoneMappingDto row : getZoneMappings(customerId)) {
            if (!Objects.equals(row.getZipCodeId(), zipCodeId)) {
                continue;
            }
            if (current != null
                    && Objects.equals(current.getZoneId(), row.getZoneId())
                    && Objects.equals(current.getZipCodeId(), row.getZipCodeId())) {
                continue;
            }
            throw new RuntimeException("PLZ ist bereits einer anderen Zone zugeordnet: " + row.getZoneNo());
        }
    }

    private void ensureRadiusCombinationDoesNotExist(Customer customer, Long radiusId, String zipcode, String cityDistrict, Integer radiusAreaNo) {
        for (ServiceRadius radius : serviceRadiusRepository.findByHeadquartersIdAndCustomerIdOrderByZipcodeAscCityDistrictAsc(customer.getHeadquartersId(), customer.getId())) {
            if (radiusId != null && Objects.equals(radius.getId(), radiusId)) {
                continue;
            }
            if (Objects.equals(radius.getZipcode(), zipcode)
                    && Objects.equals(radius.getCityDistrict(), cityDistrict.trim())
                    && Objects.equals(radius.getRadiusAreaNo(), radiusAreaNo)) {
                throw new RuntimeException("Diese Radius-Zuordnung existiert bereits");
            }
        }
    }

    private List<Long> distinctIds(List<Long> ids) {
        if (ids == null) {
            return List.of();
        }
        Map<Long, Long> unique = new LinkedHashMap<>();
        for (Long id : ids) {
            if (id != null) {
                unique.put(id, id);
            }
        }
        return new ArrayList<>(unique.values());
    }

    private void requireNonBlank(String value, String message) {
        if (value == null || value.isBlank()) {
            throw new RuntimeException(message);
        }
    }

    private boolean hasText(String value) {
        return value != null && !value.isBlank();
    }

    private String safe(String value) {
        return value != null ? value : "";
    }

    private BigDecimal zeroIfNull(BigDecimal value) {
        return value != null ? value : BigDecimal.ZERO;
    }

    private String normalizeZipcode(String zipcode) {
        if (zipcode == null) {
            return "";
        }
        String normalized = zipcode.trim();
        if (normalized.matches("\\d{1,4}")) {
            normalized = "0".repeat(5 - normalized.length()) + normalized;
        }
        return normalized;
    }
}
