package at.procon.ted.camel;

import at.procon.dip.ingestion.service.DocumentIngestionGateway;
import at.procon.dip.ingestion.spi.IngestionResult;
import at.procon.dip.ingestion.spi.OriginalContentStoragePolicy;
import at.procon.dip.ingestion.spi.SourceDescriptor;
import at.procon.ted.config.TedProcessorProperties;
import at.procon.ted.model.entity.TedDailyPackage;
import at.procon.ted.repository.TedDailyPackageRepository;
import at.procon.ted.service.BatchDocumentProcessingService;
import at.procon.ted.service.TedPackageDownloadService;
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
import at.procon.dip.runtime.config.RuntimeMode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.camel.Exchange;
import org.apache.camel.LoggingLevel;
import org.apache.camel.builder.RouteBuilder;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.time.OffsetDateTime;
import java.time.Year;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Camel-Native Route for automatic download of TED Daily Packages.
 *
 * Uses Camel HTTP Component for downloads and Enterprise Integration Patterns:
 * - Timer-based triggers
 * - Idempotent Consumer Pattern
 * - Content-Based Router
 * - Splitter Pattern
 * - Dead Letter Channel
 *
 * @author Martin.Schweitzer@procon.co.at and claude.ai
 */
@Component
@ConditionalOnProperty(name = "ted.download.enabled", havingValue = "true")
@RequiredArgsConstructor
@Slf4j
@ConditionalOnRuntimeMode(RuntimeMode.LEGACY)
public class TedPackageDownloadCamelRoute extends RouteBuilder {

    private static final String ROUTE_ID_SCHEDULER = "ted-package-scheduler";
    private static final String ROUTE_ID_DOWNLOADER = "ted-package-http-downloader";
    private static final String ROUTE_ID_EXTRACTOR = "ted-package-extractor";
    private static final String ROUTE_ID_XML_SPLITTER = "ted-package-xml-splitter";

    private final TedProcessorProperties properties;
    private final TedDailyPackageRepository packageRepository;
    private final TedPackageDownloadService downloadService;
    private final DocumentIngestionGateway documentIngestionGateway;
    private final BatchDocumentProcessingService batchProcessingService;

    /**
     * Creates thread pool for parallel XML processing.
     * Maximum 1 parallel process to prevent OutOfMemory.
     */
    private java.util.concurrent.ExecutorService executorService() {
        return java.util.concurrent.Executors.newFixedThreadPool(
            1,
            r -> {
                Thread thread = new Thread(r);
                thread.setName("ted-xml-processor-" + thread.getId());
                thread.setDaemon(true);
                thread.setPriority(Thread.NORM_PRIORITY - 1);
                return thread;
            }
        );
    }

