package com.cameleer.server.core.ingestion;

import com.cameleer.common.model.ExchangeSnapshot;
import com.cameleer.common.model.ExecutionChunk;
import com.cameleer.common.model.FlatProcessorRecord;
import com.cameleer.server.core.storage.DiagramStore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * Accumulates {@link ExecutionChunk} documents and produces:
 * <ul>
 *   <li>{@link ProcessorBatch} — pushed immediately for each chunk (append-only)</li>
 *   <li>{@link MergedExecution} — pushed when the final chunk arrives or on stale sweep</li>
 * </ul>
 */
public class ChunkAccumulator {

    private static final Logger log = LoggerFactory.getLogger(ChunkAccumulator.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();

    private final String tenantId;
    private final Consumer<MergedExecution> executionSink;
    private final Consumer<ProcessorBatch> processorSink;
    private final DiagramStore diagramStore;
    private final Duration staleThreshold;
    private final Function<String, String> environmentResolver;
    private final ConcurrentHashMap<String, PendingExchange> pending = new ConcurrentHashMap<>();

    public ChunkAccumulator(String tenantId,
                            Consumer<MergedExecution> executionSink,
                            Consumer<ProcessorBatch> processorSink,
                            DiagramStore diagramStore,
                            Duration staleThreshold,
                            Function<String, String> environmentResolver) {
        this.tenantId = tenantId;
        this.executionSink = executionSink;
        this.processorSink = processorSink;
        this.diagramStore = diagramStore;
        this.staleThreshold = staleThreshold;
        this.environmentResolver = environmentResolver;
    }

    /**
     * Process an incoming chunk: push processors immediately,
     * buffer/merge the envelope, and emit when final.
     */
    public void onChunk(ExecutionChunk chunk) {
        // 1. Push processor records immediately (append-only)
        String environment = environmentResolver.apply(
                chunk.getInstanceId() != null ? chunk.getInstanceId() : "");
        boolean chunkHasTrace = false;
        if (chunk.getProcessors() != null && !chunk.getProcessors().isEmpty()) {
            processorSink.accept(new ProcessorBatch(
                    this.tenantId,
                    chunk.getExchangeId(),
                    chunk.getRouteId(),
                    chunk.getApplicationId(),
                    environment,
                    chunk.getStartTime(),
                    chunk.getProcessors()));
            chunkHasTrace = chunk.getProcessors().stream()
                    .anyMatch(p -> isNonEmpty(p.getInputBody()) || isNonEmpty(p.getOutputBody())
                            || (p.getInputHeaders() != null && !p.getInputHeaders().isEmpty())
                            || (p.getOutputHeaders() != null && !p.getOutputHeaders().isEmpty())
                            || (p.getInputProperties() != null && !p.getInputProperties().isEmpty())
                            || (p.getOutputProperties() != null && !p.getOutputProperties().isEmpty()));
        }

        // 2. Buffer/merge the exchange envelope
        if (chunk.isFinal()) {
            // Merge with any pending envelope, then emit
            PendingExchange existing = pending.remove(chunk.getExchangeId());
            ExecutionChunk merged = existing != null
                    ? mergeEnvelopes(existing.envelope(), chunk)
                    : chunk;
            boolean hasTrace = chunkHasTrace || (existing != null && existing.hasTraceData());
            executionSink.accept(toMergedExecution(merged, hasTrace));
        } else {
            // Buffer the envelope for later merging
            boolean trace = chunkHasTrace;
            pending.merge(chunk.getExchangeId(),
                    new PendingExchange(chunk, Instant.now(), trace),
                    (old, incoming) -> new PendingExchange(
                            mergeEnvelopes(old.envelope(), incoming.envelope()),
                            old.receivedAt(),
                            old.hasTraceData() || incoming.hasTraceData()));
        }
    }

    private static boolean isNonEmpty(String s) {
        return s != null && !s.isEmpty();
    }

    /**
     * Flush exchanges that have been pending longer than the stale threshold.
     * Called periodically by a scheduled task.
     */
    public void sweepStale() {
        Instant cutoff = Instant.now().minus(staleThreshold);
        pending.forEach((exchangeId, pe) -> {
            if (pe.receivedAt().isBefore(cutoff)) {
                PendingExchange removed = pending.remove(exchangeId);
                if (removed != null) {
                    log.info("Flushing stale exchange {} (pending since {})",
                            exchangeId, removed.receivedAt());
                    executionSink.accept(toMergedExecution(removed.envelope(), removed.hasTraceData()));
                }
            }
        });
    }

    /** Number of exchanges awaiting a final chunk. */
    public int getPendingCount() {
        return pending.size();
    }

    // ---- Merge logic ----

    /**
     * COALESCE merge: for each field, prefer the newer value if non-null, else keep older.
     * The newer chunk (higher chunkSeq) takes precedence for status, endTime, durationMs.
     */
    private static ExecutionChunk mergeEnvelopes(ExecutionChunk older, ExecutionChunk newer) {
        ExecutionChunk merged = new ExecutionChunk();
        merged.setExchangeId(coalesce(newer.getExchangeId(), older.getExchangeId()));
        merged.setApplicationId(coalesce(newer.getApplicationId(), older.getApplicationId()));
        merged.setInstanceId(coalesce(newer.getInstanceId(), older.getInstanceId()));
        merged.setRouteId(coalesce(newer.getRouteId(), older.getRouteId()));
        merged.setCorrelationId(coalesce(newer.getCorrelationId(), older.getCorrelationId()));
        merged.setStatus(coalesce(newer.getStatus(), older.getStatus()));
        merged.setStartTime(coalesce(older.getStartTime(), newer.getStartTime()));  // prefer earliest startTime
        merged.setEndTime(coalesce(newer.getEndTime(), older.getEndTime()));
        merged.setDurationMs(coalesce(newer.getDurationMs(), older.getDurationMs()));
        merged.setEngineLevel(coalesce(newer.getEngineLevel(), older.getEngineLevel()));
        merged.setErrorMessage(coalesce(newer.getErrorMessage(), older.getErrorMessage()));
        merged.setErrorStackTrace(coalesce(newer.getErrorStackTrace(), older.getErrorStackTrace()));
        merged.setErrorType(coalesce(newer.getErrorType(), older.getErrorType()));
        merged.setErrorCategory(coalesce(newer.getErrorCategory(), older.getErrorCategory()));
        merged.setRootCauseType(coalesce(newer.getRootCauseType(), older.getRootCauseType()));
        merged.setRootCauseMessage(coalesce(newer.getRootCauseMessage(), older.getRootCauseMessage()));
        merged.setAttributes(coalesce(newer.getAttributes(), older.getAttributes()));
        merged.setTraceId(coalesce(newer.getTraceId(), older.getTraceId()));
        merged.setSpanId(coalesce(newer.getSpanId(), older.getSpanId()));
        merged.setOriginalExchangeId(coalesce(newer.getOriginalExchangeId(), older.getOriginalExchangeId()));
        merged.setReplayExchangeId(coalesce(newer.getReplayExchangeId(), older.getReplayExchangeId()));
        merged.setInputSnapshot(coalesce(newer.getInputSnapshot(), older.getInputSnapshot()));
        merged.setOutputSnapshot(coalesce(newer.getOutputSnapshot(), older.getOutputSnapshot()));
        merged.setChunkSeq(Math.max(newer.getChunkSeq(), older.getChunkSeq()));
        merged.setFinal(newer.isFinal() || older.isFinal());
        merged.setProcessors(List.of());  // processors are handled separately
        return merged;
    }

    private static <T> T coalesce(T a, T b) {
        return a != null ? a : b;
    }

    // ---- Conversion to MergedExecution ----

    private MergedExecution toMergedExecution(ExecutionChunk envelope, boolean hasTraceData) {
        String diagramHash = "";
        try {
            diagramHash = diagramStore
                    .findContentHashForRoute(envelope.getRouteId(), envelope.getInstanceId())
                    .orElse("");
        } catch (Exception e) {
            log.debug("Could not resolve diagram hash for route={}", envelope.getRouteId());
        }
        String env = environmentResolver.apply(
                envelope.getInstanceId() != null ? envelope.getInstanceId() : "");
        return new MergedExecution(
                this.tenantId,
                1L,
                envelope.getExchangeId(),
                envelope.getRouteId(),
                envelope.getInstanceId(),
                envelope.getApplicationId(),
                env,
                envelope.getStatus() != null ? envelope.getStatus().name() : "RUNNING",
                envelope.getCorrelationId(),
                envelope.getExchangeId(),
                envelope.getStartTime(),
                envelope.getEndTime(),
                envelope.getDurationMs(),
                envelope.getErrorMessage(),
                envelope.getErrorStackTrace(),
                envelope.getErrorType(),
                envelope.getErrorCategory(),
                envelope.getRootCauseType(),
                envelope.getRootCauseMessage(),
                diagramHash,
                envelope.getEngineLevel(),
                extractBody(envelope.getInputSnapshot()),
                extractBody(envelope.getOutputSnapshot()),
                extractHeaders(envelope.getInputSnapshot()),
                extractHeaders(envelope.getOutputSnapshot()),
                extractProperties(envelope.getInputSnapshot()),
                extractProperties(envelope.getOutputSnapshot()),
                serializeAttributes(envelope.getAttributes()),
                envelope.getTraceId(),
                envelope.getSpanId(),
                hasTraceData,
                envelope.getReplayExchangeId() != null,  // isReplay
                envelope.getOriginalExchangeId(),
                envelope.getReplayExchangeId()
        );
    }

    private static String extractBody(ExchangeSnapshot snapshot) {
        if (snapshot == null || snapshot.getBody() == null) return "";
        return snapshot.getBody();
    }

    private static String extractHeaders(ExchangeSnapshot snapshot) {
        if (snapshot == null || snapshot.getHeaders() == null) return "";
        try {
            return MAPPER.writeValueAsString(snapshot.getHeaders());
        } catch (JsonProcessingException e) {
            log.warn("Failed to serialize snapshot headers", e);
            return "";
        }
    }

    private static String extractProperties(ExchangeSnapshot snapshot) {
        if (snapshot == null || snapshot.getProperties() == null) return "";
        try {
            return MAPPER.writeValueAsString(snapshot.getProperties());
        } catch (JsonProcessingException e) {
            log.warn("Failed to serialize snapshot properties", e);
            return "";
        }
    }

    private static String serializeAttributes(Map<String, String> attributes) {
        if (attributes == null || attributes.isEmpty()) {
            return "{}";
        }
        try {
            return MAPPER.writeValueAsString(attributes);
        } catch (JsonProcessingException e) {
            log.warn("Failed to serialize attributes, falling back to empty object", e);
            return "{}";
        }
    }

    // ---- Inner types ----

    /**
     * A batch of processor records from a single chunk, ready for ClickHouse insertion.
     */
    public record ProcessorBatch(
            String tenantId,
            String executionId,
            String routeId,
            String applicationId,
            String environment,
            Instant execStartTime,
            List<FlatProcessorRecord> processors
    ) {}

    /**
     * Envelope buffered while waiting for the final chunk.
     */
    private record PendingExchange(ExecutionChunk envelope, Instant receivedAt, boolean hasTraceData) {}
}
