package at.procon.eventhub.tachographfilesession.service;

import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicle;
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.UUID;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

@Component
public class DriverCardXmlExtractionService {

    private final DriverKeyFactory driverKeyFactory;
    private final VehicleKeyFactory vehicleKeyFactory;

    public DriverCardXmlExtractionService(DriverKeyFactory driverKeyFactory, VehicleKeyFactory vehicleKeyFactory) {
        this.driverKeyFactory = driverKeyFactory;
        this.vehicleKeyFactory = vehicleKeyFactory;
    }

    public TachographFileSession extract(
            TachographXmlParser.ParsedTachographXml parsedXml,
            TachographFileSessionMetadata metadata,
            Instant createdAt,
            Instant expiresAt
    ) {
        Document document = parsedXml.document();
        List<ExtractionWarning> warnings = new ArrayList<>();

        ExtractedDriverCard driverCard = extractDriverCard(document, warnings);
        if (driverCard == null || driverCard.cardNumber() == null) {
            throw new TachographXmlValidationException("Driver card identification could not be extracted from XML.");
        }
        String driverKey = driverKeyFactory.createDriverKey(driverCard.cardNation(), driverCard.cardNumber());
        driverCard = new ExtractedDriverCard(
                driverKeyFactory.createSourceDriverCardId(driverKey),
                driverCard.cardNation(),
                driverCard.cardNumber(),
                driverCard.fullCardNumber(),
                driverCard.issuingAuthorityName(),
                driverCard.issueDate(),
                driverCard.validityBegin(),
                driverCard.expiryDate()
        );
        ExtractedDriver driver = extractDriver(document, driverKey, warnings);

        LinkedHashMap<String, ExtractedVehicleRegistration> registrationsByKey = new LinkedHashMap<>();
        LinkedHashMap<String, ExtractedVehicle> vehiclesByKey = new LinkedHashMap<>();
        List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals =
                extractVehicleUsageIntervals(document, registrationsByKey, vehiclesByKey, warnings);
        List<ExtractedCardActivityInterval> activityIntervals =
                assignVehicleCoverage(extractActivityIntervals(document, warnings), vehicleUsageIntervals);
        List<ExtractedSupportEvent> supportEvents = extractSupportEvents(document, driverKey, vehicleUsageIntervals, warnings);

        DriverExtractionSession driverSession = new DriverExtractionSession(
                driverKey,
                driver,
                driverCard,
                List.copyOf(registrationsByKey.values()),
                List.copyOf(vehiclesByKey.values()),
                List.copyOf(vehicleUsageIntervals),
                List.copyOf(activityIntervals),
                List.copyOf(supportEvents),
                List.copyOf(warnings)
        );
        Map<String, DriverExtractionSession> driversByKey = Map.of(driverKey, driverSession);
        ExtractionStats stats = new ExtractionStats(
                1,
                activityIntervals.size(),
                vehicleUsageIntervals.size(),
                registrationsByKey.size(),
                vehiclesByKey.size(),
                warnings.size()
        );
        return new TachographFileSession(
                UUID.randomUUID(),
                metadata,
                driversByKey,
                stats,
                List.copyOf(warnings),
                createdAt,
                expiresAt
        );
    }

    private ExtractedDriverCard extractDriverCard(Document document, List<ExtractionWarning> warnings) {
        Element identification = child(document.getDocumentElement(), "Identification");
        if (identification == null) {
            warnings.add(new ExtractionWarning("MISSING_IDENTIFICATION", "Driver card identification block is missing.", "/DriverCard"));
            return null;
        }
        Element cardIdentification = child(identification, "cardIdentification");
        String cardNation = childText(cardIdentification, "cardIssuingMemberState");
        String fullCardNumber = joinCardNumber(identification);
        String cardNumber = driverKeyFactory.canonicalCardNumber(fullCardNumber);
        String authority = childText(child(cardIdentification, "cardIssuingAuthorityName"), "name");
        return new ExtractedDriverCard(
                null,
                cardNation,
                cardNumber,
                fullCardNumber,
                authority,
                offsetDateTime(childText(cardIdentification, "cardIssueDate")),
                offsetDateTime(childText(cardIdentification, "cardValidityBegin")),
                offsetDateTime(childText(cardIdentification, "cardExpiryDate"))
        );
    }

