package de.votian.services.service;

import de.votian.services.entity.Address;
import de.votian.services.entity.CostCenter;
import de.votian.services.entity.GenericDataContainer;
import de.votian.services.entity.Job;
import de.votian.services.entity.MetaType;
import de.votian.services.entity.Tour;
import de.votian.services.repository.AddressRepository;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.GenericDataContainerRepository;
import de.votian.services.repository.MetaTypeRepository;
import de.votian.services.repository.TourRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

@Service
public class LegacyXmlOrderDataIntegrationService {

    private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private final LegacyXmlCustomerAuthService authService;
    private final GenericDataContainerRepository genericDataContainerRepository;
    private final CostCenterRepository costCenterRepository;
    private final MetaTypeRepository metaTypeRepository;
    private final AddressRepository addressRepository;
    private final TourRepository tourRepository;
    private final JobService jobService;
    private final JobTourServiceWriteService jobTourServiceWriteService;
    private final JdbcTemplate jdbcTemplate;

    public LegacyXmlOrderDataIntegrationService(LegacyXmlCustomerAuthService authService,
                                                GenericDataContainerRepository genericDataContainerRepository,
                                                CostCenterRepository costCenterRepository,
                                                MetaTypeRepository metaTypeRepository,
                                                AddressRepository addressRepository,
                                                TourRepository tourRepository,
                                                JobService jobService,
                                                JobTourServiceWriteService jobTourServiceWriteService,
                                                JdbcTemplate jdbcTemplate) {
        this.authService = authService;
        this.genericDataContainerRepository = genericDataContainerRepository;
        this.costCenterRepository = costCenterRepository;
        this.metaTypeRepository = metaTypeRepository;
        this.addressRepository = addressRepository;
        this.tourRepository = tourRepository;
        this.jobService = jobService;
        this.jobTourServiceWriteService = jobTourServiceWriteService;
        this.jdbcTemplate = jdbcTemplate;
    }