    @Override
    public void configure() throws Exception {

        // Error Handler with Dead Letter Channel
        errorHandler(deadLetterChannel("direct:package-download-error")
                .maximumRedeliveries(3)
                .redeliveryDelay(10000)
                .retryAttemptedLogLevel(LoggingLevel.WARN)
                .logStackTrace(true));

        // Error Handler Route
        from("direct:package-download-error")
                .routeId("ted-package-error-handler")
                .log(LoggingLevel.ERROR, "Failed to process package: ${exception.message}")
                .process(this::handleDownloadError);

        // Timer-based Scheduler (starts immediately with delay=0)
        from("timer:ted-package-scheduler?period={{ted.download.poll-interval:120000}}&delay=0")
                .routeId(ROUTE_ID_SCHEDULER)
                .autoStartup(true)
                .log(LoggingLevel.INFO, "TED Package Scheduler: Checking for new packages...")
                .process(this::checkRunningPackages)
                .choice()
                    .when(header("tooManyRunning").isEqualTo(true))
                        .log(LoggingLevel.INFO, "Skipping download - already ${header.runningCount} packages in progress (max 2)")
                    .otherwise()
                        .process(this::determineNextPackage)
                        .choice()
                            .when(header("packageId").isNotNull())
                                .to("direct:download-package")
                            .otherwise()
                                .log(LoggingLevel.INFO, "No more packages to download - all complete")
                        .end()
                .end();

        // Package Download Route (HTTP)
        from("direct:download-package")
                .routeId(ROUTE_ID_DOWNLOADER)
                .log(LoggingLevel.INFO, "Starting download of package ${header.packageId}")
                .setHeader("downloadStartTime", constant(System.currentTimeMillis()))
                // Check if already downloaded
                .process(this::createPackageRecord)
                // Rate Limiting
                .delay(simple("{{ted.download.delay-between-downloads:5000}}"))
                // HTTP Download
                .setHeader(Exchange.HTTP_METHOD, constant("GET"))
                .setHeader("CamelHttpConnectionClose", constant(true))
                .toD("${header.downloadUrl}?bridgeEndpoint=true&throwExceptionOnFailure=false&socketTimeout={{ted.download.download-timeout:300000}}")
                .choice()
                    // HTTP 200: Success
                    .when(header(Exchange.HTTP_RESPONSE_CODE).isEqualTo(200))
                        .log(LoggingLevel.INFO, "Processing package ${header.packageId}")
                        .to("direct:process-downloaded-package")
                    // HTTP 404: Not Found
                    .when(header(Exchange.HTTP_RESPONSE_CODE).isEqualTo(404))
                        .log(LoggingLevel.DEBUG, "Package not found (404): ${header.packageId}")
                        .process(this::markPackageNotFound)
                    // Other HTTP errors
                    .otherwise()
                        .log(LoggingLevel.WARN, "HTTP ${header.CamelHttpResponseCode} for package ${header.packageId}")
                        .process(this::markPackageFailed)
                .end();

        // Downloaded Package Processing
        from("direct:process-downloaded-package")
                .routeId("ted-package-processor")
                .process(this::calculateHash)
                .process(this::checkDuplicateByHash)
                .choice()
                    .when(header("isDuplicate").isEqualTo(true))
                        .log(LoggingLevel.WARN, "Duplicate package detected via hash: ${header.packageId}")
                        .process(this::markPackageDuplicate)
                    .otherwise()
                        .process(this::saveDownloadedPackage)
                        .choice()
                            .when(header("skipLegacyXmlProcessing").isEqualTo(true))
                                .log(LoggingLevel.INFO, "Package ${header.packageId}: generic ingestion gateway-only mode active, skipping legacy XML batch persistence")
                                .to("direct:complete-package-after-gateway")
                            .otherwise()
                                .to("direct:extract-tar-gz")
                        .endChoice()
                .end();

        from("direct:complete-package-after-gateway")
                .routeId("ted-package-gateway-only-completer")
                .setHeader("processingStartTime", constant(System.currentTimeMillis()))
                .process(this::markPackageCompleted)
                .process(this::logPackageStatistics);

        // tar.gz Extraction Route
        from("direct:extract-tar-gz")
                .routeId(ROUTE_ID_EXTRACTOR)
                .log(LoggingLevel.DEBUG, "Extracting package ${header.packageId}...")
                .process(this::extractTarGz)
                .choice()
                    .when(header("deleteAfterExtraction").isEqualTo(true))
                        .log(LoggingLevel.DEBUG, "Deleting tar.gz: ${header.downloadPath}")
                        .process(this::deleteTarGz)
                .end()
                .to("direct:split-xml-files");

        // XML Files Batch Processor
        from("direct:split-xml-files")
                .routeId(ROUTE_ID_XML_SPLITTER)
                .process(this::updatePackageProcessing)
                .setHeader("processingStartTime", constant(System.currentTimeMillis()))
                .process(this::processBatchDocuments)
                .process(this::markPackageCompleted)
                .process(this::logPackageStatistics);
    }