    private ExtractedDriver extractDriver(Document document, String driverKey, List<ExtractionWarning> warnings) {
        Element identification = child(document.getDocumentElement(), "Identification");
        if (identification == null) {
            warnings.add(new ExtractionWarning("MISSING_DRIVER", "Driver holder identification block is missing.", "/DriverCard"));
            return new ExtractedDriver(
                    driverKey,
                    driverKeyFactory.createSourceDriverId(driverKey),
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null
            );
        }
        Element driverCardHolderIdentification = child(identification, "driverCardHolderIdentification");
        Element cardHolderName = child(driverCardHolderIdentification, "cardHolderName");
        Element licenceInfo = child(document.getDocumentElement(), "DrivingLicenseInfo");
        Element cardDrivingLicenseInformation = child(licenceInfo, "cardDrivingLicenseInformation");
        return new ExtractedDriver(
                driverKey,
                driverKeyFactory.createSourceDriverId(driverKey),
                childText(child(cardHolderName, "holderSurname"), "name"),
                childText(child(cardHolderName, "holderFirstNames"), "name"),
                localDate(child(driverCardHolderIdentification, "cardHolderBirthDate")),
                childText(driverCardHolderIdentification, "cardHolderPreferredLanguage"),
                childText(cardDrivingLicenseInformation, "drivingLicenceNumber"),
                childText(cardDrivingLicenseInformation, "drivingLicenceIssuingNation"),
                childText(child(cardDrivingLicenseInformation, "drivingLicenceIssuingAuthority"), "name")
        );
    }

    private List<ExtractedCardVehicleUsageInterval> extractVehicleUsageIntervals(
            Document document,
            Map<String, ExtractedVehicleRegistration> registrationsByKey,
            Map<String, ExtractedVehicle> vehiclesByKey,
            List<ExtractionWarning> warnings
    ) {
        Element cardVehiclesUsed = child(child(document.getDocumentElement(), "VehiclesUsed"), "cardVehiclesUsed");
        List<Element> records = children(cardVehiclesUsed, "cardVehicleRecords");
        List<ExtractedCardVehicleUsageInterval> intervals = new ArrayList<>(records.size());
        for (int i = 0; i < records.size(); i++) {
            Element record = records.get(i);
            String path = "/DriverCard/VehiclesUsed/cardVehiclesUsed/cardVehicleRecords[" + (i + 1) + "]";
            OffsetDateTime from = offsetDateTime(childText(record, "vehicleFirstUse"));
            OffsetDateTime to = offsetDateTime(childText(record, "vehicleLastUse"));
            if (from == null) {
                warnings.add(new ExtractionWarning("MISSING_VEHICLE_FIRST_USE", "Driver-card vehicle record is missing vehicleFirstUse.", path));
                continue;
            }
            if (to != null && to.isBefore(from)) {
                warnings.add(new ExtractionWarning("INVALID_VEHICLE_USE_INTERVAL", "Driver-card vehicle record has an invalid first/last use range.", path));
                continue;
            }
            Element vehicleRegistration = child(record, "vehicleRegistration");
            String registrationNation = childText(vehicleRegistration, "vehicleRegistrationNation");
            String registrationNumber = childText(child(vehicleRegistration, "vehicleRegistrationNumber"), "vehicleRegNumber");
            String registrationKey = vehicleKeyFactory.createRegistrationKey(registrationNation, registrationNumber);
            registrationsByKey.putIfAbsent(
                    registrationKey,
                    new ExtractedVehicleRegistration(
                            registrationKey,
                            vehicleKeyFactory.createSourceVehicleRegistrationId(registrationKey),
                            registrationNation,
                            registrationNumber
                    )
            );
            String vin = childText(record, "vehicleIdentificationNumber");
            String vehicleKey = vehicleKeyFactory.createVehicleKey(vin);
            if (vehicleKey != null) {
                vehiclesByKey.putIfAbsent(
                        vehicleKey,
                        new ExtractedVehicle(vehicleKey, vehicleKeyFactory.createSourceVehicleId(vehicleKey), vin)
                );
            }
            if (to == null) {
                warnings.add(new ExtractionWarning("OPEN_VEHICLE_USAGE", "Driver-card vehicle record has no vehicleLastUse and was kept open-ended.", path));
            }
            intervals.add(new ExtractedCardVehicleUsageInterval(
                    "CVU-" + (i + 1),
                    from,
                    to,
                    longValue(childText(record, "vehicleOdometerBegin")),
                    longValue(childText(record, "vehicleOdometerEnd")),
                    registrationKey,
                    vehicleKey,
                    path
            ));
        }
        intervals.sort(Comparator.comparing(ExtractedCardVehicleUsageInterval::from));
        return intervals;
    }

