package com.cameleer.server.core.ingestion;

import com.cameleer.common.model.ExchangeSnapshot;
import com.cameleer.common.model.ProcessorExecution;
import com.cameleer.common.model.RouteExecution;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.cameleer.server.core.indexing.ExecutionUpdatedEvent;
import com.cameleer.server.core.storage.DiagramStore;
import com.cameleer.server.core.storage.ExecutionStore;
import com.cameleer.server.core.storage.ExecutionStore.ExecutionRecord;
import com.cameleer.server.core.storage.ExecutionStore.ProcessorRecord;
import com.cameleer.server.core.storage.model.MetricsSnapshot;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

public class IngestionService {

    private static final ObjectMapper JSON = new ObjectMapper()
            .findAndRegisterModules()
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

    private final ExecutionStore executionStore;
    private final DiagramStore diagramStore;
    private final WriteBuffer<MetricsSnapshot> metricsBuffer;
    private final Consumer<ExecutionUpdatedEvent> eventPublisher;
    private final int bodySizeLimit;

    public IngestionService(ExecutionStore executionStore,
                            DiagramStore diagramStore,
                            WriteBuffer<MetricsSnapshot> metricsBuffer,
                            Consumer<ExecutionUpdatedEvent> eventPublisher,
                            int bodySizeLimit) {
        this.executionStore = executionStore;
        this.diagramStore = diagramStore;
        this.metricsBuffer = metricsBuffer;
        this.eventPublisher = eventPublisher;
        this.bodySizeLimit = bodySizeLimit;
    }

    public void ingestExecution(String instanceId, String applicationId, RouteExecution execution) {
        ExecutionRecord record = toExecutionRecord(instanceId, applicationId, execution);
        executionStore.upsert(record);

        if (execution.getProcessors() != null && !execution.getProcessors().isEmpty()) {
            List<ProcessorRecord> processors = flattenProcessors(
                    execution.getProcessors(), record.executionId(),
                    record.startTime(), applicationId, execution.getRouteId(),
                    null, 0);
            executionStore.upsertProcessors(
                    record.executionId(), record.startTime(),
                    applicationId, execution.getRouteId(), processors);
        }

        eventPublisher.accept(new ExecutionUpdatedEvent(
                record.executionId(), record.startTime()));
    }

    public void ingestDiagram(TaggedDiagram diagram) {
        diagramStore.store(diagram);
    }

    public boolean acceptMetrics(List<MetricsSnapshot> metrics) {
        return metricsBuffer.offerBatch(metrics);
    }

    public int getMetricsBufferDepth() {
        return metricsBuffer.size();
    }

    public WriteBuffer<MetricsSnapshot> getMetricsBuffer() {
        return metricsBuffer;
    }

    private ExecutionRecord toExecutionRecord(String instanceId, String applicationId,
                                               RouteExecution exec) {
        String diagramHash = diagramStore
                .findContentHashForRoute(exec.getRouteId(), instanceId)
                .orElse("");

        // Extract route-level snapshots (critical for REGULAR mode where no processors are recorded)
        String inputBody = null;
        String outputBody = null;
        String inputHeaders = null;
        String outputHeaders = null;
        String inputProperties = null;
        String outputProperties = null;

        ExchangeSnapshot inputSnapshot = exec.getInputSnapshot();
        if (inputSnapshot != null) {
            inputBody = truncateBody(inputSnapshot.getBody());
            inputHeaders = toJson(inputSnapshot.getHeaders());
            inputProperties = toJson(inputSnapshot.getProperties());
        }

        ExchangeSnapshot outputSnapshot = exec.getOutputSnapshot();
        if (outputSnapshot != null) {
            outputBody = truncateBody(outputSnapshot.getBody());
            outputHeaders = toJson(outputSnapshot.getHeaders());
            outputProperties = toJson(outputSnapshot.getProperties());
        }

        boolean hasTraceData = hasAnyTraceData(exec.getProcessors());

        boolean isReplay = exec.getReplayExchangeId() != null;
        if (!isReplay && inputSnapshot != null && inputSnapshot.getHeaders() != null) {
            isReplay = "true".equalsIgnoreCase(
                    String.valueOf(inputSnapshot.getHeaders().get("X-Cameleer-Replay")));
        }

        return new ExecutionRecord(
                exec.getExchangeId(), exec.getRouteId(), instanceId, applicationId,
                null, // environment: legacy PG path; ClickHouse path uses MergedExecution with env resolved from registry
                exec.getStatus() != null ? exec.getStatus().name() : "RUNNING",
                exec.getCorrelationId(), exec.getExchangeId(),
                exec.getStartTime(), exec.getEndTime(),
                exec.getDurationMs(),
                exec.getErrorMessage(), exec.getErrorStackTrace(),
                diagramHash,
                exec.getEngineLevel(),
                inputBody, outputBody, inputHeaders, outputHeaders,
                inputProperties, outputProperties,
                toJson(exec.getAttributes()),
                exec.getErrorType(), exec.getErrorCategory(),
                exec.getRootCauseType(), exec.getRootCauseMessage(),
                exec.getTraceId(), exec.getSpanId(),
                toJsonObject(exec.getProcessors()),
                hasTraceData,
                isReplay
        );
    }

    private static boolean hasAnyTraceData(List<ProcessorExecution> processors) {
        if (processors == null) return false;
        for (ProcessorExecution p : processors) {
            if (p.getInputBody() != null || p.getOutputBody() != null
                    || p.getInputHeaders() != null || p.getOutputHeaders() != null
                    || p.getInputProperties() != null || p.getOutputProperties() != null) return true;
        }
        return false;
    }

    private List<ProcessorRecord> flattenProcessors(
            List<ProcessorExecution> processors, String executionId,
            java.time.Instant execStartTime, String applicationId, String routeId,
            String parentProcessorId, int depth) {
        List<ProcessorRecord> flat = new ArrayList<>();
        for (ProcessorExecution p : processors) {
            flat.add(new ProcessorRecord(
                    executionId, p.getProcessorId(), p.getProcessorType(),
                    applicationId, routeId,
                    depth, parentProcessorId,
                    p.getStatus() != null ? p.getStatus().name() : "RUNNING",
                    p.getStartTime() != null ? p.getStartTime() : execStartTime,
                    p.getEndTime(),
                    p.getDurationMs(),
                    p.getErrorMessage(), p.getErrorStackTrace(),
                    truncateBody(p.getInputBody()), truncateBody(p.getOutputBody()),
                    toJson(p.getInputHeaders()), toJson(p.getOutputHeaders()),
                    null, null, // inputProperties, outputProperties (not on ProcessorExecution)
                    toJson(p.getAttributes()),
                    null, null, null, null, null,
                    p.getResolvedEndpointUri(),
                    p.getErrorType(), p.getErrorCategory(),
                    p.getRootCauseType(), p.getRootCauseMessage(),
                    p.getErrorHandlerType(), p.getCircuitBreakerState(),
                    p.getFallbackTriggered(),
                    null, null, null, null, null, null
            ));
        }
        return flat;
    }

    private String truncateBody(String body) {
        if (body == null) return null;
        if (body.length() > bodySizeLimit) return body.substring(0, bodySizeLimit);
        return body;
    }

    private static String toJson(Map<String, String> headers) {
        if (headers == null) return null;
        try {
            return JSON.writeValueAsString(headers);
        } catch (JsonProcessingException e) {
            return "{}";
        }
    }

    private static String toJsonObject(Object obj) {
        if (obj == null) return null;
        try {
            return JSON.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            return null;
        }
    }
}
