package de.votian.services.service;

import de.votian.services.dto.TrackingCourierRowDto;
import de.votian.services.dto.TrackingDashboardDto;
import de.votian.services.dto.TrackingJobRowDto;
import de.votian.services.entity.Address;
import de.votian.services.entity.Company;
import de.votian.services.entity.CostCenter;
import de.votian.services.entity.Courier;
import de.votian.services.entity.CourierVehicle;
import de.votian.services.entity.Customer;
import de.votian.services.entity.Job;
import de.votian.services.entity.MetaType;
import de.votian.services.entity.ServiceZipCode;
import de.votian.services.entity.Tour;
import de.votian.services.entity.VehicleDisposition;
import de.votian.services.repository.AddressRepository;
import de.votian.services.repository.CompanyRepository;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.CourierRepository;
import de.votian.services.repository.CourierVehicleRepository;
import de.votian.services.repository.CustomerRepository;
import de.votian.services.repository.JobRepository;
import de.votian.services.repository.MetaTypeRepository;
import de.votian.services.repository.ServiceZipCodeRepository;
import de.votian.services.repository.TourRepository;
import de.votian.services.repository.VehicleDispositionRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

@Service
@Transactional(readOnly = true)
public class TrackingService {

    private final JobRepository jobRepository;
    private final TourRepository tourRepository;
    private final AddressRepository addressRepository;
    private final CostCenterRepository costCenterRepository;
    private final CustomerRepository customerRepository;
    private final CompanyRepository companyRepository;
    private final CourierRepository courierRepository;
    private final CourierVehicleRepository courierVehicleRepository;
    private final VehicleDispositionRepository vehicleDispositionRepository;
    private final ServiceZipCodeRepository serviceZipCodeRepository;
    private final MetaTypeRepository metaTypeRepository;

    public TrackingService(JobRepository jobRepository,
                           TourRepository tourRepository,
                           AddressRepository addressRepository,
                           CostCenterRepository costCenterRepository,
                           CustomerRepository customerRepository,
                           CompanyRepository companyRepository,
                           CourierRepository courierRepository,
                           CourierVehicleRepository courierVehicleRepository,
                           VehicleDispositionRepository vehicleDispositionRepository,
                           ServiceZipCodeRepository serviceZipCodeRepository,
                           MetaTypeRepository metaTypeRepository) {
        this.jobRepository = jobRepository;
        this.tourRepository = tourRepository;
        this.addressRepository = addressRepository;
        this.costCenterRepository = costCenterRepository;
        this.customerRepository = customerRepository;
        this.companyRepository = companyRepository;
        this.courierRepository = courierRepository;
        this.courierVehicleRepository = courierVehicleRepository;
        this.vehicleDispositionRepository = vehicleDispositionRepository;
        this.serviceZipCodeRepository = serviceZipCodeRepository;
        this.metaTypeRepository = metaTypeRepository;
    }