    /**
     * Checks how many packages are currently being processed.
     * Resets stuck packages (older than 30 minutes) to PENDING.
     * Sets header "tooManyRunning" to true if 3 or more packages are in progress.
     */
    private void checkRunningPackages(Exchange exchange) {
        OffsetDateTime thirtyMinutesAgo = OffsetDateTime.now().minusMinutes(30);

        // Find stuck DOWNLOADING packages
        List<TedDailyPackage> stuckDownloading = packageRepository.findByDownloadStatus(
            TedDailyPackage.DownloadStatus.DOWNLOADING).stream()
            .filter(pkg -> pkg.getUpdatedAt() != null && pkg.getUpdatedAt().isBefore(thirtyMinutesAgo))
            .toList();

        // Find stuck PROCESSING packages
        List<TedDailyPackage> stuckProcessing = packageRepository.findByDownloadStatus(
            TedDailyPackage.DownloadStatus.PROCESSING).stream()
            .filter(pkg -> pkg.getUpdatedAt() != null && pkg.getUpdatedAt().isBefore(thirtyMinutesAgo))
            .toList();

        List<TedDailyPackage> stuckPackages = new ArrayList<>();
        stuckPackages.addAll(stuckDownloading);
        stuckPackages.addAll(stuckProcessing);

        if (!stuckPackages.isEmpty()) {
            log.warn("Found {} stuck packages (older than 30 minutes), resetting to PENDING", stuckPackages.size());
            stuckPackages.forEach(pkg -> {
                log.warn("Resetting stuck package: {} (status: {}, last update: {})",
                    pkg.getPackageIdentifier(), pkg.getDownloadStatus(), pkg.getUpdatedAt());
                pkg.setDownloadStatus(TedDailyPackage.DownloadStatus.PENDING);
                pkg.setErrorMessage("Reset from stuck state");
                packageRepository.save(pkg);
            });
        }

        // Count currently running packages (after reset)
        long downloadingCount = packageRepository.findByDownloadStatus(
            TedDailyPackage.DownloadStatus.DOWNLOADING).size();
        long processingCount = packageRepository.findByDownloadStatus(
            TedDailyPackage.DownloadStatus.PROCESSING).size();
        long runningCount = downloadingCount + processingCount;

        exchange.getIn().setHeader("runningCount", runningCount);
        exchange.getIn().setHeader("tooManyRunning", runningCount >= 2);

        if (runningCount > 0) {
            log.info("Currently {} packages in progress ({} downloading, {} processing)",
                runningCount, downloadingCount, processingCount);
        }
    }

    /**
     * Determines the next package to download.
     * Strategy:
     * 1. First check for PENDING packages (previously failed/stuck)
     * 2. Then use download service strategy:
     *    - Current year: Forward from max(nr) until 404
     *    - All years: Fill gaps (if min(nr) > 1, then backward to 1)
     *    - If current year complete (min=1 and 404 after max) -> previous year
     *    - Repeat until startYear
     */
    private void determineNextPackage(Exchange exchange) {
        // First check for PENDING packages
        List<TedDailyPackage> pendingPackages = packageRepository.findByDownloadStatus(
            TedDailyPackage.DownloadStatus.PENDING);

        if (!pendingPackages.isEmpty()) {
            TedDailyPackage pkg = pendingPackages.get(0);
            log.info("Retrying PENDING package: {}", pkg.getPackageIdentifier());
            setPackageHeaders(exchange, pkg.getYear(), pkg.getSerialNumber());
            return;
        }

        // Use download service to find next package
        TedPackageDownloadService.PackageInfo packageInfo = downloadService.getNextPackageToDownload();

        if (packageInfo == null) {
            // No more packages
            exchange.getIn().setHeader("packageId", null);
            return;
        }

        setPackageHeaders(exchange, packageInfo.year(), packageInfo.serialNumber());
    }

    /**
     * Sets package headers for download.
     */
    private void setPackageHeaders(Exchange exchange, int year, int serialNumber) {
        String packageId = String.format("%04d%05d", year, serialNumber);
        String downloadUrl = properties.getDownload().getBaseUrl() + packageId;

        exchange.getIn().setHeader("packageId", packageId);
        exchange.getIn().setHeader("year", year);
        exchange.getIn().setHeader("serialNumber", serialNumber);
        exchange.getIn().setHeader("downloadUrl", downloadUrl);
    }

    /**
     * Creates package record in DB.
     */
    private void createPackageRecord(Exchange exchange) {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        Integer year = exchange.getIn().getHeader("year", Integer.class);
        Integer serialNumber = exchange.getIn().getHeader("serialNumber", Integer.class);
        String downloadUrl = exchange.getIn().getHeader("downloadUrl", String.class);

        // Check if already exists
        if (packageRepository.existsByPackageIdentifier(packageId)) {
            log.debug("Package {} already exists in DB", packageId);
            return;
        }

        TedDailyPackage pkg = TedDailyPackage.builder()
            .packageIdentifier(packageId)
            .year(year)
            .serialNumber(serialNumber)
            .downloadUrl(downloadUrl)
            .downloadStatus(TedDailyPackage.DownloadStatus.DOWNLOADING)
            .build();

        pkg = packageRepository.save(pkg);
        exchange.getIn().setHeader("packageDbId", pkg.getId());
    }