    public String handleOrderDataRequest(String xmlRequest) {
        try {
            Element request = parseRequest(xmlRequest);
            boolean debug = isDebugEnabled(request);
            LegacyXmlCustomerAuthService.AuthContext auth = authenticateCustomer(request);
            String operation = upper(text(request, "operation"), "GET");
            List<String> orderNumbers = splitList(text(request, "order_numbers"));
            List<String> operationalFlags = splitList(text(request, "is_operative"));
            String payload = "SET".equals(operation)
                    ? handleSetRequest(request, auth, orderNumbers, operationalFlags)
                    : handleGetRequest(request, auth, orderNumbers, operationalFlags);
            return ok(payload, debug ? "order_data: " + orderNumbers.size() : "");
        } catch (LegacyXmlRequestException ex) {
            return error(ex, null);
        } catch (Exception ex) {
            return error(new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen."), null);
        }
    }

    private String handleGetRequest(Element request,
                                    LegacyXmlCustomerAuthService.AuthContext auth,
                                    List<String> orderNumbers,
                                    List<String> operationalFlags) {
        List<String> fieldNames = splitList(text(request, "list_fields"));
        StringBuilder payload = new StringBuilder("<order_data>");
        for (int i = 0; i < orderNumbers.size(); i++) {
            String requestedOrderNo = orderNumbers.get(i);
            Long requestedJobId = parseLong(requestedOrderNo);
            boolean orderIsOperational = operationalFlags.isEmpty()
                    || i >= operationalFlags.size()
                    || !isFalsey(operationalFlags.get(i));
            Long operativeJobId = resolveOperativeJobId(requestedJobId, orderIsOperational);

            payload.append("<order>")
                    .append(tag("order_no", requestedOrderNo))
                    .append(tag("order_op_no", operativeJobId == null ? "" : operativeJobId.toString()));

            if (operativeJobId == null) {
                payload.append(errorField("NO_OPERATIVE_TOUR_FOUND"));
            } else {
                Long payerCustomerId = resolvePayerCustomerId(requestedJobId);
                if (payerCustomerId == null || !payerCustomerId.equals(auth.customer().getId())) {
                    payload.append(errorField("NOT_AUTHENTICATED"));
                } else {
                    payload.append(buildFieldsXml(operativeJobId, fieldNames));
                }
            }

            payload.append("</order>");
        }
        payload.append("</order_data>");
        return payload.toString();
    }

    private String handleSetRequest(Element request,
                                    LegacyXmlCustomerAuthService.AuthContext auth,
                                    List<String> orderNumbers,
                                    List<String> operationalFlags) {
        List<String> stateValues = splitWriteList(text(request, "list_jb_status"));
        List<String> courierValues = splitWriteList(text(request, "list_cr_id"));
        List<String> vehicleValues = splitWriteList(text(request, "list_crvh_id"));
        List<String> invoiceValues = splitWriteList(text(request, "list_inv_no"));
        Element jobElement = child(request, "job");
        Element stationsElement = child(request, "stations");

        StringBuilder payload = new StringBuilder("<order_data>");
        for (int i = 0; i < orderNumbers.size(); i++) {
            String requestedOrderNo = orderNumbers.get(i);
            Long requestedJobId = parseLong(requestedOrderNo);
            boolean orderIsOperational = operationalFlags.isEmpty()
                    || i >= operationalFlags.size()
                    || !isFalsey(operationalFlags.get(i));
            boolean derivedOperativeJob = !orderIsOperational;
            Long operativeJobId = resolveOperativeJobId(requestedJobId, orderIsOperational);

            payload.append("<order>")
                    .append(tag("order_no", requestedOrderNo))
                    .append(tag("order_op_no", operativeJobId == null ? "" : operativeJobId.toString()));

            if (operativeJobId == null) {
                payload.append(buildSetResult("101", derivedOperativeJob
                        ? "Der operative Auftrag ist nicht existent!"
                        : "Der Auftrag ist nicht existent!"));
            } else {
                Long payerCustomerId = resolvePayerCustomerId(requestedJobId);
                if (payerCustomerId == null || !payerCustomerId.equals(auth.customer().getId())) {
                    payload.append(buildSetResult("102", "Der Auftrag passt nicht zu den Authentifizierungsdaten!"));
                } else {
                    payload.append(handleSetOrder(
                            operativeJobId,
                            i,
                            stateValues,
                            courierValues,
                            vehicleValues,
                            invoiceValues,
                            i == 0 ? jobElement : null,
                            i == 0 ? stationsElement : null
                    ));
                }
            }

            payload.append("</order>");
        }
        payload.append("</order_data>");
        return payload.toString();
    }

    private String handleSetOrder(Long operativeJobId,
                                  int index,
                                  List<String> stateValues,
                                  List<String> courierValues,
                                  List<String> vehicleValues,
                                  List<String> invoiceValues,
                                  Element jobElement,
                                  Element stationsElement) {
        Map<String, Object> jobRow = loadJobRow(operativeJobId);
        if (jobRow.isEmpty()) {
            return buildSetResult("101", "Der Auftrag ist nicht existent!");
        }

        String opCode = "0";
        String opMsg = "OK";

        Long courierId = parseLong(valueAt(courierValues, index));
        Long courierVehicleId = parseLong(valueAt(vehicleValues, index));
        if (courierId != null && courierVehicleId != null) {
            CarrierAssignment carrierAssignment = resolveCarrierAssignment(courierId, courierVehicleId);
            if (carrierAssignment == null || !courierId.equals(carrierAssignment.courierId())) {
                opCode = "112";
                opMsg = "Das angegebene Fahrzeug scheint dem Transporteur nicht zugeordnet!";
            } else if (isBlank(carrierAssignment.courierEid()) || isBlank(carrierAssignment.vehicleSid())) {
                opCode = "113";
                opMsg = "Der spezifizierte Transporteur oder das spezifizierte Fahrzeug scheinen nicht zu existieren!";
            } else {
                assignCarrier(operativeJobId, courierId, carrierAssignment.vehicleSid());
                jobRow.put("cr_id", courierId);
                jobRow.put("cr_id_order", courierId);
                jobRow.put("cr_sid", carrierAssignment.vehicleSid());
            }
        }

        if ("0".equals(opCode) && !stateValues.isEmpty()) {
            String newStateValue = valueAt(stateValues, index);
            Integer newState = asInteger(newStateValue);
            if (isBlank(newStateValue)) {
                opCode = "103";
                opMsg = "Es wurde fuer den Auftrag kein neuer Status angefragt!";
            } else if (newState == null) {
                opCode = "104";
                opMsg = "Kein gueltiger Status fuer eine Uebernahme spezifiziert!";
            } else {
                Integer currentStatus = asInteger(value(jobRow, "jb_status"));
                switch (newState) {
                    case 0 -> {
                        opCode = "199";
                        opMsg = "Statusuebernahme fuer OPEN z.Z. nicht implementiert!";
                    }
                    case 1 -> {
                        if (Integer.valueOf(2).equals(currentStatus)) {
                            opCode = "105";
                            opMsg = "Der neue Status darf nicht gesetzt werden!";
                        } else if (!hasValue(value(jobRow, "cr_id")) || !hasValue(value(jobRow, "cr_id_order"))) {
                            opCode = "111";
                            opMsg = "Es wurde kein Transporteur oder kein Fahrzeug spezifiziert!";
                        } else {
                            updateTakenState(operativeJobId);
                        }
                    }
                    case 2 -> {
                        if (Integer.valueOf(2).equals(currentStatus)) {
                            opCode = "105";
                            opMsg = "Der neue Status darf nicht gesetzt werden!";
                        } else {
                            updateFinishedState(operativeJobId);
                        }
                    }
                    case 3 -> {
                        if (!Integer.valueOf(2).equals(currentStatus)) {
                            opCode = "106";
                            opMsg = "Der Auftrag ist noch nicht beendet und darf keinen Export-Status erhalten!";
                        } else if (isTruthy(value(jobRow, "jb_storno"))) {
                            opCode = "107";
                            opMsg = "Der Auftrag wurde storniert und darf keinen Export-Status erhalten!";
                        } else if (!isUnexported(value(jobRow, "jb_export_time"))) {
                            opCode = "108";
                            opMsg = "Der Auftrag erhielt bereits in der Vergangenheit den Export-Status!";
                        } else {
                            String invoiceNumber = valueAt(invoiceValues, index).trim();
                            if (invoiceNumber.isEmpty()) {
                                opCode = "109";
                                opMsg = "Dem zu exportierenden Auftrag wurde keine Rechnungsnummer mitgegeben!";
                            } else {
                                updateExportedState(operativeJobId, invoiceNumber);
                            }
                        }
                    }
                    case 8 -> {
                        opCode = "199";
                        opMsg = "Statusuebernahme fuer AUTOMATIC ASSIGNMENT z.Z. nicht implementiert!";
                    }
                    case 9 -> {
                        opCode = "199";
                        opMsg = "Statusuebernahme fuer REASSIGNED MANUALLY z.Z. nicht implementiert!";
                    }
                    default -> {
                        opCode = "104";
                        opMsg = "Kein gueltiger Status fuer eine Uebernahme spezifiziert!";
                    }
                }
            }
        }

        if ("0".equals(opCode)) {
            SetUpdateResult updateResult = applySelectiveUpdates(operativeJobId, jobElement, stationsElement);
            if (updateResult != null) {
                opCode = updateResult.code();
                opMsg = updateResult.message();
            }
        }

        return buildSetResult(opCode, opMsg);
    }

    private SetUpdateResult applySelectiveUpdates(Long jobId, Element jobElement, Element stationsElement) {
        if (jobElement != null) {
            Job updates = new Job();
            boolean hasJobUpdates = false;
            BigDecimal updatedTotalPrice = null;
            for (Element field : childElements(jobElement)) {
                String fieldName = field.getTagName();
                String rawValue = field.getTextContent() != null ? field.getTextContent().trim() : "";
                String normalizedValue = "--DEL--".equals(rawValue) ? "" : rawValue;
                switch (fieldName) {
                    case "jb_tourname" -> {
                        updates.setTourName(normalizedValue);
                        hasJobUpdates = true;
                    }
                    case "jb_weight" -> {
                        updates.setWeight(parseNullableInteger(normalizedValue, "100"));
                        hasJobUpdates = true;
                    }
                    case "jb_crvh_length" -> {
                        updates.setCourierVehicleLength(parseNullableInteger(normalizedValue, "100"));
                        hasJobUpdates = true;
                    }
                    case "jb_crvh_width" -> {
                        updates.setCourierVehicleWidth(parseNullableInteger(normalizedValue, "100"));
                        hasJobUpdates = true;
                    }
                    case "jb_crvh_height" -> {
                        updates.setCourierVehicleHeight(parseNullableInteger(normalizedValue, "100"));
                        hasJobUpdates = true;
                    }
                    case "jb_crvh_position" -> {
                        updates.setCourierVehiclePosition(parseNullableInteger(normalizedValue, "100"));
                        hasJobUpdates = true;
                    }
                    case "jb_cr_filter" -> {
                        updates.setCourierFilter(normalizedValue);
                        hasJobUpdates = true;
                    }
                    case "jb_freetext_1" -> {
                        updates.setFreeText1(normalizedValue);
                        hasJobUpdates = true;
                    }
                    case "jb_cr_remark" -> {
                        updates.setCourierRemark(normalizedValue);
                        hasJobUpdates = true;
                    }
                    case "jb_ordertime" -> {
                        updates.setOrderTime(parseNullableDateTime(normalizedValue));
                        hasJobUpdates = true;
                    }
                    case "jb_warningtime" -> {
                        updates.setWarningTime(parseNullableDateTime(normalizedValue));
                        hasJobUpdates = true;
                    }
                    case "jb_totalprice" -> {
                        updatedTotalPrice = parseNullableDecimal(normalizedValue, "121");
                        updates.setTotalPrice(updatedTotalPrice);
                        hasJobUpdates = true;
                    }
                    default -> {
                    }
                }
            }
            if (hasJobUpdates) {
                jobService.updateJob(jobId, updates);
            }
            if (updatedTotalPrice != null) {
                syncLegacyBasePriceRows(jobId, updatedTotalPrice);
            }
        }

        if (stationsElement != null) {
            for (Element stationElement : children(stationsElement, "station")) {
                Integer sort = parseNullableInteger(text(stationElement, "tr_sort"), "100");
                if (sort == null) {
                    continue;
                }
                Tour tour = tourRepository.findByJobIdAndSort(jobId, sort).orElseGet(Tour::new);
                tour.setJobId(jobId);
                tour.setSort(sort);

                String street = text(stationElement, "ad_street");
                String zipcode = text(stationElement, "ad_zipcode");
                String city = text(stationElement, "ad_city");
                String country = firstNonBlank(text(stationElement, "ad_country"), "DE");
                boolean hasAddressInput = !street.isBlank() || !zipcode.isBlank() || !city.isBlank();
                if (hasAddressInput && (street.isBlank() || zipcode.isBlank() || city.isBlank())) {
                    return new SetUpdateResult("130", "Mindestens eine der Stationsadressen ist nicht vollstaendig!");
                }

                if (hasAddressInput) {
                    Address address = addressRepository.findByStreetAndZipcodeAndCityAndCountry(street, zipcode, city, country)
                            .orElseGet(Address::new);
                    address.setStreet(street);
                    address.setZipcode(zipcode);
                    address.setCity(city);
                    address.setCountry(country);
                    tour.setAddressId(addressRepository.save(address).getId());
                }

                if (child(stationElement, "tr_comp") != null) {
                    tour.setComp(normalizedStationValue(text(stationElement, "tr_comp")));
                }
                if (child(stationElement, "tr_comp2") != null) {
                    tour.setComp2(normalizedStationValue(text(stationElement, "tr_comp2")));
                }
                if (child(stationElement, "tr_person") != null) {
                    tour.setPerson(normalizedStationValue(text(stationElement, "tr_person")));
                }
                if (child(stationElement, "tr_hsno") != null) {
                    tour.setHsno(normalizedStationValue(text(stationElement, "tr_hsno")));
                }
                if (child(stationElement, "tr_floor") != null) {
                    tour.setFloor(normalizedStationValue(text(stationElement, "tr_floor")));
                }
                if (child(stationElement, "tr_phone") != null) {
                    tour.setPhone(normalizedStationValue(text(stationElement, "tr_phone")));
                }
                if (child(stationElement, "tr_remark") != null) {
                    tour.setRemark(normalizedStationValue(text(stationElement, "tr_remark")));
                }
                if (child(stationElement, "tr_ware_from_to") != null) {
                    tour.setWareFromTo(normalizedStationValue(text(stationElement, "tr_ware_from_to")));
                }
                if (child(stationElement, "tr_status") != null) {
                    tour.setStatus(parseNullableInteger(text(stationElement, "tr_status"), "100"));
                }
                if (child(stationElement, "csc_id") != null) {
                    tour.setCostCenterId(parseNullableLong(text(stationElement, "csc_id"), "100"));
                }
                jobService.saveTour(tour);
            }
            refreshTourData(jobId);
        }

        return null;
    }

    private LegacyXmlCustomerAuthService.AuthContext authenticateCustomer(Element request) {
        Element auth = child(request, "auth");
        if (auth == null) {
            throw new LegacyXmlRequestException("101", "Die Authentifizierungsdaten sind nicht in Ordnung.");
        }
        return authService.authenticate(new LegacyXmlCustomerAuthService.AuthRequest(
                text(auth, "auth_type"),
                text(auth, "customer"),
                text(auth, "account"),
                text(auth, "password"),
                text(auth, "session_id"),
                text(auth, "costcenter_name")
        ));
    }

    private Long resolveOperativeJobId(Long requestedJobId, boolean orderIsOperational) {
        if (requestedJobId == null) {
            return null;
        }
        if (orderIsOperational) {
            return requestedJobId;
        }
        Optional<GenericDataContainer> mapping = genericDataContainerRepository.findByTypeAndIdAndField("jb", requestedJobId, "is_operative");
        if (mapping == null) {
            mapping = Optional.empty();
        }
        if (mapping.isEmpty() || mapping.get().getContent() == null || mapping.get().getContent().isBlank()) {
            return null;
        }
        String[] tokens = mapping.get().getContent().split("\\|", -1);
        return tokens.length > 0 ? parseLong(tokens[0]) : null;
    }

    private Long resolvePayerCustomerId(Long jobId) {
        Map<String, Object> jobRow = loadJobRow(jobId);
        Long costCenterId = asLong(value(jobRow, "csc_id_payer"));
        if (costCenterId == null) {
            return null;
        }
        CostCenter costCenter = costCenterRepository.findById(costCenterId).orElse(null);
        return costCenter != null ? costCenter.getCustomerId() : null;
    }

    private String buildFieldsXml(Long jobId, List<String> fieldNames) {
        Map<String, Object> jobRow = loadJobRow(jobId);
        if (jobRow.isEmpty()) {
            return errorField("NO_OPERATIVE_TOUR_FOUND");
        }

        boolean needsTourData = fieldNames.stream().anyMatch(field -> field.startsWith("tr_"));
        List<Map<String, Object>> tourRows = needsTourData ? loadTourRows(jobId) : List.of();

        StringBuilder xml = new StringBuilder("<fields>");
        for (String fieldName : fieldNames) {
            if (fieldName.isBlank()) {
                continue;
            }
            if (fieldName.startsWith("trat_")) {
                continue;
            }
            if (fieldName.startsWith("tr_")) {
                xml.append("<field>")
                        .append(tagCData("field_name", fieldName));
                for (int i = 0; i < tourRows.size(); i++) {
                    xml.append("<station>")
                            .append(tag("no", Integer.toString(i + 1)))
                            .append(tagCData("field_value", formatValue(value(tourRows.get(i), fieldName))))
                            .append("</station>");
                }
                xml.append("</field>");
                continue;
            }

            String fieldValue;
            if ("vht_id".equals(fieldName) || "vht_id_real".equals(fieldName)) {
                fieldValue = resolveVehicleTypeValue(asInteger(value(jobRow, fieldName)));
            } else if ("search_contractors".equals(fieldName)) {
                fieldValue = "";
            } else {
                fieldValue = formatValue(value(jobRow, fieldName));
            }
            xml.append("<field>")
                    .append(tagCData("field_name", fieldName))
                    .append(tagCData("field_value", fieldValue))
                    .append("</field>");
        }
        xml.append("</fields>");
        return xml.toString();
    }

    private Map<String, Object> loadJobRow(Long jobId) {
        if (jobId == null) {
            return Collections.emptyMap();
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("SELECT j.* FROM job j WHERE j.jb_id = ?", jobId);
        return rows.isEmpty() ? Collections.emptyMap() : rows.getFirst();
    }

    private CarrierAssignment resolveCarrierAssignment(Long courierId, Long courierVehicleId) {
        Map<String, Object> vehicleRow = loadSingleRow(
                "SELECT crvh_id, cr_id, crvh_sid FROM couriervehicle WHERE crvh_id = ?",
                courierVehicleId
        );
        if (vehicleRow.isEmpty()) {
            return null;
        }
        Long vehicleCourierId = asLong(value(vehicleRow, "cr_id"));
        if (!courierId.equals(vehicleCourierId)) {
            return new CarrierAssignment(vehicleCourierId, "", formatValue(value(vehicleRow, "crvh_sid")));
        }

        Map<String, Object> courierRow = loadSingleRow(
                "SELECT cr_id, cr_eid FROM courier WHERE cr_id = ?",
                courierId
        );
        return new CarrierAssignment(
                courierId,
                formatValue(value(courierRow, "cr_eid")),
                formatValue(value(vehicleRow, "crvh_sid"))
        );
    }

    private void assignCarrier(Long jobId, Long courierId, String vehicleSid) {
        jdbcTemplate.update(
                "UPDATE job SET cr_id = ?, cr_id_order = ?, cr_sid = ? WHERE jb_id = ?",
                courierId,
                courierId,
                vehicleSid,
                jobId
        );
    }

    private void updateTakenState(Long jobId) {
        jdbcTemplate.update(
                "UPDATE job SET jb_status = ?, jb_globaljob = ?, jb_autoranking = ? WHERE jb_id = ?",
                1,
                0,
                0,
                jobId
        );
    }

    private void updateFinishedState(Long jobId) {
        jdbcTemplate.update(
                "UPDATE job SET jb_status = ?, jb_finishtime = ? WHERE jb_id = ?",
                2,
                Timestamp.valueOf(LocalDateTime.now()),
                jobId
        );
    }

    private void updateExportedState(Long jobId, String invoiceNumber) {
        Timestamp now = Timestamp.valueOf(LocalDateTime.now());
        jdbcTemplate.update(
                "UPDATE job SET jb_export_time = ? WHERE jb_id = ?",
                now,
                jobId
        );

        List<Map<String, Object>> existingInvoiceRows = jdbcTemplate.queryForList(
                "SELECT inv_no FROM invoice WHERE jb_id = ?",
                jobId
        );
        if (existingInvoiceRows.isEmpty()) {
            jdbcTemplate.update(
                    "INSERT INTO invoice (jb_id, inv_no, inv_datetime) VALUES (?, ?, ?)",
                    jobId,
                    invoiceNumber,
                    now
            );
        } else {
            jdbcTemplate.update(
                    "UPDATE invoice SET inv_no = ?, inv_datetime = ? WHERE jb_id = ?",
                    invoiceNumber,
                    now,
                    jobId
            );
        }
    }

    private Map<String, Object> loadSingleRow(String sql, Long id) {
        if (id == null) {
            return Collections.emptyMap();
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(Objects.requireNonNull(sql), Objects.requireNonNull(id));
        return rows.isEmpty() ? Collections.emptyMap() : rows.getFirst();
    }

    private List<Map<String, Object>> loadTourRows(Long jobId) {
        if (jobId == null) {
            return List.of();
        }
        return jdbcTemplate.queryForList(
                "SELECT t.*, a.ad_street, a.ad_zipcode, a.ad_city, a.ad_country " +
                        "FROM tour t LEFT JOIN address a ON t.ad_id = a.ad_id " +
                        "WHERE t.jb_id = ? ORDER BY t.tr_sort",
                jobId
        );
    }

    private String resolveVehicleTypeValue(Integer sort) {
        if (sort == null) {
            return "";
        }
        return metaTypeRepository.findBySortAndType(sort, "vehicletype")
                .map(MetaType::getValue)
                .orElse("");
    }

    private Object value(Map<String, Object> row, String key) {
        if (row == null || row.isEmpty() || key == null || key.isBlank()) {
            return null;
        }
        if (row.containsKey(key)) {
            return row.get(key);
        }
        String lowerKey = key.toLowerCase(Locale.ROOT);
        for (Map.Entry<String, Object> entry : row.entrySet()) {
            if (entry.getKey() != null && entry.getKey().toLowerCase(Locale.ROOT).equals(lowerKey)) {
                return entry.getValue();
            }
        }
        return null;
    }

    private String errorField(String value) {
        return "<fields><field>"
                + tagCData("field_name", "ERR")
                + tagCData("field_value", value)
                + "</field></fields>";
    }

    private String buildSetResult(String opCode, String opMsg) {
        return tag("op_code", opCode) + tagCData("op_msg", opMsg);
    }

    private List<String> splitList(String raw) {
        if (raw == null || raw.isBlank()) {
            return new ArrayList<>();
        }
        String[] parts = raw.split("\\|", -1);
        List<String> values = new ArrayList<>(parts.length);
        for (String part : parts) {
            values.add(part == null ? "" : part.trim());
        }
        return values;
    }

    private List<String> splitWriteList(String raw) {
        if (raw == null) {
            return new ArrayList<>();
        }
        String[] parts = raw.split("\\|", -1);
        List<String> values = new ArrayList<>(parts.length);
        for (String part : parts) {
            values.add(part == null ? "" : part.trim());
        }
        return values;
    }

    private String valueAt(List<String> values, int index) {
        if (values == null || index < 0 || index >= values.size()) {
            return "";
        }
        return values.get(index);
    }

    private List<Element> children(Element parent, String tagName) {
        List<Element> result = new ArrayList<>();
        if (parent == null) {
            return result;
        }
        NodeList nodes = parent.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if (node instanceof Element element && tagName.equalsIgnoreCase(element.getTagName())) {
                result.add(element);
            }
        }
        return result;
    }

    private List<Element> childElements(Element parent) {
        List<Element> result = new ArrayList<>();
        if (parent == null) {
            return result;
        }
        NodeList nodes = parent.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if (node instanceof Element element) {
                result.add(element);
            }
        }
        return result;
    }

    private String normalizedStationValue(String value) {
        return "--DEL--".equals(value) ? "" : value;
    }

    private Integer parseNullableInteger(String value, String errorCode) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return Integer.valueOf(value.trim());
        } catch (NumberFormatException ex) {
            throw new LegacyXmlRequestException(errorCode, "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen.");
        }
    }

    private Long parseNullableLong(String value, String errorCode) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return Long.valueOf(value.trim());
        } catch (NumberFormatException ex) {
            throw new LegacyXmlRequestException(errorCode, "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen.");
        }
    }

    private BigDecimal parseNullableDecimal(String value, String errorCode) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return new BigDecimal(value.trim().replace(",", "."));
        } catch (NumberFormatException ex) {
            throw new LegacyXmlRequestException(errorCode, "Die Preisangabe ist nicht numerisch!");
        }
    }

    private LocalDateTime parseNullableDateTime(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return LocalDateTime.parse(value.trim(), DATE_TIME_FORMAT);
        } catch (Exception ex) {
            throw new LegacyXmlRequestException("120", "Die Datumsangabe ist nicht korrekt!");
        }
    }

    private void syncLegacyBasePriceRows(Long jobId, BigDecimal totalPrice) {
        if (jobId == null || totalPrice == null) {
            return;
        }
        int affected = jdbcTemplate.update(
                "UPDATE tourservice SET trs_price = ?, trs_discount = 0.0000 " +
                        "WHERE jb_id = ? AND trs_srv_name IN ('Fixpreis', 'Grundpreis')",
                totalPrice,
                jobId
        );
        if (affected == 0) {
            jobService.findById(jobId).ifPresent(jobTourServiceWriteService::createBasePriceRows);
        }
    }

    private void refreshTourData(Long jobId) {
        if (jobId == null) {
            return;
        }
        List<Tour> tours = jobService.getTours(jobId);
        if (tours.isEmpty()) {
            return;
        }
        StringBuilder zipcodes = new StringBuilder();
        StringBuilder cities = new StringBuilder();
        StringBuilder countries = new StringBuilder();
        for (int i = 0; i < tours.size(); i++) {
            Tour tour = tours.get(i);
            Address address = tour.getAddressId() != null ? addressRepository.findById(Objects.requireNonNull(tour.getAddressId())).orElse(null) : null;
            if (i > 0) {
                zipcodes.append(';');
                cities.append(';');
                countries.append(';');
            }
            zipcodes.append(address != null && address.getZipcode() != null ? address.getZipcode() : "");
            cities.append(address != null && address.getCity() != null ? address.getCity() : "");
            countries.append(address != null && address.getCountry() != null ? address.getCountry() : "");
        }
        Job updates = new Job();
        updates.setTourData(zipcodes + "|" + cities + "|" + countries);
        jobService.updateJob(jobId, updates);
    }

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

    private boolean isFalsey(String value) {
        return value == null || value.isBlank() || "0".equals(value.trim());
    }

    private boolean isTruthy(Object value) {
        String normalized = formatValue(value).trim();
        return !normalized.isEmpty() && !"0".equals(normalized);
    }

    private boolean hasValue(Object value) {
        return !formatValue(value).trim().isEmpty();
    }

    private boolean isBlank(String value) {
        return value == null || value.isBlank();
    }

    private boolean isUnexported(Object exportTime) {
        String normalized = formatValue(exportTime).trim();
        return normalized.isEmpty()
                || "0000-00-00 00:00:00".equals(normalized)
                || "9999-12-31 23:59:59".equals(normalized);
    }

    private Long parseLong(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return Long.valueOf(value.trim());
        } catch (NumberFormatException ex) {
            return null;
        }
    }

    private Integer asInteger(Object value) {
        if (value instanceof Integer integer) {
            return integer;
        }
        if (value instanceof Number number) {
            return number.intValue();
        }
        if (value instanceof String string && !string.isBlank()) {
            try {
                return Integer.valueOf(string.trim());
            } catch (NumberFormatException ignored) {
            }
        }
        return null;
    }

    private Long asLong(Object value) {
        if (value instanceof Long longValue) {
            return longValue;
        }
        if (value instanceof Number number) {
            return number.longValue();
        }
        if (value instanceof String string && !string.isBlank()) {
            try {
                return Long.valueOf(string.trim());
            } catch (NumberFormatException ignored) {
            }
        }
        return null;
    }

    private String formatValue(Object value) {
        if (value == null) {
            return "";
        }
        if (value instanceof Timestamp timestamp) {
            return DATE_TIME_FORMAT.format(timestamp.toLocalDateTime());
        }
        if (value instanceof LocalDateTime dateTime) {
            return DATE_TIME_FORMAT.format(dateTime);
        }
        if (value instanceof BigDecimal decimal) {
            return decimal.toPlainString();
        }
        return value.toString();
    }

    private Element parseRequest(String xmlRequest) throws Exception {
        return LegacyXmlXmlParsing.parseRequest(xmlRequest, "order_data");
    }

    private Element child(Element parent, String tagName) {
        return LegacyXmlXmlParsing.child(parent, tagName);
    }

    private String text(Element parent, String tagName) {
        return LegacyXmlXmlParsing.text(parent, tagName);
    }

    private boolean isDebugEnabled(Element request) {
        String value = upper(text(request, "debug_on"), "");
        return "ON".equals(value) || "YES".equals(value);
    }

    private String upper(String value, String fallback) {
        return value == null || value.isBlank() ? fallback : value.trim().toUpperCase(Locale.ROOT);
    }

    private String ok(String payload, String debug) {
        return "<xml><state>OK</state>" + payload + debugTag(debug) + "</xml>";
    }

    private String error(LegacyXmlRequestException ex, String debug) {
        return "<xml><state>NOT OK</state><errors><error>"
                + tag("err_no", ex.getErrorCode())
                + tagCData("err_desc", ex.getDescription())
                + "</error></errors>"
                + debugTag(debug)
                + "</xml>";
    }

    private String debugTag(String debug) {
        return debug == null || debug.isBlank() ? "" : tagCData("debug", debug);
    }

    private String tag(String name, String value) {
        return "<" + name + ">" + escapeXml(value) + "</" + name + ">";
    }

    private String tagCData(String name, String value) {
        return "<" + name + "><![CDATA[" + cdata(value) + "]]></" + name + ">";
    }

    private String cdata(String value) {
        return value == null ? "" : value.replace("]]>", "]]]]><![CDATA[>");
    }

    private String escapeXml(String value) {
        if (value == null) {
            return "";
        }
        return value
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&apos;");
    }

    private record CarrierAssignment(Long courierId, String courierEid, String vehicleSid) {
    }

    private record SetUpdateResult(String code, String message) {
    }
}
