package at.procon.ted.camel;

import at.procon.ted.config.TedProcessorProperties;
import at.procon.ted.model.entity.ProcurementDocument;
import at.procon.ted.model.entity.VectorizationStatus;
import at.procon.ted.repository.ProcurementDocumentRepository;
import at.procon.ted.service.VectorizationProcessorService;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
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.camel.model.dataformat.JsonLibrary;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.UUID;

/**
 * Apache Camel route for asynchronous document vectorization.
 *
 * Features:
 * - Async vectorization triggered after document processing
 * - Scheduled processing of pending vectorizations from database
 * - Direct REST calls to Python embedding service
 * - Error handling with retry mechanism
 *
 * @author Martin.Schweitzer@procon.co.at and claude.ai
 */
@Component
@ConditionalOnRuntimeMode(RuntimeMode.LEGACY)
@RequiredArgsConstructor
@Slf4j
public class VectorizationRoute extends RouteBuilder {

    private static final String ROUTE_ID_TRIGGER = "vectorization-trigger";
    private static final String ROUTE_ID_PROCESSOR = "vectorization-processor";
    private static final String ROUTE_ID_SCHEDULER = "vectorization-scheduler";

    private final TedProcessorProperties properties;
    private final ProcurementDocumentRepository documentRepository;
    private final VectorizationProcessorService vectorizationProcessorService;
    private final ObjectMapper objectMapper;

    /**
     * Creates thread pool for vectorization with highest priority.
     * Only 1 thread since only one embedding service is available.
     */
    private java.util.concurrent.ExecutorService executorService() {
        return java.util.concurrent.Executors.newFixedThreadPool(
            1,
            r -> {
                Thread thread = new Thread(r);
                thread.setName("ted-vectorization-" + thread.getId());
                thread.setDaemon(true);
                thread.setPriority(Thread.MAX_PRIORITY); // Highest priority
                return thread;
            }
        );
    }

    @Override
    public void configure() throws Exception {

        if (!properties.getVectorization().isEnabled()) {
            log.info("Vectorization is disabled, skipping route configuration");
            return;
        }

        log.info("Configuring vectorization routes (enabled=true, apiUrl={}, connectTimeout={}ms, socketTimeout={}ms, maxRetries={}, scheduler every 6s)",
                properties.getVectorization().getApiUrl(),
                properties.getVectorization().getConnectTimeout(),
                properties.getVectorization().getSocketTimeout(),
                properties.getVectorization().getMaxRetries());

        // Global error handler for unexpected exceptions (like NullPointer, Connection pool shutdown, etc.)
        // Only catches severe exceptions that are not handled by route-specific doCatch
        onException(NullPointerException.class, IllegalStateException.class)
            .routeId("vectorization-error-handler")
            .handled(true)
            .process(exchange -> {
                UUID documentId = exchange.getIn().getHeader("documentId", UUID.class);
                Exception exception = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class);
                String errorMsg = exception != null ? exception.getClass().getSimpleName() + ": " + exception.getMessage() : "Unknown error";

                // If connection pool is shut down, it's likely during application shutdown - just log warning
                if (errorMsg.contains("Connection pool shut down")) {
                    log.warn("Vectorization aborted for document {} - connection pool shut down (application shutting down?)", documentId);
                    return;
                }

                log.error("Unexpected error in vectorization for document {}: {}", documentId, errorMsg, exception);

                // Update document status to FAILED via service (transactional)
                if (documentId != null) {
                    try {
                        vectorizationProcessorService.markAsFailed(documentId, errorMsg);
                    } catch (Exception e) {
                        log.warn("Failed to mark document {} as failed: {}", documentId, e.getMessage());
                    }
                }
            })
            .to("log:vectorization-error?level=WARN");

        // Trigger route: Receives document ID and queues for async processing
        // Queue size limited to 1000 to prevent memory issues
        from("direct:vectorize")
            .routeId(ROUTE_ID_TRIGGER)
            .doTry()
                .to("seda:vectorize-async?waitForTaskToComplete=Never&size=1000&blockWhenFull=true&timeout=5000")
            .doCatch(Exception.class)
                .log(LoggingLevel.WARN, "Failed to queue document ${header.documentId} for vectorization (queue may be full or shutting down): ${exception.message}")
            .end();

