package at.procon.eventhub.tachographfilesession.service;

import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.service.UnifiedEventTimelineReconstructor;
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
import at.procon.eventhub.service.EventHubEventSorter;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionRequest;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographCompositeDriverEventsResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionListDriversResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionSummaryDto;
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverSummaryDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard;
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.springframework.stereotype.Service;

@Service
public class TachographCompositeSessionService {

    private final TachographCompositeSessionRepository compositeRepository;
    private final TachographFileSessionRepository fileSessionRepository;
    private final DriverTimelineEventBuilder driverTimelineEventBuilder;
    private final UnifiedEventTimelineReconstructor timelineReconstructor;
    private final EventAcquisitionRecordKeyService eventKeyService;
    private final EventHubEventSorter eventSorter;

    public TachographCompositeSessionService(
            TachographCompositeSessionRepository compositeRepository,
            TachographFileSessionRepository fileSessionRepository,
            DriverTimelineEventBuilder driverTimelineEventBuilder,
            UnifiedEventTimelineReconstructor timelineReconstructor,
            EventAcquisitionRecordKeyService eventKeyService,
            EventHubEventSorter eventSorter
    ) {
        this.compositeRepository = compositeRepository;
        this.fileSessionRepository = fileSessionRepository;
        this.driverTimelineEventBuilder = driverTimelineEventBuilder;
        this.timelineReconstructor = timelineReconstructor;
        this.eventKeyService = eventKeyService;
        this.eventSorter = eventSorter;
    }

    public CreateTachographCompositeSessionResponse createCompositeSession(CreateTachographCompositeSessionRequest request) {
        if (request == null || request.sessionIds() == null || request.sessionIds().isEmpty()) {
            throw new IllegalArgumentException("sessionIds must not be empty.");
        }
        List<UUID> memberSessionIds = request.sessionIds().stream()
                .filter(Objects::nonNull)
                .distinct()
                .toList();
        if (memberSessionIds.isEmpty()) {
            throw new IllegalArgumentException("sessionIds must not be empty.");
        }
        List<TachographFileSession> sessions = memberSessions(memberSessionIds);
        String tenantKey = sessions.stream()
                .map(session -> session.metadata() == null ? null : session.metadata().tenantKey())
                .filter(value -> value != null && !value.isBlank())
                .findFirst()
                .orElse("default");
        TachographCompositeSession compositeSession = compositeRepository.save(new TachographCompositeSession(
                UUID.randomUUID(),
                tenantKey,
                blankToNull(request.label()),
                memberSessionIds,
                Instant.now()
        ));
        return new CreateTachographCompositeSessionResponse(toSummary(compositeSession, sessions));
    }

    public TachographCompositeSessionSummaryDto getCompositeSession(UUID compositeSessionId) {
        TachographCompositeSession compositeSession = requireCompositeSession(compositeSessionId);
        return toSummary(compositeSession, memberSessions(compositeSession.memberSessionIds()));
    }

    public TachographCompositeSessionListDriversResponse listDrivers(UUID compositeSessionId) {
        TachographCompositeSession compositeSession = requireCompositeSession(compositeSessionId);
        return new TachographCompositeSessionListDriversResponse(
                compositeSessionId,
                aggregateDriverSummaries(memberSessions(compositeSession.memberSessionIds()))
        );
    }

    public TachographCompositeDriverEventsResponse getMergedDriverEvents(UUID compositeSessionId, String driverKey) {
        TachographCompositeSession compositeSession = requireCompositeSession(compositeSessionId);
        List<TachographFileSession> memberSessions = memberSessions(compositeSession.memberSessionIds());
        List<TachographFileSession> sourceSessions = sessionsWithDriver(memberSessions, driverKey);
        if (sourceSessions.isEmpty()) {
            throw new DriverNotFoundInCompositeSessionException(compositeSessionId, driverKey);
        }
        List<EventHubEventDto> mergedEvents = mergeDriverEvents(sourceSessions, driverKey);
        return new TachographCompositeDriverEventsResponse(
                compositeSessionId,
                driverKey,
                sourceSessions.stream().map(TachographFileSession::sessionId).toList(),
                mergedEvents.size(),
                mergedEvents
        );
    }

    public ResolvedDriverTimeline getMergedDriverTimeline(UUID compositeSessionId, String driverKey) {
        TachographCompositeSession compositeSession = requireCompositeSession(compositeSessionId);
        List<TachographFileSession> memberSessions = memberSessions(compositeSession.memberSessionIds());
        List<TachographFileSession> sourceSessions = sessionsWithDriver(memberSessions, driverKey);
        if (sourceSessions.isEmpty()) {
            throw new DriverNotFoundInCompositeSessionException(compositeSessionId, driverKey);
        }
        List<EventHubEventDto> mergedEvents = mergeDriverEvents(sourceSessions, driverKey);
        return timelineReconstructor.reconstruct(
                compositeSessionId,
                driverKey,
                mergedEvents,
                mergeWarnings(sourceSessions, driverKey),
                "COMPOSITE_TACHOGRAPH_FILE_SESSION"
        );
    }

