package de.votian.services.service;

import de.votian.services.dto.JobPricingResultDto;
import de.votian.services.dto.TourDetailDto;
import de.votian.services.entity.*;
import de.votian.services.repository.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.time.LocalDateTime;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

@Service
@Transactional
public class JobService {

    private static final int LEGACY_COURIER_SERVICE_PRICE_META_SORT = 3;
    private static final String JOB_OBJECT_TYPE = "jb";
    private static final String LEGACY_COURIER_SERVICE_PRICE_FIELD = "jb_cr_serviceprice";
    private static final String INVOICE_TEXT_DIRTY_FIELD = "jb_invtext_dirty";
    private static final String AUTOMATIC_STREET_SERVICE_PRICE_FIELD = "jb_serviceprice_street_auto";
    private static final String LEGACY_COURIER_PRICE_REMARK_LABEL = "Frachtverguetung:";
    private static final DateTimeFormatter LEGACY_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private final JobRepository jobRepository;
    private final TourRepository tourRepository;
    private final TourArticleRepository tourArticleRepository;
    private final AddressRepository addressRepository;
    private final JobPriceRepository jobPriceRepository;
    private final VehicleDispositionRepository vehicleDispositionRepository;
    private final GenericDataContainerRepository genericDataContainerRepository;
    private final JobPricingService jobPricingService;
    private final JobTourServiceSyncService jobTourServiceSyncService;
    private final JobTourServiceWriteService jobTourServiceWriteService;

    public JobService(JobRepository jobRepository, TourRepository tourRepository,
                      TourArticleRepository tourArticleRepository, AddressRepository addressRepository,
                      JobPriceRepository jobPriceRepository,
                      VehicleDispositionRepository vehicleDispositionRepository,
                      GenericDataContainerRepository genericDataContainerRepository,
                      JobPricingService jobPricingService,
                      JobTourServiceSyncService jobTourServiceSyncService,
                      JobTourServiceWriteService jobTourServiceWriteService) {
        this.jobRepository = jobRepository;
        this.tourRepository = tourRepository;
        this.tourArticleRepository = tourArticleRepository;
        this.addressRepository = addressRepository;
        this.jobPriceRepository = jobPriceRepository;
        this.vehicleDispositionRepository = vehicleDispositionRepository;
        this.genericDataContainerRepository = genericDataContainerRepository;
        this.jobPricingService = jobPricingService;
        this.jobTourServiceSyncService = jobTourServiceSyncService;
        this.jobTourServiceWriteService = jobTourServiceWriteService;
    }

    public Optional<Job> findById(Long id) {
        return jobRepository.findById(Objects.requireNonNull(id));
    }

    public List<Job> findByHeadquarters(Long hqId) {
        return jobRepository.findByHeadquartersId(hqId);
    }

    public List<Job> findByOrderTimeBetween(LocalDateTime from, LocalDateTime to, Long hqId) {
        return jobRepository.findByOrderTimeBetweenAndHq(from, to, hqId);
    }

    public List<Job> findByCommissionNoAndCustomer(String commNo, Long customerId) {
        return jobRepository.findByCommissionNoAndCustomer(commNo, customerId);
    }

    public List<Job> findByPayerCostCenters(List<Long> cscIds) {
        return jobRepository.findByPayerCostCenters(cscIds);
    }

    public List<Job> findByCourierId(Long courierId) {
        return jobRepository.findByCourierId(courierId);
    }

    public List<Job> findTodaysJobs() {
        return jobRepository.findTodaysJobs();
    }

    @Transactional
    public Job createJob(Job job, List<Tour> tours, List<TourArticle> articles) {
        return createJob(job, tours, articles, List.of());
    }