        // Async processor route: Performs actual vectorization with highest priority
        // Uses dedicated single-thread pool with MAX_PRIORITY (1 thread for 1 embedding service)
        from("seda:vectorize-async?size=1000")
            .routeId(ROUTE_ID_PROCESSOR)
            .threads().executorService(executorService())
            .process(exchange -> {
                UUID documentId = exchange.getIn().getHeader("documentId", UUID.class);

                log.debug("Starting vectorization for document: {}", documentId);

                // Prepare document for vectorization (transactional)
                VectorizationProcessorService.DocumentContent docContent =
                        vectorizationProcessorService.prepareDocumentForVectorization(documentId);

                if (docContent == null) {
                    // Document was skipped (no content)
                    log.debug("Document {} has no content, skipping vectorization", documentId);
                    exchange.setProperty("skipVectorization", true);
                    return;
                }

                // Prepare request object
                EmbedRequest embedRequest = new EmbedRequest();
                embedRequest.text = docContent.textContent();
                embedRequest.isQuery = false;

                // Set headers and body for REST call
                exchange.getIn().setHeader("documentId", documentId);
                exchange.getIn().setHeader(Exchange.HTTP_METHOD, "POST");
                exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json");
                exchange.getIn().setBody(embedRequest);
            })
            .choice()
                .when(exchangeProperty("skipVectorization").isEqualTo(true))
                    .log(LoggingLevel.DEBUG, "Skipping vectorization (no content): ${header.documentId}")
                .otherwise()
                    // Marshal request to JSON
                    .marshal().json(JsonLibrary.Jackson)
                    // Initialize retry counter
                    .setProperty("retryCount", constant(0))
                    .setProperty("maxRetries", constant(properties.getVectorization().getMaxRetries()))
                    .setProperty("vectorizationSuccess", constant(false))
                    // Retry loop with exponential backoff
                    .loopDoWhile(simple("${exchangeProperty.vectorizationSuccess} == false && ${exchangeProperty.retryCount} < ${exchangeProperty.maxRetries}"))
                        .process(exchange -> {
                            Integer retryCount = exchange.getProperty("retryCount", Integer.class);
                            exchange.setProperty("retryCount", retryCount + 1);

                            // Exponential backoff: 2s, 4s, 8s, 16s, 32s
                            if (retryCount > 0) {
                                long backoffMs = (long) Math.pow(2, retryCount) * 1000;
                                UUID documentId = exchange.getIn().getHeader("documentId", UUID.class);
                                log.warn("Retry #{} for document {} after {}ms backoff", retryCount, documentId, backoffMs);
                                Thread.sleep(backoffMs);
                            }
                        })
                        .doTry()
                            // HTTP call with configurable timeouts
                            .toD(properties.getVectorization().getApiUrl() + "/embed?bridgeEndpoint=true&throwExceptionOnFailure=false&connectTimeout=" +
                                 properties.getVectorization().getConnectTimeout() + "&socketTimeout=" +
                                 properties.getVectorization().getSocketTimeout())
                            .process(exchange -> {
                                UUID documentId = exchange.getIn().getHeader("documentId", UUID.class);
                                Integer statusCode = exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class);

                                if (statusCode == null) {
                                    log.error("No response from embedding service for document {} (service may be down!)", documentId);
                                    throw new RuntimeException("Embedding service not reachable (no HTTP response)");
                                }

                                if (statusCode != 200) {
                                    String responseBody = exchange.getIn().getBody(String.class);
                                    String errorMsg = "HTTP " + statusCode + " from embedding service: " + responseBody;
                                    log.error("Embedding service error for document {}: {}", documentId, errorMsg);
                                    throw new RuntimeException(errorMsg);
                                }
                            })
                            .unmarshal().json(JsonLibrary.Jackson, EmbedResponse.class)
                            .process(exchange -> {
                                UUID documentId = exchange.getIn().getHeader("documentId", UUID.class);
                                EmbedResponse response = exchange.getIn().getBody(EmbedResponse.class);

                                if (response == null || response.embedding == null) {
                                    throw new RuntimeException("Embedding service returned null response");
                                }

                                log.debug("Successfully vectorized document {}: {} dimensions, {} tokens",
                                        documentId, response.dimensions, response.tokenCount);

                                // Save embedding with token count via service (transactional)
                                vectorizationProcessorService.saveEmbedding(documentId, response.embedding, response.tokenCount);

                                // Mark as successful to stop retry loop
                                exchange.setProperty("vectorizationSuccess", true);
                            })
                        .doCatch(Exception.class)
                            .process(exchange -> {
                                UUID documentId = exchange.getIn().getHeader("documentId", UUID.class);
                                Integer retryCount = exchange.getProperty("retryCount", Integer.class);
                                Integer maxRetries = exchange.getProperty("maxRetries", Integer.class);
                                Exception exception = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class);
                                String errorMsg = exception != null ? exception.getMessage() : "Unknown error";

                                // Check if error is due to shutdown
                                if (errorMsg != null && errorMsg.contains("Connection pool shut down")) {
                                    log.warn("Vectorization aborted for document {} - connection pool shut down (application shutting down)", documentId);
                                    // Don't mark as failed - it will be retried on next startup
                                    exchange.setProperty("vectorizationSuccess", true); // Stop retry loop
                                    return;
                                }

                                if (retryCount >= maxRetries) {
                                    log.error("Vectorization failed for document {} after {} retries: {}", documentId, maxRetries, errorMsg, exception);
                                    try {
                                        vectorizationProcessorService.markAsFailed(documentId, errorMsg);
                                    } catch (Exception e) {
                                        log.warn("Failed to mark document {} as failed (may be shutting down): {}", documentId, e.getMessage());
                                    }
                                } else {
                                    log.warn("Vectorization attempt #{} failed for document {}: {}", retryCount, documentId, errorMsg);
                                }
                            })
                        .end()
                    .end()
            .end();

        // Scheduled route: Process pending and failed vectorizations from database
        // Runs every 6 seconds to catch documents that need (re-)vectorization
        from("timer:vectorization-scheduler?period=6000&delay=500")
            .routeId(ROUTE_ID_SCHEDULER)
            .log(LoggingLevel.DEBUG, "Vectorization scheduler: Checking for pending/failed documents...")
            .process(exchange -> {
                int batchSize = properties.getVectorization().getBatchSize();

                // First get PENDING documents (highest priority)
                List<ProcurementDocument> pending = documentRepository.findByVectorizationStatus(
                    VectorizationStatus.PENDING,
                    PageRequest.of(0, batchSize)
                );

                // If no PENDING, get FAILED documents for retry
                List<ProcurementDocument> failed = List.of();
                if (pending.isEmpty()) {
                    failed = documentRepository.findByVectorizationStatus(
                        VectorizationStatus.FAILED,
                        PageRequest.of(0, batchSize)
                    );
                }

                List<ProcurementDocument> toProcess = !pending.isEmpty() ? pending : failed;

                if (!toProcess.isEmpty()) {
                    String status = !pending.isEmpty() ? "PENDING" : "FAILED";
                    log.debug("Processing {} {} vectorizations from database", toProcess.size(), status);
                    exchange.getIn().setBody(toProcess);
                } else {
                    exchange.setProperty("noPendingDocs", true);
                }
            })
            .choice()
                .when(exchangeProperty("noPendingDocs").isEqualTo(true))
                    .log(LoggingLevel.DEBUG, "Vectorization scheduler: No pending or failed vectorizations found")
                .otherwise()
                    .split(body())
                        .process(exchange -> {
                            ProcurementDocument doc = exchange.getIn().getBody(ProcurementDocument.class);
                            exchange.getIn().setHeader("documentId", doc.getId());
                        })
                        .to("direct:vectorize")
                    .end()
            .end();
    }

    /**
     * Request model for embedding service.
     * Matches Python FastAPI EmbedRequest model with snake_case field names.
     */
    public static class EmbedRequest {
        @JsonProperty("text")
        public String text;

        @JsonProperty("is_query")
        public boolean isQuery;

        public EmbedRequest() {}

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        @JsonProperty("is_query")
        public boolean isIsQuery() {
            return isQuery;
        }

        @JsonProperty("is_query")
        public void setIsQuery(boolean isQuery) {
            this.isQuery = isQuery;
        }
    }

    /**
     * Response model for embedding service.
     */
    public static class EmbedResponse {
        public float[] embedding;
        public int dimensions;
        @JsonProperty("token_count")
        public int tokenCount;

        public EmbedResponse() {}

        public float[] getEmbedding() {
            return embedding;
        }

        public void setEmbedding(float[] embedding) {
            this.embedding = embedding;
        }

        public int getDimensions() {
            return dimensions;
        }

        public void setDimensions(int dimensions) {
            this.dimensions = dimensions;
        }

        @JsonProperty("token_count")
        public int getTokenCount() {
            return tokenCount;
        }

        @JsonProperty("token_count")
        public void setTokenCount(int tokenCount) {
            this.tokenCount = tokenCount;
        }
    }

}