    private TachographCompositeSession requireCompositeSession(UUID compositeSessionId) {
        return compositeRepository.find(compositeSessionId)
                .orElseThrow(() -> new TachographCompositeSessionNotFoundException(compositeSessionId));
    }

    private List<TachographFileSession> memberSessions(List<UUID> memberSessionIds) {
        List<TachographFileSession> sessions = new ArrayList<>();
        for (UUID sessionId : memberSessionIds) {
            sessions.add(fileSessionRepository.find(sessionId)
                    .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId)));
        }
        return List.copyOf(sessions);
    }

    private List<TachographFileSession> sessionsWithDriver(List<TachographFileSession> sessions, String driverKey) {
        return sessions.stream()
                .filter(session -> session.driversByKey() != null && session.driversByKey().containsKey(driverKey))
                .toList();
    }

    private List<EventHubEventDto> mergeDriverEvents(List<TachographFileSession> sessions, String driverKey) {
        LinkedHashMap<String, EventHubEventDto> bySignature = new LinkedHashMap<>();
        for (TachographFileSession session : sessions) {
            DriverExtractionSession driverSession = session.driversByKey().get(driverKey);
            if (driverSession == null) {
                continue;
            }
            for (EventHubEventDto event : driverTimelineEventBuilder.buildEvents(session, driverSession)) {
                bySignature.putIfAbsent(eventKeyService.buildEventSignatureHash(event), event);
            }
        }
        return eventSorter.sort(new ArrayList<>(bySignature.values()));
    }

    private List<ExtractionWarning> mergeWarnings(List<TachographFileSession> sessions, String driverKey) {
        LinkedHashSet<ExtractionWarning> warnings = new LinkedHashSet<>();
        for (TachographFileSession session : sessions) {
            if (session.warnings() != null) {
                warnings.addAll(session.warnings());
            }
            DriverExtractionSession driverSession = session.driversByKey().get(driverKey);
            if (driverSession != null && driverSession.warnings() != null) {
                warnings.addAll(driverSession.warnings());
            }
        }
        return List.copyOf(warnings);
    }

    private TachographCompositeSessionSummaryDto toSummary(
            TachographCompositeSession compositeSession,
            List<TachographFileSession> memberSessions
    ) {
        return new TachographCompositeSessionSummaryDto(
                compositeSession.compositeSessionId(),
                compositeSession.tenantKey(),
                compositeSession.label(),
                compositeSession.memberSessionIds(),
                aggregateDriverSummaries(memberSessions),
                compositeSession.createdAt()
        );
    }

    private List<TachographFileDriverSummaryDto> aggregateDriverSummaries(List<TachographFileSession> sessions) {
        Map<String, DriverSummaryAccumulator> byDriverKey = new LinkedHashMap<>();
        for (TachographFileSession session : sessions) {
            if (session.driversByKey() == null) {
                continue;
            }
            for (DriverExtractionSession driverSession : session.driversByKey().values()) {
                if (driverSession == null || driverSession.driverKey() == null) {
                    continue;
                }
                byDriverKey.computeIfAbsent(driverSession.driverKey(), ignored -> new DriverSummaryAccumulator(driverSession.driverKey()))
                        .accept(driverSession);
            }
        }
        return byDriverKey.values().stream()
                .map(DriverSummaryAccumulator::finish)
                .sorted(Comparator.comparing(TachographFileDriverSummaryDto::driverKey))
                .toList();
    }

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

    private static final class DriverSummaryAccumulator {
        private final String driverKey;
        private String surname;
        private String firstNames;
        private String cardNation;
        private String cardNumber;
        private int activityIntervalCount;
        private int cardVehicleUsageIntervalCount;

        private DriverSummaryAccumulator(String driverKey) {
            this.driverKey = driverKey;
        }

        private void accept(DriverExtractionSession session) {
            ExtractedDriver driver = session.driver();
            ExtractedDriverCard card = session.driverCard();
            if (surname == null && driver != null) {
                surname = driver.surname();
            }
            if (firstNames == null && driver != null) {
                firstNames = driver.firstNames();
            }
            if (cardNation == null && card != null) {
                cardNation = card.cardNation();
            }
            if (cardNumber == null && card != null) {
                cardNumber = card.cardNumber();
            }
            activityIntervalCount += session.cardActivityIntervals() == null ? 0 : session.cardActivityIntervals().size();
            cardVehicleUsageIntervalCount += session.cardVehicleUsageIntervals() == null ? 0 : session.cardVehicleUsageIntervals().size();
        }

        private TachographFileDriverSummaryDto finish() {
            return new TachographFileDriverSummaryDto(
                    driverKey,
                    surname,
                    firstNames,
                    cardNation,
                    cardNumber,
                    activityIntervalCount,
                    cardVehicleUsageIntervalCount
            );
        }
    }
}