    @Transactional
    public Job createJob(Job job,
                         List<Tour> tours,
                         List<TourArticle> articles,
                         List<JobPrice> prices) {
        if (job.getOrderTime() == null) {
            job.setOrderTime(LocalDateTime.now());
        }
        Job savedJob = jobRepository.save(job);

        for (Tour tour : tours) {
            tour.setJobId(savedJob.getId());
            // Resolve or create address
            if (tour.getAddressId() == null) {
                Address addr = new Address();
                addr.setCountry("DE");
                Address savedAddr = addressRepository.save(addr);
                tour.setAddressId(savedAddr.getId());
            }
            tourRepository.save(tour);
        }

        for (TourArticle article : articles) {
            article.setJobId(savedJob.getId());
            tourArticleRepository.save(article);
        }

        applyAutomaticStreetServicePriceOnCreate(savedJob);

        if (prices != null && !prices.isEmpty()) {
            replaceJobPrices(savedJob.getId(), prices);
            return jobRepository.findById(Objects.requireNonNull(savedJob.getId())).orElse(savedJob);
        }

        JobPricingResultDto initialBasePricePricing = calculateInitialBasePricePricing(savedJob);
        if (initialBasePricePricing != null && initialBasePricePricing.getJob() != null) {
            jobTourServiceWriteService.createBasePriceRows(savedJob, initialBasePricePricing);
            return savedJob;
        }

        jobTourServiceWriteService.createBasePriceRows(savedJob);
        return savedJob;
    }

    @Transactional
    public Job updateJob(Long jobId, Job updates) {
        Job job = jobRepository.findById(Objects.requireNonNull(jobId))
                .orElseThrow(() -> new RuntimeException("Job not found: " + jobId));

        if (updates.getStatus() != null) job.setStatus(updates.getStatus());
        if (updates.getOrderTime() != null) job.setOrderTime(updates.getOrderTime());
        if (updates.getBookTime() != null) job.setBookTime(updates.getBookTime());
        if (updates.getTakeTime() != null) job.setTakeTime(updates.getTakeTime());
        if (updates.getCourierId() != null) job.setCourierId(updates.getCourierId());
        if (updates.getCourierOrderId() != null) job.setCourierOrderId(updates.getCourierOrderId());
        if (updates.getCourierSid() != null) job.setCourierSid(updates.getCourierSid());
        if (updates.getWarningTime() != null) job.setWarningTime(updates.getWarningTime());
        if (updates.getCostCenterPayerId() != null) job.setCostCenterPayerId(updates.getCostCenterPayerId());
        if (updates.getCostCenterPayerCashId() != null) job.setCostCenterPayerCashId(updates.getCostCenterPayerCashId());
        if (updates.getCostCenterRelatedId() != null) job.setCostCenterRelatedId(updates.getCostCenterRelatedId());
        if (updates.getHeadquartersId() != null) job.setHeadquartersId(updates.getHeadquartersId());
        if (updates.getHeadquartersDispoId() != null) job.setHeadquartersDispoId(updates.getHeadquartersDispoId());
        if (updates.getTotalPrice() != null) job.setTotalPrice(updates.getTotalPrice());
        if (updates.getSubtotalPrice() != null) job.setSubtotalPrice(updates.getSubtotalPrice());
        if (updates.getCourierPrice() != null) job.setCourierPrice(updates.getCourierPrice());
        if (updates.getCourierSubprice() != null) job.setCourierSubprice(updates.getCourierSubprice());
        if (updates.getFixPrice() != null) job.setFixPrice(updates.getFixPrice());
        if (updates.getServicePrice() != null) job.setServicePrice(updates.getServicePrice());
        if (updates.getMarkup() != null) job.setMarkup(updates.getMarkup());
        if (updates.getTourName() != null) job.setTourName(updates.getTourName());
        if (updates.getTourData() != null) job.setTourData(updates.getTourData());
        if (updates.getType() != null) job.setType(updates.getType());
        if (updates.getReserve() != null) job.setReserve(updates.getReserve());
        if (updates.getPermanent() != null) job.setPermanent(updates.getPermanent());
        if (updates.getPermanent2() != null) job.setPermanent2(updates.getPermanent2());
        if (updates.getPermanentEndDate() != null) job.setPermanentEndDate(updates.getPermanentEndDate());
        if (updates.getVehicleTypeId() != null) job.setVehicleTypeId(updates.getVehicleTypeId());
        if (updates.getFinishTime() != null) job.setFinishTime(updates.getFinishTime());
        if (updates.getStorno() != null) job.setStorno(updates.getStorno());
        if (updates.getInvoiceText() != null) job.setInvoiceText(updates.getInvoiceText());
        if (updates.getIncomplete() != null) job.setIncomplete(updates.getIncomplete());
        if (updates.getExportTime() != null) job.setExportTime(updates.getExportTime());
        if (updates.getFreeText1() != null) job.setFreeText1(updates.getFreeText1());
        if (updates.getFreeText2() != null) job.setFreeText2(updates.getFreeText2());
        if (updates.getCourierFilter() != null) job.setCourierFilter(updates.getCourierFilter());
        if (updates.getCourierRemark() != null) job.setCourierRemark(updates.getCourierRemark());
        if (updates.getDispoInfo() != null) job.setDispoInfo(updates.getDispoInfo());
        if (updates.getGlobalJob() != null) job.setGlobalJob(updates.getGlobalJob());
        if (updates.getAutoranking() != null) job.setAutoranking(updates.getAutoranking());
        if (updates.getWeight() != null) job.setWeight(updates.getWeight());
        if (updates.getCourierVehicleLength() != null) job.setCourierVehicleLength(updates.getCourierVehicleLength());
        if (updates.getCourierVehicleWidth() != null) job.setCourierVehicleWidth(updates.getCourierVehicleWidth());
        if (updates.getCourierVehicleHeight() != null) job.setCourierVehicleHeight(updates.getCourierVehicleHeight());
        if (updates.getCourierVehiclePosition() != null) job.setCourierVehiclePosition(updates.getCourierVehiclePosition());
        if (updates.getLonghaul() != null) job.setLonghaul(updates.getLonghaul());
        if (updates.getService() != null) job.setService(updates.getService());
        if (updates.getCopyPermanentId() != null) job.setCopyPermanentId(updates.getCopyPermanentId());

        return jobRepository.save(Objects.requireNonNull(job));
    }

