package com.cameleer.server.core.detail;

import com.cameleer.common.model.ProcessorExecution;
import com.cameleer.server.core.storage.ExecutionStore;
import com.cameleer.server.core.storage.ExecutionStore.ProcessorRecord;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.*;

public class DetailService {

    private static final ObjectMapper JSON = new ObjectMapper()
            .findAndRegisterModules()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    private static final TypeReference<Map<String, String>> STR_MAP = new TypeReference<>() {};
    private static final TypeReference<List<ProcessorExecution>> PROCESSOR_EXEC_LIST = new TypeReference<>() {};

    private final ExecutionStore executionStore;

    public DetailService(ExecutionStore executionStore) {
        this.executionStore = executionStore;
    }

    public Optional<ExecutionDetail> getDetail(String executionId) {
        return executionStore.findById(executionId)
                .map(exec -> {
                    // Prefer the raw processor tree (faithful to agent data) over
                    // flat-record reconstruction (which loses iteration context).
                    List<ProcessorNode> processors = parseProcessorsJson(exec.processorsJson());
                    if (processors.isEmpty()) {
                        // Fallback for executions ingested before processors_json was added
                        List<ProcessorRecord> records = executionStore.findProcessors(executionId);
                        processors = buildTree(records);
                    }
                    return new ExecutionDetail(
                            exec.executionId(), exec.routeId(), exec.instanceId(),
                            exec.applicationId(), exec.environment(),
                            exec.status(), exec.startTime(), exec.endTime(),
                            exec.durationMs() != null ? exec.durationMs() : 0L,
                            exec.correlationId(), exec.exchangeId(),
                            exec.errorMessage(), exec.errorStacktrace(),
                            exec.diagramContentHash(), processors,
                            exec.inputBody(), exec.outputBody(),
                            exec.inputHeaders(), exec.outputHeaders(),
                            exec.inputProperties(), exec.outputProperties(),
                            parseAttributes(exec.attributes()),
                            exec.errorType(), exec.errorCategory(),
                            exec.rootCauseType(), exec.rootCauseMessage(),
                            exec.traceId(), exec.spanId()
                    );
                });
    }

    public Optional<Map<String, String>> getProcessorSnapshot(String executionId, String processorId) {
        return executionStore.findProcessorById(executionId, processorId)
            .map(DetailService::snapshotFromRecord);
    }

    public Optional<Map<String, String>> getProcessorSnapshotBySeq(String executionId, int seq) {
        return executionStore.findProcessorBySeq(executionId, seq)
            .map(DetailService::snapshotFromRecord);
    }

    private static Map<String, String> snapshotFromRecord(ProcessorRecord p) {
        Map<String, String> snapshot = new LinkedHashMap<>();
        if (p.inputBody() != null) snapshot.put("inputBody", p.inputBody());
        if (p.outputBody() != null) snapshot.put("outputBody", p.outputBody());
        if (p.inputHeaders() != null) snapshot.put("inputHeaders", p.inputHeaders());
        if (p.outputHeaders() != null) snapshot.put("outputHeaders", p.outputHeaders());
        if (p.inputProperties() != null) snapshot.put("inputProperties", p.inputProperties());
        if (p.outputProperties() != null) snapshot.put("outputProperties", p.outputProperties());
        return snapshot;
    }

    /** Parse the raw processor tree JSON stored alongside the execution. */
    private List<ProcessorNode> parseProcessorsJson(String json) {
        if (json == null || json.isBlank()) return List.of();
        try {
            List<ProcessorExecution> executions = JSON.readValue(json, PROCESSOR_EXEC_LIST);
            return convertProcessors(executions);
        } catch (Exception e) {
            return List.of();
        }
    }

    /** Convert agent ProcessorExecution list to detail ProcessorNode list. */
    private List<ProcessorNode> convertProcessors(List<ProcessorExecution> executions) {
        if (executions == null) return List.of();
        List<ProcessorNode> result = new ArrayList<>();
        for (ProcessorExecution p : executions) {
            boolean hasTrace = p.getInputBody() != null || p.getOutputBody() != null
                    || p.getInputHeaders() != null || p.getOutputHeaders() != null
                    || p.getInputProperties() != null || p.getOutputProperties() != null;
            ProcessorNode node = new ProcessorNode(
                    p.getProcessorId(), p.getProcessorType(),
                    p.getStatus() != null ? p.getStatus().name() : null,
                    p.getStartTime(), p.getEndTime(),
                    p.getDurationMs(),
                    p.getErrorMessage(), p.getErrorStackTrace(),
                    p.getAttributes() != null ? new LinkedHashMap<>(p.getAttributes()) : null,
                    null, null, null, null, null, null, null,
                    p.getResolvedEndpointUri(),
                    p.getErrorType(), p.getErrorCategory(),
                    p.getRootCauseType(), p.getRootCauseMessage(),
                    p.getErrorHandlerType(), p.getCircuitBreakerState(),
                    p.getFallbackTriggered(),
                    p.getFilterMatched(), p.getDuplicateMessage(),
                    hasTrace
            );
            result.add(node);
        }
        return result;
    }

