package de.votian.services.service;

import de.votian.services.entity.Address;
import de.votian.services.entity.Company;
import de.votian.services.entity.CostCenter;
import de.votian.services.entity.CostCenterAddress;
import de.votian.services.entity.CourierVehicle;
import de.votian.services.entity.GenericDataContainer;
import de.votian.services.entity.Headquarters;
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.CompanyRepository;
import de.votian.services.repository.CostCenterAddressRepository;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.CourierVehicleRepository;
import de.votian.services.repository.GenericDataContainerRepository;
import de.votian.services.repository.HeadquartersRepository;
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.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

@Service
@Transactional
public class LegacyXmlOrderRequestIntegrationService {

    private static final DateTimeFormatter LEGACY_DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final String STATION_REQUEST_MARKER = "station_request";
    private static final String JOB_EVENTS_FIELD = "jb_events";

    private final LegacyXmlB2BAuthService authService;
    private final JobService jobService;
    private final AddressRepository addressRepository;
    private final CostCenterRepository costCenterRepository;
    private final CostCenterAddressRepository costCenterAddressRepository;
    private final CourierVehicleRepository courierVehicleRepository;
    private final HeadquartersRepository headquartersRepository;
    private final MetaTypeRepository metaTypeRepository;
    private final TourRepository tourRepository;
    private final CompanyRepository companyRepository;
    private final GenericDataContainerRepository genericDataContainerRepository;
    private final JdbcTemplate jdbcTemplate;

    public LegacyXmlOrderRequestIntegrationService(LegacyXmlB2BAuthService authService,
                                                   JobService jobService,
                                                   AddressRepository addressRepository,
                                                   CostCenterRepository costCenterRepository,
                                                   CostCenterAddressRepository costCenterAddressRepository,
                                                   CourierVehicleRepository courierVehicleRepository,
                                                   HeadquartersRepository headquartersRepository,
                                                   MetaTypeRepository metaTypeRepository,
                                                   TourRepository tourRepository,
                                                   CompanyRepository companyRepository,
                                                   GenericDataContainerRepository genericDataContainerRepository,
                                                   JdbcTemplate jdbcTemplate) {
        this.authService = authService;
        this.jobService = jobService;
        this.addressRepository = addressRepository;
        this.costCenterRepository = costCenterRepository;
        this.costCenterAddressRepository = costCenterAddressRepository;
        this.courierVehicleRepository = courierVehicleRepository;
        this.headquartersRepository = headquartersRepository;
        this.metaTypeRepository = metaTypeRepository;
        this.tourRepository = tourRepository;
        this.companyRepository = companyRepository;
        this.genericDataContainerRepository = genericDataContainerRepository;
        this.jdbcTemplate = jdbcTemplate;
    }