    public TrackingDashboardDto getDashboard(Long hqId,
                                             LocalDate day,
                                             boolean includeCompleted,
                                             Long customerId,
                                             List<Long> accessibleCostCenterIds) {
        LocalDate effectiveDay = day != null ? day : LocalDate.now();
        List<Job> jobs = loadJobs(hqId, effectiveDay, includeCompleted, accessibleCostCenterIds);
        TrackingContext context = loadContext(jobs);
        if (customerId != null) {
            TrackingContext initialContext = context;
            jobs = jobs.stream()
                    .filter(job -> matchesCustomer(job, initialContext, customerId))
                    .toList();
            context = loadContext(jobs);
        }

        List<TrackingJobRowDto> jobRows = new ArrayList<>();
        Map<Long, CourierAggregate> courierAggregates = new LinkedHashMap<>();
        for (Job job : jobs) {
            TrackingJobRowDto row = toJobRow(job, context);
            jobRows.add(row);
            if (row.getCourierId() != null) {
                CourierAggregate aggregate = courierAggregates.computeIfAbsent(
                        row.getCourierId(),
                        ignored -> new CourierAggregate(row.getCourierId())
                );
                aggregate.jobIds.add(job.getId());
                if (row.getCourierVehicleId() != null) {
                    aggregate.courierVehicleId = row.getCourierVehicleId();
                }
                if (row.getVehicleTypeId() != null) {
                    aggregate.vehicleTypeId = row.getVehicleTypeId();
                }
            }
        }

        jobRows.sort(Comparator
                .comparing(TrackingJobRowDto::getOrderTime, Comparator.nullsLast(LocalDateTime::compareTo))
                .thenComparing(TrackingJobRowDto::getJobId, Comparator.nullsLast(Long::compareTo))
                .reversed());

        List<TrackingCourierRowDto> courierRows = buildCourierRows(courierAggregates, context);
        courierRows.sort(Comparator
                .comparing(TrackingCourierRowDto::isTracked, Comparator.reverseOrder())
                .thenComparing(TrackingCourierRowDto::isGpsBased, Comparator.reverseOrder())
                .thenComparing(TrackingCourierRowDto::getCourierSid, String.CASE_INSENSITIVE_ORDER));

        TrackingDashboardDto dto = new TrackingDashboardDto();
        dto.setHeadquartersId(hqId);
        dto.setCustomerId(customerId);
        dto.setDay(effectiveDay);
        dto.setIncludeCompleted(includeCompleted);
        dto.setJobs(jobRows);
        dto.setCouriers(courierRows);
        dto.setTotalJobs(jobRows.size());
        dto.setAssignedJobs((int) jobRows.stream().filter(row -> row.getCourierId() != null).count());
        dto.setTrackedJobs((int) jobRows.stream().filter(TrackingJobRowDto::isCourierTracked).count());
        dto.setTotalCouriers(courierRows.size());
        dto.setGpsCouriers((int) courierRows.stream().filter(row -> row.isTracked() && row.isGpsBased()).count());
        dto.setFallbackCouriers((int) courierRows.stream().filter(row -> row.isTracked() && !row.isGpsBased()).count());
        dto.setUnlocatedCouriers((int) courierRows.stream().filter(row -> !row.isTracked()).count());
        dto.setGeneratedAt(LocalDateTime.now());
        return dto;
    }

    private List<Job> loadJobs(Long hqId,
                               LocalDate day,
                               boolean includeCompleted,
                               List<Long> accessibleCostCenterIds) {
        List<Job> candidateJobs;
        if (accessibleCostCenterIds != null && !accessibleCostCenterIds.isEmpty()) {
            candidateJobs = jobRepository.findByPayerCostCenters(accessibleCostCenterIds);
        } else {
            candidateJobs = jobRepository.findForDispositionDay(
                    hqId,
                    day.atStartOfDay(),
                    day.plusDays(1).atStartOfDay().minusNanos(1)
            );
        }

        return candidateJobs.stream()
                .filter(job -> belongsToHeadquarters(job, hqId))
                .filter(job -> belongsToDay(job, day))
                .filter(job -> isVisibleStatus(job, includeCompleted))
                .toList();
    }

    private boolean belongsToHeadquarters(Job job, Long hqId) {
        return job != null
                && hqId != null
                && (hqId.equals(job.getHeadquartersId()) || hqId.equals(job.getHeadquartersDispoId()));
    }

    private boolean belongsToDay(Job job, LocalDate day) {
        return job != null
                && job.getOrderTime() != null
                && job.getOrderTime().toLocalDate().equals(day);
    }

    private boolean isVisibleStatus(Job job, boolean includeCompleted) {
        if (job == null || Integer.valueOf(10).equals(job.getStatus()) || "2".equals(job.getStorno())) {
            return false;
        }
        if (!includeCompleted && job.getStatus() != null && job.getStatus() >= 8) {
            return false;
        }
        return true;
    }

    private boolean matchesCustomer(Job job, TrackingContext context, Long customerId) {
        Long costCenterId = resolveCostCenterId(job, context.toursByJobId.getOrDefault(job.getId(), List.of()));
        CostCenter costCenter = costCenterId != null ? context.costCenters.get(costCenterId) : null;
        return costCenter != null && customerId.equals(costCenter.getCustomerId());
    }