    @Transactional
    public void cancelJob(Long jobId) {
        jobRepository.findById(Objects.requireNonNull(jobId)).ifPresent(job -> {
            job.setStatus(10);
            job.setStorno("1");
            jobRepository.save(job);
        });
    }

    public void clearTourNames(List<Long> jobIds) {
        for (Long jobId : jobIds) {
            jobRepository.findById(Objects.requireNonNull(jobId)).ifPresent(job -> {
                job.setTourName("");
                jobRepository.save(job);
            });
        }
    }

    public void markExported(List<Long> jobIds) {
        LocalDateTime now = LocalDateTime.now();
        for (Long jobId : jobIds) {
            jobRepository.findById(Objects.requireNonNull(jobId)).ifPresent(job -> {
                job.setExportTime(now);
                jobRepository.save(job);
            });
        }
    }

    // Tour operations
    public List<Tour> getTours(Long jobId) {
        return tourRepository.findByJobIdOrderBySort(jobId);
    }

    public Tour saveTour(Tour tour) {
        return tourRepository.save(Objects.requireNonNull(tour));
    }

    // Tour Article operations
    public List<TourArticle> getTourArticles(Long jobId) {
        return tourArticleRepository.findByJobId(jobId);
    }

    public TourArticle saveTourArticle(TourArticle article) {
        return tourArticleRepository.save(Objects.requireNonNull(article));
    }

    @Transactional
    public void replaceTourArticles(Long jobId, List<TourArticle> articles) {
        tourArticleRepository.deleteByJobId(jobId);
        for (TourArticle article : articles) {
            article.setId(null);
            article.setJobId(jobId);
            tourArticleRepository.save(article);
        }
    }

    public BigDecimal getTotalWeight(Long jobId) {
        return tourArticleRepository.sumWeightByJobId(jobId);
    }

    public Integer getTotalPackingPieces(Long jobId) {
        return tourArticleRepository.sumPackingPiecesByJobId(jobId);
    }

    // Price operations
    public List<JobPrice> getJobPrices(Long jobId) {
        List<JobPrice> prices = new ArrayList<>(jobPriceRepository.findByJobIdOrderByMetaSortAsc(jobId));
        boolean hasLegacyCourierServiceRow = prices.stream()
                .anyMatch(price -> Objects.equals(price.getMetaSort(), LEGACY_COURIER_SERVICE_PRICE_META_SORT));
        if (!hasLegacyCourierServiceRow) {
            loadLegacyCourierServicePrice(jobId).ifPresent(price -> {
                JobPrice legacyRow = new JobPrice();
                legacyRow.setJobId(jobId);
                legacyRow.setMetaSort(LEGACY_COURIER_SERVICE_PRICE_META_SORT);
                legacyRow.setPrice(price);
                prices.add(legacyRow);
            });
            prices.sort(Comparator.comparing(JobPrice::getMetaSort, Comparator.nullsLast(Integer::compareTo)));
        }
        return prices;
    }