    /**
     * Reconstruct processor tree from flat records.
     * Detects whether records use the seq-based model (ClickHouse) or
     * processorId-based model (PostgreSQL) and delegates accordingly.
     */
    List<ProcessorNode> buildTree(List<ProcessorRecord> processors) {
        if (processors.isEmpty()) return List.of();
        boolean hasSeq = processors.stream().anyMatch(p -> p.seq() != null);
        return hasSeq ? buildTreeBySeq(processors) : buildTreeByProcessorId(processors);
    }

    /**
     * Seq-based tree reconstruction for ClickHouse flat processor model.
     * Uses seq/parentSeq linkage, correctly handling duplicate processorIds
     * across iterations (e.g., the same processor inside a split running N times).
     */
    private List<ProcessorNode> buildTreeBySeq(List<ProcessorRecord> processors) {
        Map<Integer, ProcessorNode> nodeBySeq = new LinkedHashMap<>();

        for (ProcessorRecord p : processors) {
            boolean hasTrace = p.inputBody() != null || p.outputBody() != null
                    || p.inputHeaders() != null || p.outputHeaders() != null
                    || p.inputProperties() != null || p.outputProperties() != null;
            ProcessorNode node = new ProcessorNode(
                    p.processorId(), p.processorType(), p.status(),
                    p.startTime(), p.endTime(),
                    p.durationMs() != null ? p.durationMs() : 0L,
                    p.errorMessage(), p.errorStacktrace(),
                    parseAttributes(p.attributes()),
                    p.iteration(), p.iterationSize(),
                    null, null, null, null, null,
                    p.resolvedEndpointUri(),
                    p.errorType(), p.errorCategory(),
                    p.rootCauseType(), p.rootCauseMessage(),
                    null, p.circuitBreakerState(),
                    p.fallbackTriggered(),
                    p.filterMatched(), p.duplicateMessage(),
                    hasTrace
            );
            nodeBySeq.put(p.seq(), node);
        }

        List<ProcessorNode> roots = new ArrayList<>();
        for (ProcessorRecord p : processors) {
            ProcessorNode node = nodeBySeq.get(p.seq());
            if (p.parentSeq() == null) {
                roots.add(node);
            } else {
                ProcessorNode parent = nodeBySeq.get(p.parentSeq());
                if (parent != null) {
                    parent.addChild(node);
                } else {
                    roots.add(node); // orphan safety
                }
            }
        }
        return roots;
    }

    /**
     * ProcessorId-based tree reconstruction for PostgreSQL flat records.
     * Note: this loses iteration context for processors with the same ID across iterations.
     */
    private List<ProcessorNode> buildTreeByProcessorId(List<ProcessorRecord> processors) {
        Map<String, ProcessorNode> nodeMap = new LinkedHashMap<>();
        for (ProcessorRecord p : processors) {
            boolean hasTrace = p.inputBody() != null || p.outputBody() != null
                    || p.inputHeaders() != null || p.outputHeaders() != null
                    || p.inputProperties() != null || p.outputProperties() != null;
            nodeMap.put(p.processorId(), new ProcessorNode(
                    p.processorId(), p.processorType(), p.status(),
                    p.startTime(), p.endTime(),
                    p.durationMs() != null ? p.durationMs() : 0L,
                    p.errorMessage(), p.errorStacktrace(),
                    parseAttributes(p.attributes()),
                    null, null,
                    p.loopIndex(), p.loopSize(),
                    p.splitIndex(), p.splitSize(),
                    p.multicastIndex(),
                    p.resolvedEndpointUri(),
                    p.errorType(), p.errorCategory(),
                    p.rootCauseType(), p.rootCauseMessage(),
                    p.errorHandlerType(), p.circuitBreakerState(),
                    p.fallbackTriggered(),
                    null, null, // filterMatched, duplicateMessage (not in flat DB records)
                    hasTrace
            ));
        }

        List<ProcessorNode> roots = new ArrayList<>();
        for (ProcessorRecord p : processors) {
            ProcessorNode node = nodeMap.get(p.processorId());
            if (p.parentProcessorId() == null) {
                roots.add(node);
            } else {
                ProcessorNode parent = nodeMap.get(p.parentProcessorId());
                if (parent != null) {
                    parent.addChild(node);
                } else {
                    roots.add(node); // orphan safety
                }
            }
        }
        return roots;
    }

    private static Map<String, String> parseAttributes(String json) {
        if (json == null || json.isBlank()) return null;
        try {
            return JSON.readValue(json, STR_MAP);
        } catch (Exception e) {
            return null;
        }
    }
}
