package de.votian.services.service;

import de.votian.services.config.ParameterKeys;
import de.votian.services.dto.LonghaulDashboardDto;
import de.votian.services.dto.LonghaulJobDetailDto;
import de.votian.services.dto.LonghaulJobRowDto;
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.Duration;
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
public class LonghaulService {

    private static final double LONGHAUL_DISTANCE_FACTOR = 0.56720633650927d / 50.02d;

    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 ServiceZipCodeRepository serviceZipCodeRepository;
    private final VehicleDispositionRepository vehicleDispositionRepository;
    private final CourierVehicleRepository courierVehicleRepository;
    private final MetaTypeRepository metaTypeRepository;
    private final ParameterService parameterService;
    private final LonghaulRemoteDbService longhaulRemoteDbService;

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

    @Transactional(readOnly = true)
    public LonghaulDashboardDto getDashboard(Long hqId) {
        return getDashboard(hqId, false);
    }

    @Transactional(readOnly = true)
    public LonghaulDashboardDto getDashboard(Long hqId, boolean remoteDbActive) {
        if (remoteDbActive) {
            LonghaulRemoteDbService.RemoteSnapshot remoteSnapshot = longhaulRemoteDbService.loadSnapshot(hqId);
            return buildDashboard(
                    new LonghaulConfig(remoteSnapshot.mediationActive(), remoteSnapshot.minimumDistanceKm()),
                    remoteSnapshot.jobs(),
                    new LonghaulContext(
                            remoteSnapshot.toursByJobId(),
                            remoteSnapshot.addresses(),
                            remoteSnapshot.zipcodes(),
                            remoteSnapshot.costCenters(),
                            remoteSnapshot.customerNames(),
                            remoteSnapshot.dispositions(),
                            remoteSnapshot.vehicles(),
                            remoteSnapshot.vehicleTypes()
                    ),
                    remoteSnapshot.effectiveHeadquartersId(),
                    true
            );
        }

        LonghaulConfig config = resolveConfig(hqId);
        List<Job> jobs = loadCandidateJobs(hqId);
        LonghaulContext context = loadContext(jobs);
        return buildDashboard(config, jobs, context, hqId, false);
    }

    @Transactional(readOnly = true)
    public LonghaulJobDetailDto getJobDetail(Long hqId, Long jobId, boolean remoteDbActive) {
        if (remoteDbActive) {
            LonghaulRemoteDbService.RemoteJobDetailSnapshot snapshot = longhaulRemoteDbService.loadJobDetailSnapshot(hqId, jobId);
            return buildDetail(
                    snapshot.job(),
                    new LonghaulContext(
                            snapshot.toursByJobId(),
                            snapshot.addresses(),
                            snapshot.zipcodes(),
                            snapshot.costCenters(),
                            snapshot.customerNames(),
                            snapshot.dispositions(),
                            snapshot.vehicles(),
                            snapshot.vehicleTypes()
                    ),
                    toLocatingData(snapshot.courierStatus()),
                    snapshot.effectiveHeadquartersId(),
                    true
            );
        }

        Job job = jobRepository.findById(Objects.requireNonNull(jobId))
                .orElseThrow(() -> new IllegalArgumentException("Auftrag nicht gefunden: " + jobId));
        if (!hqId.equals(job.getHeadquartersId())) {
            throw new IllegalArgumentException("Auftrag gehoert nicht zur aktiven Niederlassung.");
        }

        LonghaulJobDetailDto dto = buildDetail(
                job,
                loadContext(List.of(job)),
                job.getCourierId() != null ? courierRepository.findById(Objects.requireNonNull(job.getCourierId())).map(this::toLocatingData).orElse(null) : null,
                hqId,
                false
        );
        dto.setRemoteDbActive(false);
        return dto;
    }