    public void saveJobPrice(JobPrice price) {
        jobPriceRepository.save(Objects.requireNonNull(price));
        if (price != null && Objects.equals(price.getMetaSort(), LEGACY_COURIER_SERVICE_PRICE_META_SORT)) {
            syncLegacyCourierServicePrice(price.getJobId(), price.getPrice());
        }
        syncPersistedPricing(price != null ? price.getJobId() : null);
    }

    @Transactional
    public void replaceJobPrices(Long jobId, List<JobPrice> prices) {
        storeJobPrices(jobId, prices);
        syncPersistedPricing(jobId);
    }

    public void deleteJobPrice(Long jobId, Integer metaSort) {
        jobPriceRepository.deleteByJobIdAndMetaSort(jobId, metaSort);
        if (Objects.equals(metaSort, LEGACY_COURIER_SERVICE_PRICE_META_SORT)) {
            syncLegacyCourierServicePrice(jobId, null);
        }
        syncPersistedPricing(jobId);
    }

    // Disposition
    public Optional<VehicleDisposition> getVehicleDisposition(Long jobId) {
        return vehicleDispositionRepository.findByJobId(jobId);
    }

    private Optional<BigDecimal> loadLegacyCourierServicePrice(Long jobId) {
        return genericDataContainerRepository.findByTypeAndIdAndField(
                        JOB_OBJECT_TYPE,
                        jobId,
                        LEGACY_COURIER_SERVICE_PRICE_FIELD
                )
                .map(GenericDataContainer::getContent)
                .map(this::parseLegacyMoney);
    }