    private TrackingContext loadContext(List<Job> jobs) {
        if (jobs.isEmpty()) {
            return TrackingContext.empty();
        }

        List<Long> jobIds = jobs.stream().map(Job::getId).toList();
        Map<Long, List<Tour>> toursByJobId = new LinkedHashMap<>();
        for (Tour tour : tourRepository.findByJobIdInOrderByJobIdAscSortAsc(jobIds)) {
            toursByJobId.computeIfAbsent(tour.getJobId(), ignored -> new ArrayList<>()).add(tour);
        }

        Set<Long> addressIds = new LinkedHashSet<>();
        Set<Long> costCenterIds = new LinkedHashSet<>();
        Set<Long> courierIds = new LinkedHashSet<>();
        for (Job job : jobs) {
            List<Tour> tours = toursByJobId.getOrDefault(job.getId(), List.of());
            Tour pickup = findTour(tours, 1);
            Tour delivery = findTour(tours, 2);
            if (pickup != null && pickup.getAddressId() != null) {
                addressIds.add(pickup.getAddressId());
            }
            if (delivery != null && delivery.getAddressId() != null) {
                addressIds.add(delivery.getAddressId());
            }
            Long costCenterId = resolveCostCenterId(job, tours);
            if (costCenterId != null) {
                costCenterIds.add(costCenterId);
            }
            if (job.getCourierId() != null) {
                courierIds.add(job.getCourierId());
            }
        }

        Map<Long, Address> addresses = new HashMap<>();
        for (Address address : addressRepository.findAllById(addressIds)) {
            addresses.put(address.getId(), address);
        }

        Set<String> zipcodes = new LinkedHashSet<>();
        for (Address address : addresses.values()) {
            String zipcode = normalizeZipcode(address.getZipcode());
            if (zipcode != null) {
                zipcodes.add(zipcode);
            }
        }

        Map<Long, Courier> couriers = new HashMap<>();
        for (Courier courier : courierRepository.findAllById(courierIds)) {
            couriers.put(courier.getId(), courier);
            String courierZipcode = normalizeZipcode(courier.getLocationZipcode());
            if (courierZipcode != null) {
                zipcodes.add(courierZipcode);
            }
        }

        Map<String, ServiceZipCode> serviceZipCodes = new HashMap<>();
        for (ServiceZipCode serviceZipCode : serviceZipCodeRepository.findByZipcodeIn(zipcodes)) {
            if (serviceZipCode.getZipcode() != null && !serviceZipCodes.containsKey(serviceZipCode.getZipcode())) {
                serviceZipCodes.put(serviceZipCode.getZipcode(), serviceZipCode);
            }
        }

        Map<Long, CostCenter> costCenters = new HashMap<>();
        for (CostCenter costCenter : costCenterRepository.findAllById(costCenterIds)) {
            costCenters.put(costCenter.getId(), costCenter);
        }

        Map<Long, String> customerNames = loadCustomerNames(costCenters.values());
        Map<Long, String> courierNames = loadCourierNames(couriers.values());

        Map<Long, VehicleDisposition> dispositions = new HashMap<>();
        for (VehicleDisposition disposition : vehicleDispositionRepository.findByJobIdIn(jobIds)) {
            dispositions.put(disposition.getJobId(), disposition);
        }

        Set<Long> courierVehicleIds = new LinkedHashSet<>();
        for (VehicleDisposition disposition : dispositions.values()) {
            if (disposition.getCourierVehicleId() != null) {
                courierVehicleIds.add(disposition.getCourierVehicleId());
            }
        }

        Map<Long, CourierVehicle> vehicles = new HashMap<>();
        for (CourierVehicle vehicle : courierVehicleRepository.findAllById(courierVehicleIds)) {
            vehicles.put(vehicle.getId(), vehicle);
        }

        Map<Integer, String> vehicleTypes = new HashMap<>();
        for (MetaType metaType : metaTypeRepository.findByTypeOrderBySort("vehicletype")) {
            vehicleTypes.put(metaType.getSort(), metaType.getValue());
        }

        return new TrackingContext(
                toursByJobId,
                addresses,
                serviceZipCodes,
                costCenters,
                customerNames,
                couriers,
                courierNames,
                dispositions,
                vehicles,
                vehicleTypes
        );
    }

    private Map<Long, String> loadCustomerNames(Collection<CostCenter> costCenters) {
        Set<Long> customerIds = new LinkedHashSet<>();
        for (CostCenter costCenter : costCenters) {
            if (costCenter.getCustomerId() != null) {
                customerIds.add(costCenter.getCustomerId());
            }
        }
        Map<Long, Customer> customers = new HashMap<>();
        for (Customer customer : customerRepository.findAllById(customerIds)) {
            customers.put(customer.getId(), customer);
        }

        Set<Long> companyIds = new LinkedHashSet<>();
        for (Customer customer : customers.values()) {
            if (customer.getCompanyId() != null) {
                companyIds.add(customer.getCompanyId());
            }
        }

        Map<Long, Company> companies = new HashMap<>();
        for (Company company : companyRepository.findAllById(companyIds)) {
            companies.put(company.getId(), company);
        }

        Map<Long, String> result = new HashMap<>();
        for (Customer customer : customers.values()) {
            Company company = companies.get(customer.getCompanyId());
            result.put(customer.getId(), company != null && company.getComp() != null && !company.getComp().isBlank()
                    ? company.getComp()
                    : blank(customer.getEid()));
        }
        return result;
    }