    private LonghaulDashboardDto buildDashboard(LonghaulConfig config,
                                                List<Job> jobs,
                                                LonghaulContext context,
                                                Long effectiveHeadquartersId,
                                                boolean remoteDbActive) {
        List<LonghaulJobRowDto> pendingJobs = new ArrayList<>();
        List<LonghaulJobRowDto> acceptedJobs = new ArrayList<>();
        List<LonghaulJobRowDto> returnJobs = new ArrayList<>();

        for (Job job : jobs) {
            List<Tour> tours = context.toursByJobId.getOrDefault(job.getId(), List.of());
            Tour pickup = findTour(tours, 1);
            Tour delivery = findTour(tours, 2);
            if (pickup == null || delivery == null) {
                continue;
            }
            if (!matchesLonghaul(job, pickup, delivery, context.addresses, context.zipcodes, config)) {
                continue;
            }

            LonghaulJobRowDto row = toRow(job, pickup, delivery, context, config.mediationActive());
            if (Integer.valueOf(9).equals(job.getStatus())) {
                pendingJobs.add(row);
            } else if (Integer.valueOf(1).equals(job.getStatus())) {
                acceptedJobs.add(row);
            } else if (Integer.valueOf(2).equals(job.getStatus()) && isReturnTrip(job)) {
                returnJobs.add(row);
            }
        }

        Comparator<LonghaulJobRowDto> sorter = Comparator
                .comparing(LonghaulJobRowDto::getOrderTime, Comparator.nullsLast(LocalDateTime::compareTo))
                .thenComparing(LonghaulJobRowDto::getJobId, Comparator.nullsLast(Long::compareTo))
                .reversed();
        pendingJobs.sort(sorter);
        acceptedJobs.sort(sorter);
        returnJobs.sort(sorter);

        LonghaulDashboardDto dto = new LonghaulDashboardDto();
        dto.setMediationActive(config.mediationActive());
        dto.setMinimumDistanceKm(config.minimumDistanceKm());
        dto.setPendingJobs(pendingJobs);
        dto.setAcceptedJobs(acceptedJobs);
        dto.setReturnJobs(returnJobs);
        dto.setPendingCount(pendingJobs.size());
        dto.setAcceptedCount(acceptedJobs.size());
        dto.setReturnCount(returnJobs.size());
        dto.setRemoteDbActive(remoteDbActive);
        dto.setEffectiveHeadquartersId(effectiveHeadquartersId);
        dto.setGeneratedAt(LocalDateTime.now());
        return dto;
    }

    private LonghaulJobDetailDto buildDetail(Job job,
                                             LonghaulContext context,
                                             LocatingData locatingData,
                                             Long effectiveHeadquartersId,
                                             boolean remoteDbActive) {
        List<Tour> tours = context.toursByJobId.getOrDefault(job.getId(), List.of());
        Tour pickup = findTour(tours, 1);
        Tour delivery = findTour(tours, 2);
        LonghaulJobRowDto row = toRow(job, pickup, delivery, context, true);

        LonghaulJobDetailDto dto = new LonghaulJobDetailDto();
        copyRow(row, dto);
        dto.setRemoteDbActive(remoteDbActive);
        dto.setEffectiveHeadquartersId(effectiveHeadquartersId);
        dto.setTourName(blank(job.getTourName()));
        applyStopDetail(dto, pickup, delivery, context.addresses);
        applyLocatingDetail(dto, locatingData);
        return dto;
    }

    public void returnJobToHeadquarters(Long hqId, Long jobId) {
        returnJobToHeadquarters(hqId, jobId, false);
    }

    public void returnJobToHeadquarters(Long hqId, Long jobId, boolean remoteDbActive) {
        if (remoteDbActive) {
            longhaulRemoteDbService.returnJobToHeadquarters(hqId, jobId);
            return;
        }
        Job job = jobRepository.findById(Objects.requireNonNull(jobId))
                .orElseThrow(() -> new IllegalArgumentException("Auftrag nicht gefunden: " + jobId));
        if (!hqId.equals(job.getHeadquartersId())) {
            throw new IllegalArgumentException("Auftrag gehoert nicht zur aktiven Niederlassung.");
        }

        LonghaulConfig config = resolveConfig(hqId);
        if (!config.mediationActive()) {
            throw new IllegalArgumentException("Die Rueckgabe ist nur bei aktiver Ferntour-Vermittlung verfuegbar.");
        }
        if (!Integer.valueOf(1).equals(job.getLonghaul())) {
            throw new IllegalArgumentException("Der Auftrag kann nicht zurueckgegeben werden, da er manuell als Ferntour markiert wurde.");
        }

        job.setLonghaul(2);
        jobRepository.save(job);
    }

    private List<Job> loadCandidateJobs(Long hqId) {
        return jobRepository.findByHeadquartersIdAndStatusInOrderByOrderTimeAscIdAsc(hqId, List.of(1, 2, 9)).stream()
                .filter(job -> !Integer.valueOf(10).equals(job.getStatus()))
                .filter(job -> !"2".equals(job.getStorno()))
                .toList();
    }