    /**
     * Calculates SHA-256 hash.
     */
    private void calculateHash(Exchange exchange) throws Exception {
        byte[] body = exchange.getIn().getBody(byte[].class);

        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashBytes = digest.digest(body);

        StringBuilder sb = new StringBuilder();
        for (byte b : hashBytes) {
            sb.append(String.format("%02x", b));
        }

        String hash = sb.toString();
        exchange.getIn().setHeader("fileHash", hash);
        log.debug("Calculated hash: {}", hash);
    }

    /**
     * Checks for duplicate via hash.
     */
    private void checkDuplicateByHash(Exchange exchange) {
        String hash = exchange.getIn().getHeader("fileHash", String.class);

        Optional<TedDailyPackage> duplicate = packageRepository.findAll().stream()
            .filter(p -> hash.equals(p.getFileHash()))
            .findFirst();

        exchange.getIn().setHeader("isDuplicate", duplicate.isPresent());
        if (duplicate.isPresent()) {
            exchange.getIn().setHeader("duplicateOf", duplicate.get().getPackageIdentifier());
        }
    }

    /**
     * Saves downloaded package.
     */
    private void saveDownloadedPackage(Exchange exchange) throws IOException {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        String hash = exchange.getIn().getHeader("fileHash", String.class);
        byte[] body = exchange.getIn().getBody(byte[].class);

        // Save tar.gz
        Path downloadDir = Paths.get(properties.getDownload().getDownloadDirectory());
        Files.createDirectories(downloadDir);
        Path downloadPath = downloadDir.resolve(packageId + ".tar.gz");
        Files.write(downloadPath, body);

        long downloadDuration = System.currentTimeMillis() -
            exchange.getIn().getHeader("downloadStartTime", Long.class);

        // Update DB
        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            pkg.setFileHash(hash);
            pkg.setDownloadStatus(TedDailyPackage.DownloadStatus.DOWNLOADED);
            pkg.setDownloadedAt(OffsetDateTime.now());
            pkg.setDownloadDurationMs(downloadDuration);
            packageRepository.save(pkg);
        });

        exchange.getIn().setHeader("downloadPath", downloadPath.toString());
        exchange.getIn().setHeader("deleteAfterExtraction",
            properties.getDownload().isDeleteAfterExtraction());

        exchange.getIn().setHeader("skipLegacyXmlProcessing", false);
        exchange.getIn().setBody(null);

        if (properties.getGenericIngestion().isEnabled() && properties.getGenericIngestion().isTedPackageAdapterEnabled()) {
            try {
                IngestionResult ingestionResult = documentIngestionGateway.ingest(new SourceDescriptor(
                        null,
                        at.procon.dip.domain.document.SourceType.TED_PACKAGE,
                        packageId,
                        downloadPath.toString(),
                        packageId + ".tar.gz",
                        "application/gzip",
                        null,
                        null,
                        OffsetDateTime.now(),
                        OriginalContentStoragePolicy.DEFAULT,
                        java.util.Map.of(
                                "packageId", packageId,
                                "title", packageId + ".tar.gz"
                        )
                ));

                int importedChildCount = Math.max(0, ingestionResult.documents().size() - 1);
                exchange.getIn().setHeader("gatewayImportedChildCount", importedChildCount);
                exchange.getIn().setHeader("gatewayImportWarnings", ingestionResult.warnings().size());

                if (properties.getGenericIngestion().isGatewayOnlyForTedPackages()) {
                    packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
                        pkg.setXmlFileCount(importedChildCount);
                        pkg.setProcessedCount(importedChildCount);
                        pkg.setFailedCount(0);
                        packageRepository.save(pkg);
                    });

                    if (properties.getDownload().isDeleteAfterExtraction()) {
                        Files.deleteIfExists(downloadPath);
                    }

                    exchange.getIn().setHeader("skipLegacyXmlProcessing", true);
                }
            } catch (Exception e) {
                log.warn("Phase 4.1 TED package adapter import failed for {}: {}", packageId, e.getMessage());
            }
        }
    }

    /**
     * Extracts tar.gz.
     */
    private void extractTarGz(Exchange exchange) throws IOException {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        String downloadPath = exchange.getIn().getHeader("downloadPath", String.class);

        Path tarGzFile = Paths.get(downloadPath);
        Path extractDir = Paths.get(properties.getDownload().getExtractDirectory())
            .resolve(packageId);
        Files.createDirectories(extractDir);

        List<Path> xmlFiles = new ArrayList<>();

        try (FileInputStream fis = new FileInputStream(tarGzFile.toFile());
             GzipCompressorInputStream gzis = new GzipCompressorInputStream(fis);
             TarArchiveInputStream tais = new TarArchiveInputStream(gzis)) {

            TarArchiveEntry entry;
            while ((entry = tais.getNextTarEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }

                String name = entry.getName();
                if (!name.toLowerCase().endsWith(".xml")) {
                    continue;
                }

                Path outputPath = extractDir.resolve(new File(name).getName());

                try (OutputStream os = Files.newOutputStream(outputPath)) {
                    byte[] buffer = new byte[8192];
                    int read;
                    while ((read = tais.read(buffer)) > 0) {
                        os.write(buffer, 0, read);
                    }
                }

                xmlFiles.add(outputPath);
            }
        }

        exchange.getIn().setHeader("xmlFiles", xmlFiles);
        exchange.getIn().setHeader("xmlFileCount", xmlFiles.size());

        // Update DB
        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            pkg.setXmlFileCount(xmlFiles.size());
            packageRepository.save(pkg);
        });

        log.debug("Extracted {} XML files from package {}", xmlFiles.size(), packageId);
    }

    /**
     * Deletes tar.gz.
     */
    private void deleteTarGz(Exchange exchange) throws IOException {
        String downloadPath = exchange.getIn().getHeader("downloadPath", String.class);
        Files.deleteIfExists(Paths.get(downloadPath));
    }

    /**
     * Process XML files in chunks to avoid connection leaks.
     * Splits large packages into batches of 100 files.
     */
    @SuppressWarnings("unchecked")
    private void processBatchDocuments(Exchange exchange) {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        List<Path> xmlFiles = (List<Path>) exchange.getIn().getHeader("xmlFiles");

        log.info("Package {}: Processing {} XML files in batches of 25", packageId, xmlFiles.size());

        int totalInserted = 0;
        int totalDuplicates = 0;
        int totalErrors = 0;
        long totalDuration = 0;

        // Process in chunks of 25 to avoid connection leaks (must complete in <60s)
        int chunkSize = 25;
        for (int i = 0; i < xmlFiles.size(); i += chunkSize) {
            int end = Math.min(i + chunkSize, xmlFiles.size());
            List<Path> chunk = xmlFiles.subList(i, end);

            log.debug("Package {}: Processing chunk {}-{} of {}",
                    packageId, i + 1, end, xmlFiles.size());

            // Process chunk in one transaction
            BatchDocumentProcessingService.BatchProcessingResult result =
                    batchProcessingService.processBatch(chunk);

            totalInserted += result.insertedCount();
            totalDuplicates += result.duplicateCount();
            totalErrors += result.errorCount();
            totalDuration += result.durationMs();

            // Update package statistics after each chunk
            updatePackageStatistics(packageId, totalInserted + totalDuplicates, totalErrors);
        }

        // Store final result in exchange for logging
        exchange.setProperty("processedCount", totalInserted + totalDuplicates);
        exchange.setProperty("failedCount", totalErrors);

        log.info("Package {}: Batch processing completed - {} inserted, {} duplicates, {} errors in {}ms",
                packageId, totalInserted, totalDuplicates, totalErrors, totalDuration);
    }

    /**
     * Update package processing statistics.
     */
    private void updatePackageStatistics(String packageId, int processedCount, int failedCount) {
        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            pkg.setProcessedCount(processedCount);
            pkg.setFailedCount(failedCount);
            packageRepository.save(pkg);
        });
    }

    /**
     * Updates status to PROCESSING.
     */
    private void updatePackageProcessing(Exchange exchange) {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            pkg.setDownloadStatus(TedDailyPackage.DownloadStatus.PROCESSING);
            packageRepository.save(pkg);
        });
    }

    /**
     * Marks package as COMPLETED and cleans up extracted XML files.
     */
    private void markPackageCompleted(Exchange exchange) {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        Long processingStartTime = exchange.getIn().getHeader("processingStartTime", Long.class);

        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            pkg.setDownloadStatus(TedDailyPackage.DownloadStatus.COMPLETED);
            pkg.setProcessedAt(OffsetDateTime.now());

            if (processingStartTime != null) {
                long processingDuration = System.currentTimeMillis() - processingStartTime;
                pkg.setProcessingDurationMs(processingDuration);
            }

            packageRepository.save(pkg);
        });

        // Clean up extracted XML files and package directory to free memory
        List<Path> xmlFiles = exchange.getIn().getHeader("xmlFiles", List.class);
        if (xmlFiles != null) {
            int deletedCount = 0;
            for (Path xmlFile : xmlFiles) {
                try {
                    if (Files.deleteIfExists(xmlFile)) {
                        deletedCount++;
                    }
                } catch (IOException e) {
                    log.warn("Failed to delete XML file {}: {}", xmlFile, e.getMessage());
                }
            }

            // Delete package directory if empty
            if (!xmlFiles.isEmpty()) {
                try {
                    Path packageDir = xmlFiles.get(0).getParent();
                    if (packageDir != null && Files.isDirectory(packageDir)) {
                        try (var stream = Files.list(packageDir)) {
                            if (stream.findAny().isEmpty()) {
                                Files.deleteIfExists(packageDir);
                                log.debug("Deleted empty package directory: {}", packageDir);
                            }
                        }
                    }
                } catch (IOException e) {
                    log.debug("Could not delete package directory: {}", e.getMessage());
                }
            }

            if (deletedCount > 0) {
                log.debug("Cleaned up {} XML files for package {}", deletedCount, packageId);
            }
        }
    }

    /**
     * Marks package as NOT_FOUND.
     */
    private void markPackageNotFound(Exchange exchange) {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            pkg.setDownloadStatus(TedDailyPackage.DownloadStatus.NOT_FOUND);
            pkg.setErrorMessage("Package not found (404)");
            packageRepository.save(pkg);
        });
    }

    /**
     * Marks package as FAILED.
     */
    private void markPackageFailed(Exchange exchange) {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        Integer httpCode = exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class);
        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            pkg.setDownloadStatus(TedDailyPackage.DownloadStatus.FAILED);
            pkg.setErrorMessage("HTTP " + httpCode);
            packageRepository.save(pkg);
        });
    }

    /**
     * Marks package as DUPLICATE.
     */
    private void markPackageDuplicate(Exchange exchange) {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        String duplicateOf = exchange.getIn().getHeader("duplicateOf", String.class);
        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            pkg.setDownloadStatus(TedDailyPackage.DownloadStatus.COMPLETED);
            pkg.setErrorMessage("Duplicate of " + duplicateOf);
            packageRepository.save(pkg);
        });
    }

    /**
     * Logs package processing statistics.
     */
    private void logPackageStatistics(Exchange exchange) {
        String packageId = exchange.getIn().getHeader("packageId", String.class);
        packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
            long totalDuration = (pkg.getDownloadDurationMs() != null ? pkg.getDownloadDurationMs() : 0) +
                               (pkg.getProcessingDurationMs() != null ? pkg.getProcessingDurationMs() : 0);

            log.info("Package {} completed: {} XML files, {} processed, {} failed, total duration: {}ms",
                    packageId,
                    pkg.getXmlFileCount(),
                    pkg.getProcessedCount(),
                    pkg.getFailedCount(),
                    totalDuration);
        });
    }

    /**
     * Error Handler.
     */
    private void handleDownloadError(Exchange exchange) {
        Exception exception = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class);
        String packageId = exchange.getIn().getHeader("packageId", String.class);

        if (packageId != null) {
            packageRepository.findByPackageIdentifier(packageId).ifPresent(pkg -> {
                pkg.setDownloadStatus(TedDailyPackage.DownloadStatus.FAILED);
                pkg.setErrorMessage(exception != null ? exception.getMessage() : "Unknown error");
                packageRepository.save(pkg);
            });
        }
    }
}