    private List<ExtractedCardActivityInterval> extractActivityIntervals(Document document, List<ExtractionWarning> warnings) {
        Element cardDriverActivity = child(child(document.getDocumentElement(), "DriverActivityData"), "cardDriverActivity");
        List<Element> dayRecords = children(cardDriverActivity, "cardActivityDailyRecord");
        List<ExtractedCardActivityInterval> intervals = new ArrayList<>(dayRecords.size() * 8);
        int intervalNo = 0;
        for (int dayIndex = 0; dayIndex < dayRecords.size(); dayIndex++) {
            Element dayRecord = dayRecords.get(dayIndex);
            String dayPath = "/DriverCard/DriverActivityData/cardDriverActivity/cardActivityDailyRecord[" + (dayIndex + 1) + "]";
            OffsetDateTime recordDate = offsetDateTime(childText(dayRecord, "activityRecordDate"));
            if (recordDate == null) {
                warnings.add(new ExtractionWarning("MISSING_ACTIVITY_RECORD_DATE", "Activity daily record has no activityRecordDate.", dayPath));
                continue;
            }
            LocalDate date = recordDate.withOffsetSameInstant(ZoneOffset.UTC).toLocalDate();
            List<Element> changes = children(dayRecord, "activityChangeInfos");
            List<ActivityChange> parsedChanges = new ArrayList<>(changes.size());
            for (int changeIndex = 0; changeIndex < changes.size(); changeIndex++) {
                Element change = changes.get(changeIndex);
                OffsetDateTime from = combine(date, childText(change, "timeOfChange"));
                if (from == null) {
                    warnings.add(new ExtractionWarning("INVALID_ACTIVITY_CHANGE_TIME", "Activity change has invalid timeOfChange.", dayPath + "/activityChangeInfos[" + (changeIndex + 1) + "]"));
                    continue;
                }
                parsedChanges.add(new ActivityChange(
                        from,
                        normalizeActivity(childText(change, "activity")),
                        normalizeToken(childText(change, "slot")),
                        normalizeToken(childText(change, "cardStatus")),
                        normalizeToken(childText(change, "drivingStatus")),
                        dayPath + "/activityChangeInfos[" + (changeIndex + 1) + "]"
                ));
            }
            parsedChanges.sort(Comparator.comparing(ActivityChange::from));
            for (int i = 0; i < parsedChanges.size(); i++) {
                ActivityChange current = parsedChanges.get(i);
                OffsetDateTime to = i + 1 < parsedChanges.size()
                        ? parsedChanges.get(i + 1).from()
                        : OffsetDateTime.of(date.plusDays(1), LocalTime.MIDNIGHT, ZoneOffset.UTC);
                if (!current.from().isBefore(to)) {
                    continue;
                }
                intervalNo++;
                intervals.add(new ExtractedCardActivityInterval(
                        "ACT-" + intervalNo,
                        current.from(),
                        to,
                        current.activityType(),
                        current.slot(),
                        current.cardStatus(),
                        current.drivingStatus(),
                        null,
                        null,
                        current.rawRecordPath()
                ));
            }
        }
        intervals.sort(Comparator.comparing(ExtractedCardActivityInterval::from));
        return intervals;
    }

    private List<ExtractedCardActivityInterval> assignVehicleCoverage(
            List<ExtractedCardActivityInterval> activityIntervals,
            List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals
    ) {
        List<ExtractedCardActivityInterval> result = new ArrayList<>(activityIntervals.size());
        int usageStartIndex = 0;
        for (ExtractedCardActivityInterval interval : activityIntervals) {
            while (usageStartIndex < vehicleUsageIntervals.size()
                    && !usageEndExclusive(vehicleUsageIntervals.get(usageStartIndex), interval.to()).isAfter(interval.from())) {
                usageStartIndex++;
            }

            int overlapEndIndex = usageStartIndex;
            while (overlapEndIndex < vehicleUsageIntervals.size()
                    && vehicleUsageIntervals.get(overlapEndIndex).from().isBefore(interval.to())) {
                overlapEndIndex++;
            }

            result.addAll(splitByVehicleCoverage(
                    interval,
                    vehicleUsageIntervals.subList(usageStartIndex, overlapEndIndex)
            ));
        }
        return result;
    }