    public String handleOrderRequest(String xmlRequest) {
        try {
            Element request = LegacyXmlXmlParsing.parseRequest(xmlRequest, "order");
            boolean debug = isDebugEnabled(request);
            LegacyXmlB2BAuthService.AuthContext auth = authenticate(request);
            ParsedOrderRequest parsed = parseOrderRequest(request, auth);
            Long jobId = executeOrderOperation(parsed, auth);
            return okCarrier(jobId, List.of(), debug ? "order_request" : "");
        } 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);
        }
    }

    public String handleStationRequest(String xmlRequest) {
        try {
            Element request = LegacyXmlXmlParsing.parseRequest(xmlRequest, "order");
            boolean debug = isDebugEnabled(request);
            LegacyXmlB2BAuthService.AuthContext auth = authenticate(request);
            ParsedOrderRequest parsed = parseOrderRequest(request, auth);
            StationResult result = executeStationOperation(parsed, auth);
            return okCarrier(result.jobId(), result.stationIds(), debug ? "station_request" : "");
        } 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 Long executeOrderOperation(ParsedOrderRequest parsed,
                                       LegacyXmlB2BAuthService.AuthContext auth) {
        String operation = parsed.operation();
        if ("DEL".equals(operation) || "DELETE".equals(operation)) {
            Job existing = findLatestJobByCommissionNo(parsed.commissionNo(), auth.customer().getId());
            if (existing == null) {
                throw new LegacyXmlRequestException("107", "Der Stornovorgang konnte nicht abgeschlossen werden, weil der Auftrag nicht gefunden wurde.");
            }
            clearCommissionNumbers(existing.getId());
            jobService.cancelJob(existing.getId());
            appendEvents(existing.getId(), parsed.events());
            return existing.getId();
        }

        Job existing = ("UPD".equals(operation) || "UPDATE".equals(operation))
                ? findLatestJobByCommissionNo(parsed.commissionNo(), auth.customer().getId())
                : null;

        if (existing == null) {
            Job job = buildJob(parsed, auth, null);
            List<Tour> tours = buildOrderTours(parsed, auth, null);
            Job created = jobService.createJob(job, tours, List.of());
            upsertJobMetadata(created.getId(), parsed, false, auth.customer().getId());
            return created.getId();
        }

        Job updates = buildJob(parsed, auth, existing);
        jobService.updateJob(existing.getId(), updates);
        syncOrderTours(existing.getId(), parsed, auth);
        upsertJobMetadata(existing.getId(), parsed, false, auth.customer().getId());
        return existing.getId();
    }

    private StationResult executeStationOperation(ParsedOrderRequest parsed,
                                                  LegacyXmlB2BAuthService.AuthContext auth) {
        String operation = parsed.operation();
        LocalDate effectiveDay = parsed.orderTime() != null ? parsed.orderTime().toLocalDate() : LocalDate.now();
        Job currentJob = findStationRequestJob(auth.customer().getId(), effectiveDay);
        List<ParsedStation> stationPayload = extractStationPayload(parsed.stations());

        if ("DEL".equals(operation) || "DELETE".equals(operation)) {
            Tour currentTour = findStationByCommissionNo(currentJob, parsed.commissionNo());
            if (currentTour == null) {
                throw new LegacyXmlRequestException("107", "Der Stornovorgang konnte nicht abgeschlossen werden, weil die Station nicht gefunden wurde.");
            }
            tourRepository.delete(currentTour);
            appendEvents(currentJob.getId(), parsed.events());
            return new StationResult(currentJob.getId(), List.of());
        }

        if ("UPD".equals(operation) || "UPDATE".equals(operation)) {
            Tour currentTour = findStationByCommissionNo(currentJob, parsed.commissionNo());
            if (currentTour == null) {
                throw new LegacyXmlRequestException("105", "Es wurde keine Station fuer ein Update gefunden! Bitte Kommissionsnummer ueberpruefen!");
            }
            if (stationPayload.isEmpty()) {
                throw new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen.");
            }
            applyStation(currentTour, stationPayload.getLast(), parsed.commissionNo(), currentTour.getSort());
            jobService.saveTour(currentTour);
            appendEvents(currentJob.getId(), parsed.events());
            return new StationResult(currentJob.getId(), List.of());
        }

        if (stationPayload.isEmpty()) {
            throw new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen.");
        }

        Job job = currentJob;
        if (job == null) {
            Job create = buildJob(parsed, auth, null);
            List<Tour> tours = new ArrayList<>();
            tours.add(buildDefaultPickupTour(auth));
            tours.addAll(buildStationTours(stationPayload, parsed.commissionNo(), 2));
            job = jobService.createJob(create, tours, List.of());
            upsertJobMetadata(job.getId(), parsed, true, auth.customer().getId());
            List<Long> stationIds = jobService.getTours(job.getId()).stream()
                    .filter(tour -> tour.getSort() != null && tour.getSort() > 1)
                    .map(Tour::getId)
                    .toList();
            return new StationResult(job.getId(), stationIds);
        }

        List<Tour> existingTours = jobService.getTours(job.getId());
        int nextSort = existingTours.stream()
                .map(Tour::getSort)
                .filter(sort -> sort != null)
                .max(Comparator.naturalOrder())
                .orElse(0) + 1;
        List<Long> stationIds = new ArrayList<>();
        for (Tour tour : buildStationTours(stationPayload, parsed.commissionNo(), nextSort)) {
            tour.setJobId(job.getId());
            stationIds.add(jobService.saveTour(tour).getId());
            nextSort++;
        }
        upsertJobMetadata(job.getId(), parsed, true, auth.customer().getId());
        return new StationResult(job.getId(), stationIds);
    }

    private void syncOrderTours(Long jobId,
                                ParsedOrderRequest parsed,
                                LegacyXmlB2BAuthService.AuthContext auth) {
        List<ParsedStation> stations = parsed.stations();
        if (stations.isEmpty()) {
            return;
        }

        List<Tour> existingTours = jobService.getTours(jobId);
        for (int i = 0; i < stations.size(); i++) {
            int sort = i + 1;
            Tour tour = existingTours.stream()
                    .filter(existing -> existing.getSort() != null && existing.getSort() == sort)
                    .findFirst()
                    .orElseGet(Tour::new);
            tour.setJobId(jobId);
            applyStation(tour, stations.get(i), i == 0 ? parsed.commissionNo() : "", sort);
            jobService.saveTour(tour);
        }

        for (Tour existingTour : existingTours) {
            if (existingTour.getSort() != null && existingTour.getSort() > stations.size()) {
                tourRepository.delete(existingTour);
            }
        }
    }

    private List<Tour> buildOrderTours(ParsedOrderRequest parsed,
                                       LegacyXmlB2BAuthService.AuthContext auth,
                                       Job existing) {
        List<Tour> tours = new ArrayList<>();
        for (int i = 0; i < parsed.stations().size(); i++) {
            ParsedStation station = parsed.stations().get(i);
            Tour tour = new Tour();
            applyStation(tour, station, i == 0 ? parsed.commissionNo() : "", i + 1);
            tours.add(tour);
        }
        return tours;
    }

    private List<Tour> buildStationTours(List<ParsedStation> stations,
                                         String commissionNo,
                                         int startSort) {
        List<Tour> tours = new ArrayList<>();
        int sort = startSort;
        for (ParsedStation station : stations) {
            Tour tour = new Tour();
            applyStation(tour, station, commissionNo, sort);
            tours.add(tour);
            sort++;
        }
        return tours;
    }

    private void applyStation(Tour target, ParsedStation station, String commissionNo, int sort) {
        target.setSort(sort);
        target.setComp(firstNonBlank(station.company(), station.person(), ""));
        target.setComp2(station.company2());
        target.setPerson(station.person());
        target.setHsno(station.houseNo());
        target.setFloor(station.floor());
        target.setCommissionNo(commissionNo == null ? "" : commissionNo);
        target.setStatus(target.getStatus() != null ? target.getStatus() : 0);
        target.setRemark(station.remark());
        target.setPhone(station.phone());
        target.setCostCenterId(station.costCenterId());
        target.setWareFromTo(station.wareFromTo());
        target.setAddressId(resolveAddressId(station));
    }

    private Tour buildDefaultPickupTour(LegacyXmlB2BAuthService.AuthContext auth) {
        ParsedStation station = resolveCostCenterStation(auth.effectiveCostCenter(), auth.customer());
        Tour tour = new Tour();
        applyStation(tour, station, "", 1);
        tour.setStatus(0);
        return tour;
    }

    private Job buildJob(ParsedOrderRequest parsed,
                         LegacyXmlB2BAuthService.AuthContext auth,
                         Job existing) {
        Job job = new Job();
        LocalDateTime effectiveOrderTime = parsed.orderTime() != null
                ? parsed.orderTime()
                : existing != null
                ? existing.getOrderTime()
                : LocalDateTime.now();
        if (existing == null || parsed.hqId() != null) {
            job.setHeadquartersId(parsed.hqId());
            job.setHeadquartersDispoId(parsed.hqId());
        }
        if (existing == null) {
            Long costCenterId = auth.effectiveCostCenter() != null ? auth.effectiveCostCenter().getId() : null;
            job.setCostCenterPayerId(costCenterId);
            job.setCostCenterRelatedId(costCenterId);
            job.setStatus(9);
            job.setBookTime(effectiveOrderTime);
            job.setService(1);
            job.setGlobalJob(0);
            job.setAutoranking(0);
        }
        job.setOrderTime(effectiveOrderTime);
        job.setWarningTime(parsed.warningTime());
        job.setVehicleTypeId(parsed.vehicleTypeId());
        job.setType(parsed.type());
        job.setCourierSid(parsed.courierVehicleSid());
        job.setCourierOrderId(parsed.courierOrderId());
        job.setCourierFilter(parsed.filter());
        job.setCourierRemark(parsed.courierRemark());
        job.setDispoInfo(parsed.dispoInfo());
        job.setFreeText1(parsed.remark());
        job.setInvoiceText(parsed.remark());
        job.setTourName(firstNonBlank(parsed.remark(), parsed.commissionNo(), existing != null ? existing.getTourName() : "XML-Auftrag"));
        job.setReserve(parsed.orderTime() != null || existing == null ? 1 : null);
        job.setWeight(parsed.weight());
        job.setCourierVehicleLength(parsed.length());
        job.setCourierVehicleWidth(parsed.width());
        job.setCourierVehicleHeight(parsed.height());
        job.setCourierVehiclePosition(parsed.position());
        return job;
    }

    private ParsedOrderRequest parseOrderRequest(Element request,
                                                 LegacyXmlB2BAuthService.AuthContext auth) {
        String operation = normalizeOperation(text(request, "operation"));
        Long hqId = resolveHeadquartersId(text(request, "hq"), auth);
        Integer vehicleTypeId = resolveVehicleTypeId(child(request, "vehicle"));
        CourierAssignment courierAssignment = resolveCourierAssignment(text(request, "courier"));
        LocalDateTime orderTime = parseDateTime(firstNonBlank(text(request, "ordertime"), text(request, "ordertimeUTC"), null));
        LocalDateTime warningTime = parseDateTime(firstNonBlank(text(request, "warningtime"), text(request, "warningtimeUTC"), null));

        List<ParsedStation> stations = parseStations(child(request, "stations"), auth);
        return new ParsedOrderRequest(
                operation,
                hqId,
                text(request, "no"),
                nullIfBlank(text(request, "type")),
                vehicleTypeId,
                parseInteger(text(child(request, "vehicle"), "weight")),
                parseInteger(text(child(request, "vehicle"), "length")),
                parseInteger(text(child(request, "vehicle"), "width")),
                parseInteger(text(child(request, "vehicle"), "height")),
                parseInteger(text(child(request, "vehicle"), "position")),
                orderTime,
                warningTime,
                courierAssignment.courierVehicleSid(),
                courierAssignment.courierOrderId(),
                nullIfBlank(text(request, "filter")),
                nullIfBlank(text(request, "remark")),
                nullIfBlank(text(request, "courier_remark")),
                nullIfBlank(text(request, "dispoinfo")),
                parseValues(child(request, "gdc")),
                parseValues(child(request, "event")),
                stations
        );
    }

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

    private Long resolveHeadquartersId(String mnemonic,
                                       LegacyXmlB2BAuthService.AuthContext auth) {
        if (mnemonic == null || mnemonic.isBlank()) {
            return auth.customer() != null ? auth.customer().getHeadquartersId() : null;
        }
        Headquarters headquarters = headquartersRepository.findByMnemonic(mnemonic.trim());
        if (headquarters == null) {
            throw new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen.");
        }
        return headquarters.getId();
    }

    private Integer resolveVehicleTypeId(Element vehicleElement) {
        if (vehicleElement == null) {
            return null;
        }
        Integer typeNo = parseInteger(text(vehicleElement, "type_no"));
        if (typeNo != null) {
            return typeNo;
        }
        String typeName = text(vehicleElement, "type_name");
        if (typeName == null || typeName.isBlank()) {
            return null;
        }
        return metaTypeRepository.findByTypeOrderBySort("vehicletype").stream()
                .filter(type -> type.getValue() != null && type.getValue().equalsIgnoreCase(typeName.trim()))
                .map(MetaType::getSort)
                .findFirst()
                .orElse(null);
    }

    private CourierAssignment resolveCourierAssignment(String courierVehicleSid) {
        if (courierVehicleSid == null || courierVehicleSid.isBlank()) {
            return new CourierAssignment(null, null);
        }
        CourierVehicle vehicle = courierVehicleRepository.findBySid(courierVehicleSid.trim()).orElse(null);
        if (vehicle == null) {
            return new CourierAssignment(courierVehicleSid.trim(), null);
        }
        return new CourierAssignment(vehicle.getSid(), vehicle.getCourierId());
    }

    private List<ParsedStation> parseStations(Element stationsElement,
                                              LegacyXmlB2BAuthService.AuthContext auth) {
        if (stationsElement == null) {
            return List.of();
        }
        List<ParsedStation> result = new ArrayList<>();
        for (Element stationElement : children(stationsElement, "station")) {
            String costCenterName = text(stationElement, "costcenter");
            CostCenter costCenter = null;
            if (costCenterName != null && !costCenterName.isBlank() && auth.customer() != null) {
                costCenter = costCenterRepository.findByCustomerIdAndName(auth.customer().getId(), costCenterName.trim()).orElse(null);
            }

            ParsedStation baseStation = costCenter != null
                    ? resolveCostCenterStation(costCenter, auth.customer())
                    : new ParsedStation(null, null, null, null, null, null, "DE", null, null, null, null, null, null);

            String country = firstNonBlank(text(stationElement, "country"), baseStation.country(), "DE");
            result.add(new ParsedStation(
                    costCenter != null ? costCenter.getId() : (auth.effectiveCostCenter() != null ? auth.effectiveCostCenter().getId() : null),
                    firstNonBlank(text(stationElement, "company"), baseStation.company(), null),
                    firstNonBlank(text(stationElement, "company2"), baseStation.company2(), null),
                    firstNonBlank(text(stationElement, "street"), baseStation.street(), null),
                    firstNonBlank(text(stationElement, "houseno"), baseStation.houseNo(), null),
                    firstNonBlank(text(stationElement, "zipcode"), baseStation.zipcode(), null),
                    country,
                    firstNonBlank(text(stationElement, "city"), baseStation.city(), null),
                    firstNonBlank(text(stationElement, "person"), baseStation.person(), null),
                    firstNonBlank(text(stationElement, "phone"), baseStation.phone(), null),
                    firstNonBlank(text(stationElement, "special_remark"), baseStation.remark(), null),
                    firstNonBlank(text(stationElement, "floorno"), baseStation.floor(), null),
                    nullIfBlank(text(stationElement, "ware_from_to"))
            ));
        }
        return result;
    }

    private ParsedStation resolveCostCenterStation(CostCenter costCenter, de.votian.services.entity.Customer customer) {
        if (costCenter == null) {
            return new ParsedStation(null, null, null, null, null, null, "DE", null, null, null, null, null, null);
        }

        CostCenterAddress costCenterAddress = costCenterAddressRepository.findByCostCenterId(costCenter.getId()).stream()
                .findFirst()
                .orElse(null);
        Address address = costCenterAddress != null && costCenterAddress.getAddressId() != null
                ? addressRepository.findById(Objects.requireNonNull(costCenterAddress.getAddressId())).orElse(null)
                : null;
        if (address == null && customer != null && customer.getCompanyId() != null) {
            Company company = companyRepository.findById(Objects.requireNonNull(customer.getCompanyId())).orElse(null);
            if (company != null && company.getAddressId() != null) {
                address = addressRepository.findById(Objects.requireNonNull(company.getAddressId())).orElse(null);
            }
        }

        return new ParsedStation(
                costCenter.getId(),
                costCenterAddress != null ? costCenterAddress.getComp() : costCenter.getName(),
                costCenterAddress != null ? costCenterAddress.getComp2() : null,
                address != null ? address.getStreet() : null,
                costCenterAddress != null ? costCenterAddress.getHsno() : null,
                address != null ? address.getZipcode() : null,
                address != null && address.getCountry() != null ? address.getCountry() : "DE",
                address != null ? address.getCity() : null,
                costCenterAddress != null ? costCenterAddress.getPerson() : null,
                costCenterAddress != null ? costCenterAddress.getPhone() : null,
                costCenterAddress != null ? costCenterAddress.getRemark() : null,
                costCenterAddress != null ? costCenterAddress.getFloor() : null,
                null
        );
    }

    private List<ParsedStation> extractStationPayload(List<ParsedStation> stations) {
        if (stations == null || stations.isEmpty()) {
            return List.of();
        }
        if (stations.size() == 1) {
            return List.of(stations.getFirst());
        }
        return new ArrayList<>(stations.subList(1, stations.size()));
    }

    private Job findLatestJobByCommissionNo(String commissionNo, Long customerId) {
        if (commissionNo == null || commissionNo.isBlank() || customerId == null) {
            return null;
        }
        return jobService.findByCommissionNoAndCustomer(commissionNo.trim(), customerId).stream()
                .findFirst()
                .orElse(null);
    }

    private Job findStationRequestJob(Long customerId, LocalDate day) {
        if (customerId == null || day == null) {
            return null;
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT jb.jb_id " +
                        "FROM job jb " +
                        "JOIN costcenter csc ON jb.csc_id_payer = csc.csc_id " +
                        "JOIN genericdatacontainer gdc ON gdc.gdc_obj_id = jb.jb_id " +
                        "WHERE csc.cs_id = ? " +
                        "AND gdc.gdc_obj_type = 'jb' " +
                        "AND gdc.gdc_gen_fieldname = ? " +
                        "AND LEFT(jb.jb_ordertime, 10) = ? " +
                        "ORDER BY jb.jb_id DESC",
                customerId,
                STATION_REQUEST_MARKER,
                day.toString()
        );
        if (rows.isEmpty()) {
            return null;
        }
        Object rawId = rows.getFirst().get("jb_id");
        if (rawId instanceof Number number) {
            return jobService.findById(number.longValue()).orElse(null);
        }
        return null;
    }

    private Tour findStationByCommissionNo(Job job, String commissionNo) {
        if (job == null || job.getId() == null || commissionNo == null || commissionNo.isBlank()) {
            return null;
        }
        return jobService.getTours(job.getId()).stream()
                .filter(tour -> commissionNo.equals(tour.getCommissionNo()))
                .findFirst()
                .orElse(null);
    }

    private Long resolveAddressId(ParsedStation station) {
        String street = nullIfBlank(station.street());
        String zipcode = nullIfBlank(station.zipcode());
        String city = nullIfBlank(station.city());
        String country = firstNonBlank(station.country(), "DE");
        if (street == null && zipcode == null && city == null) {
            return null;
        }
        Optional<Address> existing = addressRepository.findByStreetAndZipcodeAndCityAndCountry(street, zipcode, city, country);
        if (existing.isPresent()) {
            return existing.get().getId();
        }
        Address address = new Address();
        address.setStreet(street);
        address.setZipcode(zipcode);
        address.setCity(city);
        address.setCountry(country);
        return addressRepository.save(address).getId();
    }

    private void upsertJobMetadata(Long jobId,
                                   ParsedOrderRequest parsed,
                                   boolean stationRequest,
                                   Long customerId) {
        if (jobId == null) {
            return;
        }
        upsertMetadata(jobId, STATION_REQUEST_MARKER, stationRequest ? "1" : null, stationRequest && customerId != null ? customerId.toString() : "");
        for (int i = 0; i < parsed.jobGdcValues().size(); i++) {
            String value = parsed.jobGdcValues().get(i);
            if (value != null && !value.isBlank()) {
                upsertMetadata(jobId, "info_" + i, value, "");
            }
        }
        appendEvents(jobId, parsed.events());
    }

    private void appendEvents(Long jobId, List<String> events) {
        if (jobId == null || events == null || events.isEmpty()) {
            return;
        }
        String joined = String.join("|", events.stream().filter(value -> value != null && !value.isBlank()).toList());
        if (joined.isBlank()) {
            return;
        }
        Optional<GenericDataContainer> existing = findMetadata("jb", jobId, JOB_EVENTS_FIELD);
        if (existing.isPresent()) {
            String current = existing.get().getContent() != null && !existing.get().getContent().isBlank()
                    ? existing.get().getContent() + "|" + joined
                    : joined;
            genericDataContainerRepository.updateContent("jb", jobId, JOB_EVENTS_FIELD, current, "");
        } else {
            GenericDataContainer entry = new GenericDataContainer();
            entry.setObjectType("jb");
            entry.setObjectId(jobId);
            entry.setFieldName(JOB_EVENTS_FIELD);
            entry.setContent(joined);
            entry.setContext("");
            genericDataContainerRepository.save(entry);
        }
    }

    private void upsertMetadata(Long jobId, String fieldName, String content, String context) {
        Optional<GenericDataContainer> existing = findMetadata("jb", jobId, fieldName);
        if (content == null || content.isBlank()) {
            return;
        }
        if (existing.isPresent()) {
            genericDataContainerRepository.updateContent("jb", jobId, fieldName, content, context == null ? "" : context);
            return;
        }
        GenericDataContainer entry = new GenericDataContainer();
        entry.setObjectType("jb");
        entry.setObjectId(jobId);
        entry.setFieldName(fieldName);
        entry.setContent(content);
        entry.setContext(context == null ? "" : context);
        genericDataContainerRepository.save(entry);
    }

    private Optional<GenericDataContainer> findMetadata(String objectType, Long objectId, String fieldName) {
        Optional<GenericDataContainer> existing = genericDataContainerRepository.findByTypeAndIdAndField(objectType, objectId, fieldName);
        return existing != null ? existing : Optional.empty();
    }

    private void clearCommissionNumbers(Long jobId) {
        for (Tour tour : jobService.getTours(jobId)) {
            if (tour.getCommissionNo() != null && !tour.getCommissionNo().isBlank()) {
                tour.setCommissionNo("");
                jobService.saveTour(tour);
            }
        }
    }

    private List<String> parseValues(Element parent) {
        if (parent == null) {
            return List.of();
        }
        List<String> values = new ArrayList<>();
        for (Element element : children(parent, "val")) {
            if (element.getTextContent() != null) {
                String value = element.getTextContent().trim();
                if (!value.isBlank()) {
                    values.add(value);
                }
            }
        }
        return values;
    }

    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 String normalizeOperation(String value) {
        return value == null || value.isBlank()
                ? "INS"
                : value.trim().toUpperCase(Locale.ROOT);
    }

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

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

    private String nullIfBlank(String value) {
        return value == null || value.isBlank() ? null : value.trim();
    }

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

    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 String firstNonBlank(String... values) {
        if (values == null) {
            return null;
        }
        for (String value : values) {
            if (value != null && !value.isBlank()) {
                return value.trim();
            }
        }
        return null;
    }

    private String okCarrier(Long jobId, List<Long> stationIds, String debug) {
        StringBuilder xml = new StringBuilder("<xml><state>OK</state><carrier>")
                .append(tag("orderno", jobId == null ? "" : jobId.toString()));
        if (stationIds != null && !stationIds.isEmpty()) {
            xml.append("<stations>");
            for (Long stationId : stationIds) {
                xml.append(tag("station_no", stationId == null ? "" : stationId.toString()));
            }
            xml.append("</stations>");
        }
        xml.append("</carrier>");
        if (debug != null && !debug.isBlank()) {
            xml.append(tagCData("debug", debug));
        }
        xml.append("</xml>");
        return xml.toString();
    }

    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>"
                + (debug == null || debug.isBlank() ? "" : tagCData("debug", debug))
                + "</xml>";
    }

    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 CourierAssignment(String courierVehicleSid, Long courierOrderId) {
    }

    private record ParsedStation(Long costCenterId,
                                 String company,
                                 String company2,
                                 String street,
                                 String houseNo,
                                 String zipcode,
                                 String country,
                                 String city,
                                 String person,
                                 String phone,
                                 String remark,
                                 String floor,
                                 String wareFromTo) {
    }

    private record ParsedOrderRequest(String operation,
                                      Long hqId,
                                      String commissionNo,
                                      String type,
                                      Integer vehicleTypeId,
                                      Integer weight,
                                      Integer length,
                                      Integer width,
                                      Integer height,
                                      Integer position,
                                      LocalDateTime orderTime,
                                      LocalDateTime warningTime,
                                      String courierVehicleSid,
                                      Long courierOrderId,
                                      String filter,
                                      String remark,
                                      String courierRemark,
                                      String dispoInfo,
                                      List<String> jobGdcValues,
                                      List<String> events,
                                      List<ParsedStation> stations) {
    }

    private record StationResult(Long jobId, List<Long> stationIds) {
    }
}
