package at.procon.eventhub.tachographfilesession.service;

import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto;
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverSummaryDto;
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionDeleteResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDriversResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
public class TachographFileSessionService {

    private static final Logger log = LoggerFactory.getLogger(TachographFileSessionService.class);

    private final EventHubProperties properties;
    private final TachographFileSessionRepository repository;
    private final LegalRequirementsClient legalRequirementsClient;
    private final TachographXmlParser tachographXmlParser;
    private final DriverCardXmlExtractionService driverCardExtractionService;
    private final VehicleUnitXmlExtractionService vehicleUnitExtractionService;

    public TachographFileSessionService(
            EventHubProperties properties,
            TachographFileSessionRepository repository,
            LegalRequirementsClient legalRequirementsClient,
            TachographXmlParser tachographXmlParser,
            DriverCardXmlExtractionService driverCardExtractionService,
            VehicleUnitXmlExtractionService vehicleUnitExtractionService
    ) {
        this.properties = properties;
        this.repository = repository;
        this.legalRequirementsClient = legalRequirementsClient;
        this.tachographXmlParser = tachographXmlParser;
        this.driverCardExtractionService = driverCardExtractionService;
        this.vehicleUnitExtractionService = vehicleUnitExtractionService;
    }

    public CreateTachographFileSessionResponse createSession(
            MultipartFile file,
            String tenantKey,
            String sourceInstanceKey,
            String sessionLabel
    ) {
        try {
            validateFile(file);
            byte[] fileBytes = file.getBytes();
            validateFileBytes(fileBytes);
            long uploadStartedAt = System.nanoTime();
            LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadTachographFile(fileBytes, file.getOriginalFilename());
            long uploadDurationMs = elapsedMillis(uploadStartedAt);

            long parseStartedAt = System.nanoTime();
            TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parse(uploadResult.xmlContent());
            long parseDurationMs = elapsedMillis(parseStartedAt);

            Instant createdAt = Instant.now();
            Instant expiresAt = createdAt.plus(properties.getTachographFileSession().getTtl());
            boolean driverCardFile = "DriverCard".equals(parsedXml.rootElementName());
            TachographFileSessionMetadata metadata = new TachographFileSessionMetadata(
                    tenant(tenantKey),
                    sourceInstance(sourceInstanceKey, driverCardFile),
                    blankToNull(sessionLabel),
                    file.getOriginalFilename(),
                    sha256(fileBytes),
                    fileBytes.length,
                    uploadResult.dataPackageId(),
                    sha256(uploadResult.xmlContent().getBytes(StandardCharsets.UTF_8)),
                    driverCardFile,
                    null
            );
            long extractStartedAt = System.nanoTime();
            TachographFileSession session = driverCardFile
                    ? driverCardExtractionService.extract(parsedXml, metadata, createdAt, expiresAt)
                    : vehicleUnitExtractionService.extract(parsedXml, metadata, createdAt, expiresAt);
            long extractDurationMs = elapsedMillis(extractStartedAt);

            log.info(
                    "Created tachograph file session timing: file='{}', type={}, uploadMs={}, parseMs={}, extractMs={}, sizeBytes={}",
                    file.getOriginalFilename(),
                    driverCardFile ? "DRIVER_CARD" : "VEHICLE_UNIT",
                    uploadDurationMs,
                    parseDurationMs,
                    extractDurationMs,
                    fileBytes.length
            );
            TachographFileSession saved = repository.save(session);
            return new CreateTachographFileSessionResponse(toSummary(saved));
        } catch (Exception e) {
            if (e instanceof RuntimeException runtimeException) {
                throw runtimeException;
            }
            throw new IllegalStateException("Failed to create tachograph file session.", e);
        }
    }

    public TachographFileSessionSummaryDto getSession(UUID sessionId) {
        return toSummary(requireSession(sessionId));
    }