    private Map<Long, String> loadCourierNames(Collection<Courier> couriers) {
        Set<Long> companyIds = new LinkedHashSet<>();
        for (Courier courier : couriers) {
            if (courier.getCompanyId() != null) {
                companyIds.add(courier.getCompanyId());
            }
        }

        Map<Long, Company> companies = new HashMap<>();
        for (Company company : companyRepository.findAllById(companyIds)) {
            companies.put(company.getId(), company);
        }

        Map<Long, String> result = new HashMap<>();
        for (Courier courier : couriers) {
            Company company = companies.get(courier.getCompanyId());
            result.put(courier.getId(), company != null && company.getComp() != null && !company.getComp().isBlank()
                    ? company.getComp()
                    : blank(courier.getSid()));
        }
        return result;
    }

    private TrackingJobRowDto toJobRow(Job job, TrackingContext context) {
        List<Tour> tours = context.toursByJobId.getOrDefault(job.getId(), List.of());
        Tour pickup = findTour(tours, 1);
        Tour delivery = findTour(tours, 2);

        TrackingJobRowDto dto = new TrackingJobRowDto();
        dto.setJobId(job.getId());
        dto.setStatus(job.getStatus());
        dto.setStatusLabel(statusLabel(job.getStatus()));
        dto.setOrderTime(job.getOrderTime());
        dto.setTakeTime(job.getTakeTime());
        dto.setFinishTime(job.getFinishTime());
        dto.setTotalPrice(job.getTotalPrice());

        Long costCenterId = resolveCostCenterId(job, tours);
        CostCenter costCenter = costCenterId != null ? context.costCenters.get(costCenterId) : null;
        dto.setCostCenterId(costCenterId);
        dto.setCostCenterName(costCenter != null ? blank(costCenter.getName()) : "");
        dto.setCustomerId(costCenter != null ? costCenter.getCustomerId() : null);
        dto.setCustomerName(costCenter != null
                ? context.customerNames.getOrDefault(costCenter.getCustomerId(), "")
                : "");

        dto.setCommissionNo(firstNonBlank(
                pickup != null ? pickup.getCommissionNo() : null,
                delivery != null ? delivery.getCommissionNo() : null,
                ""
        ));
        dto.setPickupName(formatTourStop(pickup));
        dto.setDeliveryName(formatTourStop(delivery));

        Address pickupAddress = pickup != null && pickup.getAddressId() != null ? context.addresses.get(pickup.getAddressId()) : null;
        Address deliveryAddress = delivery != null && delivery.getAddressId() != null ? context.addresses.get(delivery.getAddressId()) : null;
        dto.setPickupZipcode(pickupAddress != null ? blank(pickupAddress.getZipcode()) : "");
        dto.setPickupCity(pickupAddress != null ? blank(pickupAddress.getCity()) : "");
        dto.setDeliveryZipcode(deliveryAddress != null ? blank(deliveryAddress.getZipcode()) : "");
        dto.setDeliveryCity(deliveryAddress != null ? blank(deliveryAddress.getCity()) : "");
        applyMapCoordinates(dto, pickupAddress, deliveryAddress, context.zipcodes);

        dto.setCourierId(job.getCourierId());
        dto.setCourierSid(blank(job.getCourierSid()));
        Courier courier = job.getCourierId() != null ? context.couriers.get(job.getCourierId()) : null;
        dto.setCourierCompanyName(courier != null ? context.courierNames.getOrDefault(courier.getId(), "") : "");
        CourierTracking courierTracking = resolveCourierTracking(courier, context.zipcodes);
        dto.setCourierLatitude(courierTracking.latitude());
        dto.setCourierLongitude(courierTracking.longitude());
        dto.setCourierGpsTime(courierTracking.gpsTime());
        dto.setCourierGpsType(courierTracking.gpsType());
        dto.setCourierGpsTypeLabel(courierTracking.gpsTypeLabel());
        dto.setCourierLocationZipcode(courierTracking.locationZipcode());
        dto.setCourierTracked(courierTracking.tracked());
        dto.setCourierGpsBased(courierTracking.gpsBased());

        VehicleDisposition disposition = context.dispositions.get(job.getId());
        CourierVehicle vehicle = disposition != null ? context.vehicles.get(disposition.getCourierVehicleId()) : null;
        dto.setCourierVehicleId(vehicle != null ? vehicle.getId() : null);
        dto.setCourierVehicleSid(vehicle != null ? blank(vehicle.getSid()) : "");
        Integer vehicleTypeId = vehicle != null ? vehicle.getVehicleTypeId() : job.getVehicleTypeId();
        dto.setVehicleTypeId(vehicleTypeId);
        dto.setVehicleTypeName(vehicleTypeId != null
                ? context.vehicleTypes.getOrDefault(vehicleTypeId, "Typ " + vehicleTypeId)
                : "");
        return dto;
    }