    private BigDecimal parseLegacyMoney(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return new BigDecimal(value.trim().replace(",", "."));
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private void syncLegacyCourierServicePrice(Long jobId, BigDecimal price) {
        if (jobId == null) {
            return;
        }
        Optional<GenericDataContainer> existing = genericDataContainerRepository.findByTypeAndIdAndField(
                JOB_OBJECT_TYPE,
                jobId,
                LEGACY_COURIER_SERVICE_PRICE_FIELD
        );
        if (price == null || price.compareTo(BigDecimal.ZERO) == 0) {
            existing.ifPresent(genericDataContainerRepository::delete);
            return;
        }
        GenericDataContainer entry = existing.orElseGet(GenericDataContainer::new);
        entry.setObjectType(JOB_OBJECT_TYPE);
        entry.setObjectId(jobId);
        entry.setFieldName(LEGACY_COURIER_SERVICE_PRICE_FIELD);
        entry.setContent(price.toPlainString());
        entry.setContext("");
        genericDataContainerRepository.save(entry);
    }

    private void syncAutomaticStreetServicePrice(Long jobId, BigDecimal price) {
        if (jobId == null) {
            return;
        }
        Optional<GenericDataContainer> existing = genericDataContainerRepository.findByTypeAndIdAndField(
                JOB_OBJECT_TYPE,
                jobId,
                AUTOMATIC_STREET_SERVICE_PRICE_FIELD
        );
        if (!hasAmount(price)) {
            existing.ifPresent(genericDataContainerRepository::delete);
            return;
        }
        GenericDataContainer entry = existing.orElseGet(GenericDataContainer::new);
        entry.setObjectType(JOB_OBJECT_TYPE);
        entry.setObjectId(jobId);
        entry.setFieldName(AUTOMATIC_STREET_SERVICE_PRICE_FIELD);
        entry.setContent(price.setScale(2, RoundingMode.HALF_UP).toPlainString());
        entry.setContext("");
        genericDataContainerRepository.save(entry);
    }

    private void storeJobPrices(Long jobId, List<JobPrice> prices) {
        if (jobId == null) {
            return;
        }
        jobPriceRepository.deleteByJobId(jobId);
        List<JobPrice> safePrices = prices != null ? prices : List.of();
        for (JobPrice input : safePrices) {
            if (input == null || input.getMetaSort() == null) {
                continue;
            }
            JobPrice row = new JobPrice();
            row.setJobId(jobId);
            row.setMetaSort(input.getMetaSort());
            row.setPrice(input.getPrice());
            row.setRemark(input.getRemark());
            row.setReference(input.getReference());
            jobPriceRepository.save(row);
        }
        BigDecimal legacyCourierServicePrice = safePrices.stream()
                .filter(price -> price != null && Objects.equals(price.getMetaSort(), LEGACY_COURIER_SERVICE_PRICE_META_SORT))
                .map(JobPrice::getPrice)
                .findFirst()
                .orElse(null);
        syncLegacyCourierServicePrice(jobId, legacyCourierServicePrice);
    }

    private void syncPersistedPricing(Long jobId) {
        if (jobId == null) {
            return;
        }
        Job job = jobRepository.findById(jobId).orElse(null);
        if (job == null) {
            return;
        }
        List<JobPrice> prices = getJobPrices(jobId);
        List<TourDetailDto> tours = loadPricingTours(jobId);
        JobPricingResultDto result = calculatePersistedCourierPricing(job, prices, tours);
        if (result == null || result.getJob() == null) {
            return;
        }

        BigDecimal previousMarkup = job.getMarkup();
        Long headquartersId = job.getHeadquartersId() != null ? job.getHeadquartersId() : recalculatedHeadquartersId(job, result.getJob());
        Job recalculated = result.getJob();
        boolean invoiceTextDirty = hasInvoiceRelevantPricingChange(job, recalculated);
        job.setMarkup(recalculated.getMarkup());
        job.setSubtotalPrice(recalculated.getSubtotalPrice());
        job.setTotalPrice(recalculated.getTotalPrice());
        job.setCourierPrice(recalculated.getCourierPrice());
        job.setCourierSubprice(recalculated.getCourierSubprice());
        jobRepository.save(job);
        storeJobPrices(jobId, result.getPrices());
        jobTourServiceSyncService.syncMarkupAdjustedPrices(jobId, headquartersId, previousMarkup, recalculated.getMarkup());
        jobTourServiceWriteService.syncBasePriceRows(job, result);
        syncPersistedCourierPriceRemark(jobId, recalculated.getCourierPrice());
        syncInvoiceTextDirtyFlag(jobId, invoiceTextDirty);
    }

    private JobPricingResultDto calculatePersistedCourierPricing(Job job,
                                                                 List<JobPrice> prices,
                                                                 List<TourDetailDto> tours) {
        JobPricingResultDto calculated = jobPricingService.calculateDraft(copyJob(job), prices, tours, false);
        if (shouldRetryWithManualCourierTotalOverride(job, prices, calculated)) {
            return jobPricingService.calculateDraft(copyJob(job), prices, tours, true);
        }
        return calculated;
    }

    private JobPricingResultDto calculateInitialBasePricePricing(Job job) {
        if (job == null || (!hasAmount(job.getFixPrice()) && !hasAmount(job.getServicePrice()))) {
            return null;
        }
        return jobPricingService.calculateDraft(copyJob(job), List.of(), List.of(), false);
    }

    private boolean shouldRetryWithManualCourierTotalOverride(Job job,
                                                              List<JobPrice> prices,
                                                              JobPricingResultDto calculated) {
        if (job == null
                || calculated == null
                || calculated.getJob() == null
                || !hasAmount(job.getCourierPrice())
                || hasExplicitPrice(prices, JobPricingService.JB_CR_FIXPRICE)
                || job.getCourierSubprice() != null
                || hasExplicitPrice(prices, JobPricingService.JB_CR_SUBTOTAL)) {
            return false;
        }
        return !sameAmount(job.getCourierPrice(), calculated.getJob().getCourierPrice());
    }

    private List<TourDetailDto> loadPricingTours(Long jobId) {
        List<Tour> tours = tourRepository.findByJobIdOrderBySort(jobId);
        if (tours.isEmpty()) {
            return List.of();
        }
        Map<Long, Address> addressById = new LinkedHashMap<>();
        for (Tour tour : tours) {
            if (tour == null || tour.getAddressId() == null || addressById.containsKey(tour.getAddressId())) {
                continue;
            }
            addressRepository.findById(Objects.requireNonNull(tour.getAddressId())).ifPresent(address -> addressById.put(tour.getAddressId(), address));
        }
        List<TourDetailDto> result = new ArrayList<>();
        for (Tour tour : tours) {
            if (tour == null) {
                continue;
            }
            Address address = tour.getAddressId() != null ? addressById.get(tour.getAddressId()) : null;
            TourDetailDto dto = new TourDetailDto();
            dto.setId(tour.getId());
            dto.setJobId(tour.getJobId());
            dto.setSort(tour.getSort());
            dto.setComp(tour.getComp());
            dto.setPerson(tour.getPerson());
            dto.setAddressId(tour.getAddressId());
            dto.setStreet(address != null ? address.getStreet() : null);
            dto.setZipcode(address != null ? address.getZipcode() : null);
            dto.setCity(address != null ? address.getCity() : null);
            dto.setHsno(tour.getHsno());
            dto.setCommissionNo(tour.getCommissionNo());
            dto.setStatus(tour.getStatus());
            dto.setRemark(tour.getRemark());
            dto.setPhone(tour.getPhone());
            dto.setCostCenterId(tour.getCostCenterId());
            result.add(dto);
        }
        return result;
    }

    private boolean hasExplicitPrice(List<JobPrice> prices, int metaSort) {
        return prices != null && prices.stream().anyMatch(price -> price != null && Objects.equals(price.getMetaSort(), metaSort));
    }

    private void applyAutomaticStreetServicePriceOnCreate(Job job) {
        if (job == null || job.getId() == null) {
            return;
        }
        BigDecimal automaticStreetServicePrice = jobPricingService.calculateAutomaticStreetServicePrice(job, loadPricingTours(job.getId()));
        if (!hasAmount(automaticStreetServicePrice)) {
            syncAutomaticStreetServicePrice(job.getId(), null);
            return;
        }
        job.setServicePrice(money(job.getServicePrice()).add(automaticStreetServicePrice).setScale(2, RoundingMode.HALF_UP));
        job.setSubtotalPrice(money(job.getSubtotalPrice()).add(automaticStreetServicePrice).setScale(2, RoundingMode.HALF_UP));
        job.setTotalPrice(money(job.getTotalPrice()).add(automaticStreetServicePrice).setScale(2, RoundingMode.HALF_UP));
        jobRepository.save(job);
        syncAutomaticStreetServicePrice(job.getId(), automaticStreetServicePrice);
    }

    private Long recalculatedHeadquartersId(Job existing, Job recalculated) {
        if (existing != null && existing.getHeadquartersId() != null) {
            return existing.getHeadquartersId();
        }
        return recalculated != null ? recalculated.getHeadquartersId() : null;
    }

    private boolean hasInvoiceRelevantPricingChange(Job existing, Job recalculated) {
        if (existing == null || recalculated == null) {
            return false;
        }
        return !sameAmount(existing.getMarkup(), recalculated.getMarkup())
                || !sameAmount(existing.getSubtotalPrice(), recalculated.getSubtotalPrice())
                || !sameAmount(existing.getTotalPrice(), recalculated.getTotalPrice());
    }

    private void syncInvoiceTextDirtyFlag(Long jobId, boolean invoiceTextDirty) {
        if (!invoiceTextDirty || jobId == null) {
            return;
        }
        Optional<GenericDataContainer> existing = genericDataContainerRepository.findByTypeAndIdAndField(
                JOB_OBJECT_TYPE,
                jobId,
                INVOICE_TEXT_DIRTY_FIELD
        );
        GenericDataContainer entry = existing.orElseGet(GenericDataContainer::new);
        entry.setObjectType(JOB_OBJECT_TYPE);
        entry.setObjectId(jobId);
        entry.setFieldName(INVOICE_TEXT_DIRTY_FIELD);
        entry.setContent("1");
        entry.setContext(LocalDateTime.now().withNano(0).format(LEGACY_TIMESTAMP_FORMAT));
        genericDataContainerRepository.save(entry);
    }

    private void syncPersistedCourierPriceRemark(Long jobId, BigDecimal courierPrice) {
        if (jobId == null) {
            return;
        }
        Tour pickupTour = tourRepository.findByJobIdAndSort(jobId, 1).orElse(null);
        if (pickupTour == null) {
            return;
        }
        String updatedRemark = replaceLegacyCourierPriceRemark(pickupTour.getRemark(), courierPrice);
        if (Objects.equals(updatedRemark, pickupTour.getRemark())) {
            return;
        }
        pickupTour.setRemark(updatedRemark);
        tourRepository.save(pickupTour);
    }

    private String replaceLegacyCourierPriceRemark(String remark, BigDecimal courierPrice) {
        if (remark == null) {
            return null;
        }
        int blockStart = remark.indexOf("[[");
        if (blockStart < 0) {
            return remark;
        }
        int blockEnd = remark.indexOf("]]", blockStart + 2);
        if (blockEnd < 0) {
            return remark;
        }
        int priceLabelStart = remark.indexOf(LEGACY_COURIER_PRICE_REMARK_LABEL, blockStart);
        if (priceLabelStart < 0 || priceLabelStart >= blockEnd) {
            return remark;
        }
        int eurEnd = remark.indexOf("EUR", priceLabelStart);
        if (eurEnd < 0 || eurEnd > blockEnd) {
            return remark;
        }
        String replacement = LEGACY_COURIER_PRICE_REMARK_LABEL + " " + formatLegacyMoney(courierPrice) + " EUR";
        return remark.substring(0, priceLabelStart) + replacement + remark.substring(eurEnd + 3);
    }

    private String formatLegacyMoney(BigDecimal amount) {
        BigDecimal normalized = amount != null ? amount : BigDecimal.ZERO;
        return normalized.setScale(2, RoundingMode.HALF_UP).toPlainString().replace('.', ',');
    }

    private BigDecimal money(BigDecimal value) {
        return (value != null ? value : BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
    }

    private boolean hasAmount(BigDecimal value) {
        return value != null && value.compareTo(BigDecimal.ZERO) != 0;
    }

    private boolean sameAmount(BigDecimal left, BigDecimal right) {
        if (left == null && right == null) {
            return true;
        }
        if (left == null || right == null) {
            return false;
        }
        return left.compareTo(right) == 0;
    }

    private Job copyJob(Job source) {
        Job copy = new Job();
        copy.setId(source.getId());
        copy.setStatus(source.getStatus());
        copy.setOrderTime(source.getOrderTime());
        copy.setFinishTime(source.getFinishTime());
        copy.setBookTime(source.getBookTime());
        copy.setTakeTime(source.getTakeTime());
        copy.setCourierId(source.getCourierId());
        copy.setCourierSid(source.getCourierSid());
        copy.setCostCenterPayerId(source.getCostCenterPayerId());
        copy.setCostCenterPayerCashId(source.getCostCenterPayerCashId());
        copy.setCostCenterRelatedId(source.getCostCenterRelatedId());
        copy.setHeadquartersId(source.getHeadquartersId());
        copy.setHeadquartersDispoId(source.getHeadquartersDispoId());
        copy.setTotalPrice(source.getTotalPrice());
        copy.setSubtotalPrice(source.getSubtotalPrice());
        copy.setCourierPrice(source.getCourierPrice());
        copy.setCourierSubprice(source.getCourierSubprice());
        copy.setFixPrice(source.getFixPrice());
        copy.setServicePrice(source.getServicePrice());
        copy.setMarkup(source.getMarkup());
        copy.setTourName(source.getTourName());
        copy.setTourData(source.getTourData());
        copy.setPermanent(source.getPermanent());
        copy.setPermanent2(source.getPermanent2());
        copy.setPermanentEndDate(source.getPermanentEndDate());
        copy.setStorno(source.getStorno());
        copy.setInvoiceText(source.getInvoiceText());
        copy.setIncomplete(source.getIncomplete());
        copy.setExportTime(source.getExportTime());
        copy.setFreeText2(source.getFreeText2());
        copy.setLonghaul(source.getLonghaul());
        copy.setService(source.getService());
        copy.setVehicleTypeId(source.getVehicleTypeId());
        copy.setCopyPermanentId(source.getCopyPermanentId());
        return copy;
    }
}