    public TachographFileSessionListDriversResponse listDrivers(UUID sessionId) {
        TachographFileSession session = requireSession(sessionId);
        return new TachographFileSessionListDriversResponse(session.sessionId(), driverSummaries(session));
    }

    public TachographFileDriverDetailDto getDriver(UUID sessionId, String driverKey) {
        TachographFileSession session = requireSession(sessionId);
        DriverExtractionSession driver = session.driversByKey().get(driverKey);
        if (driver == null) {
            throw new DriverNotFoundInSessionException(sessionId, driverKey);
        }
        return new TachographFileDriverDetailDto(
                session.sessionId(),
                driver.driverKey(),
                driver.driver(),
                driver.driverCard(),
                driver.vehicleRegistrations(),
                driver.vehicles(),
                driver.cardVehicleUsageIntervals(),
                driver.cardActivityIntervals(),
                driver.supportEvents(),
                driver.warnings()
        );
    }

    public TachographFileSessionDeleteResponse deleteSession(UUID sessionId) {
        return new TachographFileSessionDeleteResponse(sessionId, repository.delete(sessionId));
    }

    private TachographFileSession requireSession(UUID sessionId) {
        return repository.find(sessionId).orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
    }

    private TachographFileSessionSummaryDto toSummary(TachographFileSession session) {
        return new TachographFileSessionSummaryDto(
                session.sessionId(),
                session.metadata().tenantKey(),
                session.metadata().sourceInstanceKey(),
                session.metadata().sessionLabel(),
                session.metadata().originalFileName(),
                session.metadata().driverCardFile(),
                session.metadata().legalRequirementsDataPackageId(),
                session.extractionStats(),
                driverSummaries(session),
                session.warnings(),
                session.createdAt(),
                session.expiresAt()
        );
    }

    private List<TachographFileDriverSummaryDto> driverSummaries(TachographFileSession session) {
        return session.driversByKey().values().stream()
                .sorted(Comparator.comparing(DriverExtractionSession::driverKey))
                .map(driver -> new TachographFileDriverSummaryDto(
                        driver.driverKey(),
                        driver.driver() == null ? null : driver.driver().surname(),
                        driver.driver() == null ? null : driver.driver().firstNames(),
                        driver.driverCard() == null ? null : driver.driverCard().cardNation(),
                        driver.driverCard() == null ? null : driver.driverCard().cardNumber(),
                        driver.cardActivityIntervals().size(),
                        driver.cardVehicleUsageIntervals().size()
                ))
                .toList();
    }

    private void validateFile(MultipartFile file) {
        if (file == null || file.isEmpty() || file.getSize() == 0L) {
            throw new IllegalArgumentException("Tachograph file must not be empty.");
        }
        if (file.getSize() > properties.getTachographFileSession().getMaxFileSizeBytes()) {
            throw new IllegalArgumentException("Tachograph file exceeds the configured size limit.");
        }
    }

    private void validateFileBytes(byte[] fileBytes) {
        if (fileBytes == null || fileBytes.length == 0) {
            throw new IllegalArgumentException("Tachograph file must not be empty.");
        }
        if (fileBytes.length > properties.getTachographFileSession().getMaxFileSizeBytes()) {
            throw new IllegalArgumentException("Tachograph file exceeds the configured size limit.");
        }
    }

    private String tenant(String tenantKey) {
        return blankToNull(tenantKey) == null ? "default" : tenantKey.trim();
    }

    private String sourceInstance(String sourceInstanceKey, boolean driverCardFile) {
        if (blankToNull(sourceInstanceKey) != null) {
            return sourceInstanceKey.trim();
        }
        return driverCardFile ? "legalrequirements-drivercard" : "legalrequirements-vehicleunit";
    }

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

    private String sha256(byte[] bytes) {
        try {
            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 digest is not available.", e);
        }
    }

    private long elapsedMillis(long startedAtNanos) {
        return (System.nanoTime() - startedAtNanos) / 1_000_000L;
    }
}