    private List<TrackingCourierRowDto> buildCourierRows(Map<Long, CourierAggregate> courierAggregates,
                                                         TrackingContext context) {
        List<TrackingCourierRowDto> rows = new ArrayList<>();
        for (CourierAggregate aggregate : courierAggregates.values()) {
            Courier courier = context.couriers.get(aggregate.courierId);
            if (courier == null) {
                continue;
            }

            TrackingCourierRowDto dto = new TrackingCourierRowDto();
            dto.setCourierId(courier.getId());
            dto.setCourierSid(blank(courier.getSid()));
            dto.setCompanyName(context.courierNames.getOrDefault(courier.getId(), ""));
            dto.setActiveJobs(aggregate.jobIds.size());
            dto.setLocationZipcode(blank(courier.getLocationZipcode()));
            dto.setGpsTime(courier.getGpsTime());
            dto.setGpsType(courier.getGpsType());
            dto.setGpsTypeLabel(locatingTypeLabel(courier.getGpsType()));
            dto.setAvailableTime(courier.getAvailableTime());
            dto.setMobilePda(blank(courier.getMobilePda()));

            CourierTracking tracking = resolveCourierTracking(courier, context.zipcodes);
            dto.setLatitude(tracking.latitude());
            dto.setLongitude(tracking.longitude());
            dto.setTracked(tracking.tracked());
            dto.setGpsBased(tracking.gpsBased());

            CourierVehicle vehicle = aggregate.courierVehicleId != null
                    ? context.vehicles.get(aggregate.courierVehicleId)
                    : null;
            Integer vehicleTypeId = vehicle != null ? vehicle.getVehicleTypeId() : aggregate.vehicleTypeId;
            String vehicleTypeName = vehicleTypeId != null
                    ? context.vehicleTypes.getOrDefault(vehicleTypeId, "Typ " + vehicleTypeId)
                    : "";
            dto.setVehicleDisplay(vehicleDisplay(vehicle != null ? vehicle.getSid() : "", vehicleTypeName));
            rows.add(dto);
        }
        return rows;
    }

    private CourierTracking resolveCourierTracking(Courier courier, Map<String, ServiceZipCode> zipcodes) {
        if (courier == null) {
            return CourierTracking.empty();
        }
        if (courier.getGpsLatitude() != null && courier.getGpsLongitude() != null) {
            return new CourierTracking(
                    courier.getGpsLatitude(),
                    courier.getGpsLongitude(),
                    courier.getGpsTime(),
                    courier.getGpsType(),
                    locatingTypeLabel(courier.getGpsType()),
                    blank(courier.getLocationZipcode()),
                    true,
                    true
            );
        }

        ServiceZipCode zipcode = zipcodes.get(normalizeZipcode(courier.getLocationZipcode()));
        if (isGeocoded(zipcode)) {
            return new CourierTracking(
                    zipcode.getLatitude(),
                    zipcode.getLongitude(),
                    courier.getGpsTime(),
                    courier.getGpsType(),
                    locatingTypeLabel(courier.getGpsType()),
                    blank(courier.getLocationZipcode()),
                    true,
                    false
            );
        }

        return new CourierTracking(
                null,
                null,
                courier.getGpsTime(),
                courier.getGpsType(),
                locatingTypeLabel(courier.getGpsType()),
                blank(courier.getLocationZipcode()),
                false,
                false
        );
    }

    private void applyMapCoordinates(TrackingJobRowDto dto,
                                     Address pickupAddress,
                                     Address deliveryAddress,
                                     Map<String, ServiceZipCode> zipcodes) {
        ServiceZipCode pickupZip = pickupAddress != null ? zipcodes.get(normalizeZipcode(pickupAddress.getZipcode())) : null;
        ServiceZipCode deliveryZip = deliveryAddress != null ? zipcodes.get(normalizeZipcode(deliveryAddress.getZipcode())) : null;
        if (!isGeocoded(pickupZip) || !isGeocoded(deliveryZip)) {
            return;
        }
        dto.setPickupLatitude(Objects.requireNonNull(pickupZip).getLatitude());
        dto.setPickupLongitude(pickupZip.getLongitude());
        dto.setDeliveryLatitude(Objects.requireNonNull(deliveryZip).getLatitude());
        dto.setDeliveryLongitude(deliveryZip.getLongitude());
    }