    private List<ExtractedSupportEvent> extractSupportEvents(
            Document document,
            String driverKey,
            List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals,
            List<ExtractionWarning> warnings
    ) {
        VehicleUsageLookup vehicleUsageLookup = new VehicleUsageLookup(vehicleUsageIntervals);
        List<ExtractedSupportEvent> supportEvents = new ArrayList<>();
        Element root = document.getDocumentElement();
        extractCardPlaceSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
        extractCardGnssSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
        extractCardSpecificConditionSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
        extractCardBorderCrossingSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
        extractCardLoadUnloadSupportEvents(root, driverKey, vehicleUsageLookup, supportEvents, warnings);
        supportEvents.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt)
                .thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
                .thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)));
        return List.copyOf(supportEvents);
    }

    private void extractCardPlaceSupportEvents(
            Element root,
            String driverKey,
            VehicleUsageLookup vehicleUsageLookup,
            List<ExtractedSupportEvent> supportEvents,
            List<ExtractionWarning> warnings
    ) {
        List<Element> sections = children(root, "Places");
        for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
            Element cardPlaceDailyWorkPeriod = child(sections.get(sectionIndex), "cardPlaceDailyWorkPeriod");
            List<Element> records = children(cardPlaceDailyWorkPeriod, "placeRecords");
            for (int i = 0; i < records.size(); i++) {
                Element record = records.get(i);
                String path = "/DriverCard/Places[" + (sectionIndex + 1) + "]/cardPlaceDailyWorkPeriod/placeRecords[" + (i + 1) + "]";
                OffsetDateTime occurredAt = offsetDateTime(childText(record, "entryTime"));
                if (occurredAt == null) {
                    warnings.add(new ExtractionWarning("CARD_PLACE_MISSING_TIME", "Driver-card place record is missing entryTime.", path));
                    continue;
                }

                ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
                if (!hasKnownRegistration(usage)) {
                    warnings.add(new ExtractionWarning("CARD_PLACE_UNKNOWN_VRN", "Driver-card place record has unknown vehicle registration and was ignored.", path));
                    continue;
                }
                Element gnss = child(record, "entryGnssPlaceRecord");
                Element geoCoordinates = child(gnss, "geoCoordinates");
                BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
                BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
                String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
                String entryType = childText(record, "entryTypeDailyWorkPeriod");
                supportEvents.add(new ExtractedSupportEvent(
                        "CARDPLACE-" + (i + 1),
                        driverKey,
                        occurredAt,
                        "PLACE",
                        mapPlaceEntryType(entryType),
                        null,
                        null,
                        usage == null ? null : usage.registrationKey(),
                        usage == null ? null : usage.vehicleKey(),
                        childText(record, "dailyWorkPeriodCountry"),
                        childText(record, "dailyWorkPeriodRegion"),
                        null,
                        null,
                        null,
                        latitude,
                        longitude,
                        authenticationStatus,
                        longValue(childText(record, "vehicleOdometerValue")),
                        normalizeToken(entryType),
                        null,
                        null,
                        path
                ));
            }
        }
    }

    private void extractCardGnssSupportEvents(
            Element root,
            String driverKey,
            VehicleUsageLookup vehicleUsageLookup,
            List<ExtractedSupportEvent> supportEvents,
            List<ExtractionWarning> warnings
    ) {
        List<Element> sections = children(root, "GnssPlaces");
        for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
            Element gnssAccumulatedDriving = child(sections.get(sectionIndex), "gnssAccumulatedDriving");
            List<Element> records = children(gnssAccumulatedDriving, "gnssAccumulatedDrivingRecord");
            for (int i = 0; i < records.size(); i++) {
                Element record = records.get(i);
                String path = "/DriverCard/GnssPlaces[" + (sectionIndex + 1) + "]/gnssAccumulatedDriving/gnssAccumulatedDrivingRecord[" + (i + 1) + "]";
                OffsetDateTime occurredAt = offsetDateTime(childText(record, "timeStamp"));
                if (occurredAt == null) {
                    warnings.add(new ExtractionWarning("CARD_GNSS_MISSING_TIME", "Driver-card GNSS record is missing timeStamp.", path));
                    continue;
                }

                ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
                if (!hasKnownRegistration(usage)) {
                    warnings.add(new ExtractionWarning("CARD_GNSS_MISSING_VRN", "Driver-card GNSS record is missing vehicle registration.", path));
                    continue;
                }

                Element gnss = child(record, "gnssPlaceRecord");
                Element geoCoordinates = child(gnss, "geoCoordinates");
                BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
                BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
                String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
                supportEvents.add(new ExtractedSupportEvent(
                        "CARDGNSS-" + (i + 1),
                        driverKey,
                        occurredAt,
                        "POSITION",
                        "POSITION_RECORDED",
                        "SNAPSHOT",
                        null,
                        usage == null ? null : usage.registrationKey(),
                        usage == null ? null : usage.vehicleKey(),
                        null,
                        null,
                        null,
                        null,
                        null,
                        latitude,
                        longitude,
                        authenticationStatus,
                        longValue(childText(record, "vehicleOdometerValue")),
                        null,
                        null,
                        null,
                        path
                ));
            }
        }
    }

    private void extractCardSpecificConditionSupportEvents(
            Element root,
            String driverKey,
            VehicleUsageLookup vehicleUsageLookup,
            List<ExtractedSupportEvent> supportEvents,
            List<ExtractionWarning> warnings
    ) {
        List<Element> sections = children(root, "SpecificConditions");
        for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
            Element specificConditionData = child(sections.get(sectionIndex), "specificConditionData");
            List<Element> records = children(specificConditionData, "specificConditionRecord");
            for (int i = 0; i < records.size(); i++) {
                Element record = records.get(i);
                String path = "/DriverCard/SpecificConditions[" + (sectionIndex + 1) + "]/specificConditionData/specificConditionRecord[" + (i + 1) + "]";
                OffsetDateTime occurredAt = offsetDateTime(childText(record, "entryTime"));
                if (occurredAt == null) {
                    warnings.add(new ExtractionWarning("CARD_SPECIFIC_CONDITION_MISSING_TIME", "Driver-card specific-condition record is missing entryTime.", path));
                    continue;
                }

                ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
                if (!hasKnownRegistration(usage)) {
                    warnings.add(new ExtractionWarning("CARD_SPECIFIC_CONDITION_UNKNOWN_VRN", "Driver-card specific-condition record has unknown vehicle registration and was ignored.", path));
                    continue;
                }
                String conditionCode = normalizeToken(childText(record, "specificConditionType"));
                String[] specificCondition = mapSpecificCondition(conditionCode);
                supportEvents.add(new ExtractedSupportEvent(
                        "CARDSC-" + (i + 1),
                        driverKey,
                        occurredAt,
                        "SPECIFIC_CONDITION",
                        specificCondition[0],
                        specificCondition[1],
                        null,
                        usage == null ? null : usage.registrationKey(),
                        usage == null ? null : usage.vehicleKey(),
                        null,
                        null,
                        null,
                        null,
                        null,
                        null,
                        null,
                        null,
                        null,
                        conditionCode,
                        null,
                        null,
                        path
                ));
            }
        }
    }

    private void extractCardBorderCrossingSupportEvents(
            Element root,
            String driverKey,
            VehicleUsageLookup vehicleUsageLookup,
            List<ExtractedSupportEvent> supportEvents,
            List<ExtractionWarning> warnings
    ) {
        List<Element> sections = children(root, "BorderCrossings");
        for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
            Element cardBorderCrossings = child(sections.get(sectionIndex), "cardBorderCrossings");
            List<Element> records = children(cardBorderCrossings, "cardBorderCrossingRecord");
            for (int i = 0; i < records.size(); i++) {
                Element record = records.get(i);
                String path = "/DriverCard/BorderCrossings[" + (sectionIndex + 1) + "]/cardBorderCrossings/cardBorderCrossingRecord[" + (i + 1) + "]";
                Element gnss = child(record, "gnssPlaceAuthRecord");
                OffsetDateTime occurredAt = offsetDateTime(childText(gnss, "timeStamp"));
                if (occurredAt == null) {
                    warnings.add(new ExtractionWarning("CARD_BORDER_CROSSING_MISSING_TIME", "Driver-card border-crossing record is missing gnssPlaceAuthRecord/timeStamp.", path));
                    continue;
                }

                ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
                if (!hasKnownRegistration(usage)) {
                    warnings.add(new ExtractionWarning("CARD_BORDER_CROSSING_UNKNOWN_VRN", "Driver-card border-crossing record has unknown vehicle registration and was ignored.", path));
                    continue;
                }
                Element geoCoordinates = child(gnss, "geoCoordinates");
                BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
                BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
                String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
                supportEvents.add(new ExtractedSupportEvent(
                        "CARDBORDER-" + (i + 1),
                        driverKey,
                        occurredAt,
                        "BORDER_CROSSING",
                        "BORDER_OUTBOUND",
                        "OUTBOUND",
                        null,
                        usage == null ? null : usage.registrationKey(),
                        usage == null ? null : usage.vehicleKey(),
                        null,
                        null,
                        childText(record, "countryLeft"),
                        childText(record, "countryEntered"),
                        null,
                        latitude,
                        longitude,
                        authenticationStatus,
                        longValue(childText(record, "vehicleOdometerValue")),
                        null,
                        null,
                        null,
                        path
                ));
            }
        }
    }

    private void extractCardLoadUnloadSupportEvents(
            Element root,
            String driverKey,
            VehicleUsageLookup vehicleUsageLookup,
            List<ExtractedSupportEvent> supportEvents,
            List<ExtractionWarning> warnings
    ) {
        List<Element> sections = children(root, "LoadUnloadOperations");
        for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
            Element cardLoadUnloadOperations = child(sections.get(sectionIndex), "cardLoadUnloadOperations");
            List<Element> records = children(cardLoadUnloadOperations, "cardLoadUnloadRecord");
            for (int i = 0; i < records.size(); i++) {
                Element record = records.get(i);
                String path = "/DriverCard/LoadUnloadOperations[" + (sectionIndex + 1) + "]/cardLoadUnloadOperations/cardLoadUnloadRecord[" + (i + 1) + "]";
                OffsetDateTime occurredAt = offsetDateTime(childText(record, "timeStamp"));
                if (occurredAt == null) {
                    warnings.add(new ExtractionWarning("CARD_LOAD_UNLOAD_MISSING_TIME", "Driver-card load/unload record is missing timeStamp.", path));
                    continue;
                }

                ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
                if (!hasKnownRegistration(usage)) {
                    warnings.add(new ExtractionWarning("CARD_LOAD_UNLOAD_UNKNOWN_VRN", "Driver-card load/unload record has unknown vehicle registration and was ignored.", path));
                    continue;
                }
                Element gnss = child(record, "gnssPlaceAuthRecord");
                Element geoCoordinates = child(gnss, "geoCoordinates");
                BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
                BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
                String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
                String operation = mapOperation(childText(record, "operationType"));
                supportEvents.add(new ExtractedSupportEvent(
                        "CARDLOAD-" + (i + 1),
                        driverKey,
                        occurredAt,
                        "LOAD_UNLOAD",
                        operation,
                        "SNAPSHOT",
                        null,
                        usage == null ? null : usage.registrationKey(),
                        usage == null ? null : usage.vehicleKey(),
                        null,
                        null,
                        null,
                        null,
                        operation,
                        latitude,
                        longitude,
                        authenticationStatus,
                        longValue(childText(record, "vehicleOdometerValue")),
                        normalizeToken(childText(record, "operationType")),
                        null,
                        null,
                        path
                ));
            }
        }
    }

    private List<ExtractedCardActivityInterval> splitByVehicleCoverage(
            ExtractedCardActivityInterval interval,
            List<ExtractedCardVehicleUsageInterval> overlappingUsages
    ) {
        TreeSet<OffsetDateTime> cutPoints = new TreeSet<>();
        cutPoints.add(interval.from());
        cutPoints.add(interval.to());

        for (ExtractedCardVehicleUsageInterval usage : overlappingUsages) {
            if (usage.from().isAfter(interval.from()) && usage.from().isBefore(interval.to())) {
                cutPoints.add(usage.from());
            }
            OffsetDateTime usageEndExclusive = usageEndExclusive(usage, interval.to());
            if (usageEndExclusive.isAfter(interval.from()) && usageEndExclusive.isBefore(interval.to())) {
                cutPoints.add(usageEndExclusive);
            }
        }

        List<OffsetDateTime> orderedCutPoints = List.copyOf(cutPoints);
        List<ExtractedCardActivityInterval> segments = new ArrayList<>(Math.max(1, orderedCutPoints.size() - 1));
        int coverageIndex = 0;
        for (int i = 0; i < orderedCutPoints.size() - 1; i++) {
            OffsetDateTime segmentFrom = orderedCutPoints.get(i);
            OffsetDateTime segmentTo = orderedCutPoints.get(i + 1);
            if (!segmentFrom.isBefore(segmentTo)) {
                continue;
            }

            while (coverageIndex < overlappingUsages.size()
                    && !usageEndExclusive(overlappingUsages.get(coverageIndex), interval.to()).isAfter(segmentFrom)) {
                coverageIndex++;
            }
            ExtractedCardVehicleUsageInterval covering = coverageIndex < overlappingUsages.size()
                    && covers(overlappingUsages.get(coverageIndex), segmentFrom)
                    ? overlappingUsages.get(coverageIndex)
                    : null;
            String intervalId = orderedCutPoints.size() == 2 ? interval.intervalId() : interval.intervalId() + "-" + (i + 1);
            segments.add(new ExtractedCardActivityInterval(
                    intervalId,
                    segmentFrom,
                    segmentTo,
                    interval.activityType(),
                    interval.slot(),
                    interval.cardStatus(),
                    interval.drivingStatus(),
                    covering == null ? null : covering.registrationKey(),
                    covering == null ? null : covering.vehicleKey(),
                    interval.rawRecordPath()
            ));
        }
        return segments;
    }

    private boolean covers(ExtractedCardVehicleUsageInterval usage, OffsetDateTime timestamp) {
        return !usage.from().isAfter(timestamp) && timestamp.isBefore(usageEndExclusive(usage, null));
    }

    private boolean hasKnownRegistration(ExtractedCardVehicleUsageInterval usage) {
        return usage != null && hasKnownRegistrationKey(usage.registrationKey());
    }

    private boolean hasKnownRegistrationKey(String registrationKey) {
        return registrationKey != null
                && !registrationKey.isBlank()
                && !registrationKey.startsWith("UNKNOWN:")
                && !registrationKey.endsWith(":UNKNOWN");
    }

    private OffsetDateTime usageEndExclusive(ExtractedCardVehicleUsageInterval usage, OffsetDateTime fallbackExclusiveEnd) {
        if (usage.to() == null) {
            return fallbackExclusiveEnd == null ? OffsetDateTime.MAX : fallbackExclusiveEnd;
        }
        return usage.to().plusSeconds(1);
    }

    private BigDecimal geoCoordinate(Element geoCoordinates, String component, boolean latitude) {
        String value = childText(geoCoordinates, component);
        if (value == null || value.isBlank()) {
            return null;
        }
        BigDecimal raw = new BigDecimal(value);
        BigDecimal abs = raw.abs();
        BigDecimal degreeThreshold = BigDecimal.valueOf(latitude ? 90D : 180D);
        if (abs.compareTo(degreeThreshold) <= 0) {
            return raw;
        }
        BigDecimal sign = raw.signum() < 0 ? BigDecimal.valueOf(-1L) : BigDecimal.ONE;
        BigDecimal absolute = raw.abs();
        BigDecimal[] degreeAndRemainder = absolute.divideAndRemainder(BigDecimal.valueOf(1000L));
        BigDecimal degrees = degreeAndRemainder[0];
        BigDecimal minutes = degreeAndRemainder[1].divide(BigDecimal.TEN);
        BigDecimal decimalDegrees = degrees.add(minutes.divide(BigDecimal.valueOf(60L), 8, java.math.RoundingMode.HALF_UP));
        return decimalDegrees.multiply(sign);
    }

    private Element child(Element parent, String name) {
        if (parent == null) {
            return null;
        }
        NodeList childNodes = parent.getChildNodes();
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node child = childNodes.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE && name.equals(child.getNodeName())) {
                return (Element) child;
            }
        }
        return null;
    }

    private List<Element> children(Element parent, String name) {
        if (parent == null) {
            return List.of();
        }
        NodeList childNodes = parent.getChildNodes();
        List<Element> children = new ArrayList<>();
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node child = childNodes.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE && name.equals(child.getNodeName())) {
                children.add((Element) child);
            }
        }
        return children;
    }

    private String childText(Element parent, String name) {
        Element child = child(parent, name);
        if (child == null) {
            return null;
        }
        String value = child.getTextContent();
        return value == null || value.isBlank() ? null : value.trim();
    }

    private OffsetDateTime offsetDateTime(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        return OffsetDateTime.parse(value.trim()).withOffsetSameInstant(ZoneOffset.UTC);
    }

    private LocalDate localDate(Element dateElement) {
        if (dateElement == null) {
            return null;
        }
        String year = childText(dateElement, "year");
        String month = childText(dateElement, "month");
        String day = childText(dateElement, "day");
        if (year == null || month == null || day == null) {
            return null;
        }
        return LocalDate.of(Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day));
    }

    private OffsetDateTime combine(LocalDate date, String timeText) {
        return TachographTimeParser.combineUtc(date, timeText);
    }

    private Long longValue(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        return Long.parseLong(value.trim());
    }

    private String normalizeActivity(String value) {
        String normalized = normalizeToken(value);
        if (normalized == null) {
            return "UNKNOWN_ACTIVITY";
        }
        return switch (normalized) {
            case "DRIVING", "DRIVE" -> "DRIVE";
            case "WORK" -> "WORK";
            case "AVAILABILITY", "AVAILABLE" -> "AVAILABILITY";
            case "BREAK_REST", "BREAK/REST", "REST" -> "BREAK_REST";
            default -> "UNKNOWN_ACTIVITY";
        };
    }

    private String normalizeToken(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        return value.trim().toUpperCase().replace('-', '_').replace(' ', '_');
    }

    private String mapPlaceEntryType(String entryType) {
        String normalized = normalizeToken(entryType);
        if (normalized == null) {
            return "DAILY_WORK_PERIOD_PLACE";
        }
        return switch (normalized) {
            case "0" -> "BEGIN_DAILY_WORK_PERIOD";
            case "1" -> "END_DAILY_WORK_PERIOD";
            case "2" -> "BEGIN_MANUAL_DAILY_WORK_PERIOD";
            case "3" -> "END_MANUAL_DAILY_WORK_PERIOD";
            case "4" -> "BEGIN_ASSUMED_DAILY_WORK_PERIOD";
            case "5" -> "END_ASSUMED_DAILY_WORK_PERIOD";
            case "6" -> "BEGIN_GNSS_DAILY_WORK_PERIOD";
            case "7" -> "END_GNSS_DAILY_WORK_PERIOD";
            default -> "DAILY_WORK_PERIOD_PLACE";
        };
    }

    private String[] mapSpecificCondition(String conditionCode) {
        String normalized = normalizeToken(conditionCode);
        if (normalized == null) {
            return new String[]{"UNKNOWN_EVENT", "SNAPSHOT"};
        }
        return switch (normalized) {
            case "1" -> new String[]{"OUT", "BEGIN"};
            case "2" -> new String[]{"OUT", "END"};
            case "3" -> new String[]{"FERRY_TRAIN", "BEGIN"};
            case "4" -> new String[]{"FERRY_TRAIN", "END"};
            default -> new String[]{"UNKNOWN_EVENT", "SNAPSHOT"};
        };
    }

    private String mapOperation(String operationType) {
        String normalized = normalizeToken(operationType);
        if (normalized == null) {
            return "LOAD_UNLOAD";
        }
        return switch (normalized) {
            case "1", "LOAD" -> "LOAD";
            case "2", "UNLOAD" -> "UNLOAD";
            default -> "LOAD_UNLOAD";
        };
    }

    private record ActivityChange(
            OffsetDateTime from,
            String activityType,
            String slot,
            String cardStatus,
            String drivingStatus,
            String rawRecordPath
    ) {
    }

    private String joinCardNumber(Element identification) {
        Element cardIdentification = child(identification, "cardIdentification");
        Element cardNumber = child(cardIdentification, "cardNumber");
        String driverIdentification = childText(cardNumber, "driverIdentification");
        if (driverIdentification == null) {
            return null;
        }
        String replacement = childText(cardNumber, "cardReplacementIndex");
        String renewal = childText(cardNumber, "cardRenewalIndex");
        StringBuilder builder = new StringBuilder(driverIdentification);
        if (replacement != null) {
            builder.append(replacement);
        }
        if (renewal != null) {
            builder.append(renewal);
        }
        return builder.toString();
    }

    private final class VehicleUsageLookup {
        private final List<ExtractedCardVehicleUsageInterval> intervals;

        private VehicleUsageLookup(List<ExtractedCardVehicleUsageInterval> intervals) {
            this.intervals = intervals == null ? List.of() : intervals;
        }

        private ExtractedCardVehicleUsageInterval resolve(OffsetDateTime occurredAt) {
            if (occurredAt == null || intervals.isEmpty()) {
                return null;
            }
            int low = 0;
            int high = intervals.size() - 1;
            while (low <= high) {
                int mid = (low + high) >>> 1;
                ExtractedCardVehicleUsageInterval interval = intervals.get(mid);
                if (interval.from().isAfter(occurredAt)) {
                    high = mid - 1;
                    continue;
                }
                if (covers(interval, occurredAt)) {
                    return interval;
                }
                low = mid + 1;
            }
            for (int i = Math.min(high, intervals.size() - 1); i >= 0; i--) {
                ExtractedCardVehicleUsageInterval interval = intervals.get(i);
                if (covers(interval, occurredAt)) {
                    return interval;
                }
                if (interval.from().isBefore(occurredAt) && usageEndExclusive(interval, null).isBefore(occurredAt)) {
                    break;
                }
            }
            return null;
        }
    }
}