    private LonghaulContext loadContext(List<Job> jobs) {
        if (jobs.isEmpty()) {
            return LonghaulContext.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<>();
        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);
            }
        }

        Map<Long, Address> addresses = new HashMap<>();
        if (!addressIds.isEmpty()) {
            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<String, ServiceZipCode> serviceZipCodes = new HashMap<>();
        if (!zipcodes.isEmpty()) {
            for (ServiceZipCode serviceZipCode : serviceZipCodeRepository.findByZipcodeIn(zipcodes)) {
                if (serviceZipCode.getZipcode() != null) {
                    serviceZipCodes.put(serviceZipCode.getZipcode(), serviceZipCode);
                }
            }
        }

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

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

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

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

        Map<Long, CourierVehicle> vehicles = new HashMap<>();
        if (!vehicleIds.isEmpty()) {
            for (CourierVehicle vehicle : courierVehicleRepository.findAllById(vehicleIds)) {
                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 LonghaulContext(toursByJobId, addresses, serviceZipCodes, costCenters, customerNames, 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());
            }
        }
        if (customerIds.isEmpty()) {
            return Map.of();
        }

        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<>();
        if (!companyIds.isEmpty()) {
            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());
            String name = company != null && company.getComp() != null && !company.getComp().isBlank()
                    ? company.getComp()
                    : customer.getEid();
            result.put(customer.getId(), name);
        }
        return result;
    }

    private LonghaulConfig resolveConfig(Long hqId) {
        boolean mediationActive = "1".equals(parameterService.getParameterValueWithFallback(ParameterKeys.LONGHAUL_ACTIVE, hqId));
        int minimumDistanceKm = parseInt(parameterService.getParameterValueWithFallback(ParameterKeys.LONGHAUL_KM, hqId), 80);
        return new LonghaulConfig(mediationActive, minimumDistanceKm);
    }

    private int parseInt(String value, int fallback) {
        try {
            return value != null && !value.isBlank() ? Integer.parseInt(value.trim()) : fallback;
        } catch (NumberFormatException e) {
            return fallback;
        }
    }

    private boolean matchesLonghaul(Job job,
                                    Tour pickup,
                                    Tour delivery,
                                    Map<Long, Address> addresses,
                                    Map<String, ServiceZipCode> zipcodes,
                                    LonghaulConfig config) {
        if (config.mediationActive()) {
            return isLonghaulStateVisible(job.getLonghaul());
        }

        Address pickupAddress = pickup.getAddressId() != null ? addresses.get(pickup.getAddressId()) : null;
        Address deliveryAddress = delivery.getAddressId() != null ? addresses.get(delivery.getAddressId()) : null;
        if (pickupAddress == null || deliveryAddress == null) {
            return false;
        }

        ServiceZipCode pickupZip = zipcodes.get(normalizeZipcode(pickupAddress.getZipcode()));
        ServiceZipCode deliveryZip = zipcodes.get(normalizeZipcode(deliveryAddress.getZipcode()));
        if (!isGeocoded(pickupZip) || !isGeocoded(deliveryZip)) {
            return false;
        }

        double delta = calculateCoordinateDelta(pickupZip, deliveryZip);
        return delta >= config.minimumDistanceKm() * LONGHAUL_DISTANCE_FACTOR;
    }

    private boolean isReturnTrip(Job job) {
        if (job.getTakeTime() == null || job.getFinishTime() == null) {
            return false;
        }
        if (job.getFinishTime().isBefore(LocalDateTime.now().minusDays(31))) {
            return false;
        }

        Duration outboundDuration = Duration.between(job.getTakeTime(), job.getFinishTime());
        if (outboundDuration.isNegative()) {
            return false;
        }
        return !job.getFinishTime().plus(outboundDuration).isBefore(LocalDateTime.now());
    }

    private LonghaulJobRowDto toRow(Job job,
                                    Tour pickup,
                                    Tour delivery,
                                    LonghaulContext context,
                                    boolean mediationActive) {
        LonghaulJobRowDto dto = new LonghaulJobRowDto();
        dto.setJobId(job.getId());
        dto.setStatus(job.getStatus());
        dto.setLonghaulState(job.getLonghaul());
        dto.setOrderTime(job.getOrderTime());
        dto.setTakeTime(job.getTakeTime());
        dto.setFinishTime(job.getFinishTime());
        dto.setCourierId(job.getCourierId());
        dto.setCourierSid(job.getCourierSid());
        dto.setTotalPrice(job.getTotalPrice());
        dto.setManualLonghaul(Integer.valueOf(3).equals(job.getLonghaul()));
        dto.setReturnable(mediationActive && Integer.valueOf(9).equals(job.getStatus()) && Integer.valueOf(1).equals(job.getLonghaul()));

        Long costCenterId = resolveCostCenterId(job, context.toursByJobId.getOrDefault(job.getId(), List.of()));
        CostCenter costCenter = costCenterId != null ? context.costCenters.get(costCenterId) : null;
        dto.setCostCenterName(costCenter != null ? costCenter.getName() : "");
        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 ? pickupAddress.getZipcode() : "");
        dto.setPickupCity(pickupAddress != null ? pickupAddress.getCity() : "");
        dto.setDeliveryZipcode(deliveryAddress != null ? deliveryAddress.getZipcode() : "");
        dto.setDeliveryCity(deliveryAddress != null ? deliveryAddress.getCity() : "");
        applyMapCoordinates(dto, pickupAddress, deliveryAddress, context.zipcodes);

        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 && vehicle.getSid() != null ? 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 void copyRow(LonghaulJobRowDto source, LonghaulJobDetailDto target) {
        target.setJobId(source.getJobId());
        target.setStatus(source.getStatus());
        target.setLonghaulState(source.getLonghaulState());
        target.setOrderTime(source.getOrderTime());
        target.setTakeTime(source.getTakeTime());
        target.setFinishTime(source.getFinishTime());
        target.setCustomerName(source.getCustomerName());
        target.setCostCenterName(source.getCostCenterName());
        target.setCommissionNo(source.getCommissionNo());
        target.setPickupName(source.getPickupName());
        target.setPickupZipcode(source.getPickupZipcode());
        target.setPickupCity(source.getPickupCity());
        target.setPickupLatitude(source.getPickupLatitude());
        target.setPickupLongitude(source.getPickupLongitude());
        target.setDeliveryName(source.getDeliveryName());
        target.setDeliveryZipcode(source.getDeliveryZipcode());
        target.setDeliveryCity(source.getDeliveryCity());
        target.setDeliveryLatitude(source.getDeliveryLatitude());
        target.setDeliveryLongitude(source.getDeliveryLongitude());
        target.setCourierId(source.getCourierId());
        target.setCourierSid(source.getCourierSid());
        target.setCourierVehicleId(source.getCourierVehicleId());
        target.setCourierVehicleSid(source.getCourierVehicleSid());
        target.setVehicleTypeId(source.getVehicleTypeId());
        target.setVehicleTypeName(source.getVehicleTypeName());
        target.setTotalPrice(source.getTotalPrice());
        target.setAirDistanceKm(source.getAirDistanceKm());
        target.setManualLonghaul(source.isManualLonghaul());
        target.setReturnable(source.isReturnable());
    }

    private void applyStopDetail(LonghaulJobDetailDto dto,
                                 Tour pickup,
                                 Tour delivery,
                                 Map<Long, Address> addresses) {
        Address pickupAddress = pickup != null && pickup.getAddressId() != null ? addresses.get(pickup.getAddressId()) : null;
        Address deliveryAddress = delivery != null && delivery.getAddressId() != null ? addresses.get(delivery.getAddressId()) : null;

        dto.setPickupPerson(pickup != null ? blank(pickup.getPerson()) : "");
        dto.setPickupPhone(pickup != null ? blank(pickup.getPhone()) : "");
        dto.setPickupRemark(pickup != null ? blank(pickup.getRemark()) : "");
        dto.setPickupStreet(formatStreet(pickupAddress, pickup));
        dto.setPickupCountry(pickupAddress != null ? blank(pickupAddress.getCountry()) : "");

        dto.setDeliveryPerson(delivery != null ? blank(delivery.getPerson()) : "");
        dto.setDeliveryPhone(delivery != null ? blank(delivery.getPhone()) : "");
        dto.setDeliveryRemark(delivery != null ? blank(delivery.getRemark()) : "");
        dto.setDeliveryStreet(formatStreet(deliveryAddress, delivery));
        dto.setDeliveryCountry(deliveryAddress != null ? blank(deliveryAddress.getCountry()) : "");
    }

    private void applyLocatingDetail(LonghaulJobDetailDto dto, LocatingData locatingData) {
        if (locatingData == null) {
            return;
        }
        dto.setLocatingLatitude(locatingData.latitude());
        dto.setLocatingLongitude(locatingData.longitude());
        dto.setLocatingTime(locatingData.gpsTime());
        dto.setLocatingType(locatingData.gpsType());
        dto.setLocatingTypeLabel(locatingTypeLabel(locatingData.gpsType()));
        dto.setLocatingZipcode(blank(locatingData.locationZipcode()));
        dto.setAvailableTime(locatingData.availableTime());
    }

    private boolean isLonghaulStateVisible(Integer longhaulState) {
        return longhaulState != null && longhaulState != 0 && longhaulState != 2;
    }

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

    private void applyMapCoordinates(LonghaulJobRowDto dto,
                                     Address pickupAddress,
                                     Address deliveryAddress,
                                     Map<String, ServiceZipCode> zipcodes) {
        ServiceZipCode pickupZip = resolveServiceZipCode(pickupAddress, zipcodes);
        ServiceZipCode deliveryZip = resolveServiceZipCode(deliveryAddress, zipcodes);
        if (!isGeocoded(pickupZip) || !isGeocoded(deliveryZip)) {
            return;
        }

        dto.setPickupLatitude(pickupZip.getLatitude());
        dto.setPickupLongitude(pickupZip.getLongitude());
        dto.setDeliveryLatitude(deliveryZip.getLatitude());
        dto.setDeliveryLongitude(deliveryZip.getLongitude());
        dto.setAirDistanceKm(roundOneDecimal(calculateDistanceKm(pickupZip, deliveryZip)));
    }

    private ServiceZipCode resolveServiceZipCode(Address address, Map<String, ServiceZipCode> zipcodes) {
        if (address == null) {
            return null;
        }
        return zipcodes.get(normalizeZipcode(address.getZipcode()));
    }

    private double calculateCoordinateDelta(ServiceZipCode pickupZip, ServiceZipCode deliveryZip) {
        return Math.sqrt(
                Math.pow(Math.abs(pickupZip.getLatitude() - deliveryZip.getLatitude()), 2)
                        + Math.pow(Math.abs(pickupZip.getLongitude() - deliveryZip.getLongitude()), 2)
        );
    }

    private double calculateDistanceKm(ServiceZipCode pickupZip, ServiceZipCode deliveryZip) {
        return calculateCoordinateDelta(pickupZip, deliveryZip) / LONGHAUL_DISTANCE_FACTOR;
    }

    private double roundOneDecimal(double value) {
        return Math.round(value * 10.0d) / 10.0d;
    }

    private Long resolveCostCenterId(Job job, List<Tour> tours) {
        if (job.getCostCenterPayerId() != null && job.getCostCenterPayerId() > 0) {
            return job.getCostCenterPayerId();
        }
        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 formatStreet(Address address, Tour tour) {
        if (address == null) {
            return "";
        }
        return firstNonBlank(
                joinNonBlank(address.getStreet(), tour != null ? tour.getHsno() : null),
                address.getStreet(),
                ""
        );
    }

    private String joinNonBlank(String left, String right) {
        String leftValue = blank(left);
        String rightValue = blank(right);
        if (leftValue.isBlank()) {
            return rightValue;
        }
        if (rightValue.isBlank()) {
            return leftValue;
        }
        return leftValue + " " + rightValue;
    }

    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 trimmed = zipcode.trim();
        return trimmed.isBlank() ? null : trimmed;
    }

    private LocatingData toLocatingData(Courier courier) {
        return courier != null
                ? new LocatingData(
                courier.getGpsLatitude(),
                courier.getGpsLongitude(),
                courier.getGpsTime(),
                courier.getGpsType(),
                courier.getLocationZipcode(),
                courier.getAvailableTime()
        )
                : null;
    }

    private LocatingData toLocatingData(LonghaulRemoteDbService.RemoteCourierStatus courierStatus) {
        return courierStatus != null
                ? new LocatingData(
                courierStatus.latitude(),
                courierStatus.longitude(),
                courierStatus.gpsTime(),
                courierStatus.gpsType(),
                courierStatus.locationZipcode(),
                courierStatus.availableTime()
        )
                : null;
    }

    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 record LonghaulConfig(boolean mediationActive, int minimumDistanceKm) {
    }

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

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

    private record LocatingData(Double latitude,
                                Double longitude,
                                LocalDateTime gpsTime,
                                Integer gpsType,
                                String locationZipcode,
                                LocalDateTime availableTime) {
    }
}