    private boolean isGeocoded(ServiceZipCode zipcode) {
        return zipcode != null && zipcode.getLatitude() != null && zipcode.getLongitude() != null;
    }

    private Long resolveCostCenterId(Job job, List<Tour> tours) {
        if (job.getCostCenterPayerId() != null && job.getCostCenterPayerId() > 0) {
            return job.getCostCenterPayerId();
        }
        if (job.getCostCenterPayerCashId() != null && job.getCostCenterPayerCashId() > 0) {
            return job.getCostCenterPayerCashId();
        }
        Tour pickup = findTour(tours, 1);
        if (pickup != null && pickup.getCostCenterId() != null && pickup.getCostCenterId() > 0) {
            return pickup.getCostCenterId();
        }
        if (job.getCostCenterRelatedId() != null && job.getCostCenterRelatedId() > 0) {
            return job.getCostCenterRelatedId();
        }
        return null;
    }

    private Tour findTour(List<Tour> tours, int sort) {
        return tours.stream()
                .filter(tour -> Integer.valueOf(sort).equals(tour.getSort()))
                .findFirst()
                .orElse(null);
    }

    private String formatTourStop(Tour tour) {
        return tour != null ? firstNonBlank(tour.getComp(), tour.getPerson(), "") : "";
    }

    private String vehicleDisplay(String vehicleSid, String vehicleTypeName) {
        String sid = blank(vehicleSid);
        String type = blank(vehicleTypeName);
        if (!sid.isBlank() && !type.isBlank()) {
            return sid + " / " + type;
        }
        return !sid.isBlank() ? sid : type;
    }

    private String statusLabel(Integer status) {
        if (status == null) {
            return "";
        }
        return switch (status) {
            case 0 -> "Blockiert";
            case 1 -> "Angenommen";
            case 2 -> "Unterwegs";
            case 8 -> "Zugestellt";
            case 9 -> "Zu vermitteln";
            case 10 -> "Storniert";
            default -> "Status " + status;
        };
    }

    private String locatingTypeLabel(Integer locatingType) {
        if (locatingType == null) {
            return "";
        }
        return switch (locatingType) {
            case 0 -> "unbestimmt";
            case 1 -> "LBS";
            case 2 -> "GPS";
            case 3 -> "Network";
            case 9 -> "Ortung aus";
            default -> "Typ " + locatingType;
        };
    }

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

    private String firstNonBlank(String... values) {
        for (String value : values) {
            if (value != null && !value.isBlank()) {
                return value;
            }
        }
        return "";
    }

    private String normalizeZipcode(String zipcode) {
        if (zipcode == null) {
            return null;
        }
        String value = zipcode.trim();
        return value.isBlank() ? null : value;
    }

    private record TrackingContext(Map<Long, List<Tour>> toursByJobId,
                                   Map<Long, Address> addresses,
                                   Map<String, ServiceZipCode> zipcodes,
                                   Map<Long, CostCenter> costCenters,
                                   Map<Long, String> customerNames,
                                   Map<Long, Courier> couriers,
                                   Map<Long, String> courierNames,
                                   Map<Long, VehicleDisposition> dispositions,
                                   Map<Long, CourierVehicle> vehicles,
                                   Map<Integer, String> vehicleTypes) {

        private static TrackingContext empty() {
            return new TrackingContext(Map.of(), Map.of(), Map.of(), Map.of(), Map.of(), Map.of(), Map.of(), Map.of(), Map.of(), Map.of());
        }
    }

    private record CourierTracking(Double latitude,
                                   Double longitude,
                                   LocalDateTime gpsTime,
                                   Integer gpsType,
                                   String gpsTypeLabel,
                                   String locationZipcode,
                                   boolean tracked,
                                   boolean gpsBased) {

        private static CourierTracking empty() {
            return new CourierTracking(null, null, null, null, "", "", false, false);
        }
    }

    private static final class CourierAggregate {
        private final Long courierId;
        private final Set<Long> jobIds = new LinkedHashSet<>();
        private Long courierVehicleId;
        private Integer vehicleTypeId;

        private CourierAggregate(Long courierId) {
            this.courierId = courierId;
        }
    }
}
