package com.cameleer.server.app.storage;

import com.cameleer.server.core.ingestion.ChunkAccumulator;
import com.cameleer.server.core.ingestion.MergedExecution;
import com.cameleer.server.core.storage.ExecutionStore;
import com.cameleer.common.model.FlatProcessorRecord;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class ClickHouseExecutionStore implements ExecutionStore {

    private final String tenantId;
    private final JdbcTemplate jdbc;
    private final ObjectMapper objectMapper;

    public ClickHouseExecutionStore(String tenantId, JdbcTemplate jdbc) {
        this(tenantId, jdbc, new ObjectMapper());
    }

    public ClickHouseExecutionStore(String tenantId, JdbcTemplate jdbc, ObjectMapper objectMapper) {
        this.tenantId = tenantId;
        this.jdbc = jdbc;
        this.objectMapper = objectMapper;
    }

    public void insertExecutionBatch(List<MergedExecution> executions) {
        if (executions.isEmpty()) return;

        jdbc.batchUpdate("""
                INSERT INTO executions (
                    tenant_id, _version, execution_id, route_id, instance_id, application_id,
                    environment, status, correlation_id, exchange_id, start_time, end_time, duration_ms,
                    error_message, error_stacktrace, error_type, error_category,
                    root_cause_type, root_cause_message, diagram_content_hash, engine_level,
                    input_body, output_body, input_headers, output_headers,
                    input_properties, output_properties, attributes,
                    trace_id, span_id, has_trace_data, is_replay,
                    original_exchange_id, replay_exchange_id
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                executions.stream().map(e -> new Object[]{
                        nullToEmpty(e.tenantId()),
                        e.version(),
                        nullToEmpty(e.executionId()),
                        nullToEmpty(e.routeId()),
                        nullToEmpty(e.instanceId()),
                        nullToEmpty(e.applicationId()),
                        nullToEmpty(e.environment()),
                        nullToEmpty(e.status()),
                        nullToEmpty(e.correlationId()),
                        nullToEmpty(e.exchangeId()),
                        Timestamp.from(e.startTime()),
                        e.endTime() != null ? Timestamp.from(e.endTime()) : null,
                        e.durationMs(),
                        nullToEmpty(e.errorMessage()),
                        nullToEmpty(e.errorStacktrace()),
                        nullToEmpty(e.errorType()),
                        nullToEmpty(e.errorCategory()),
                        nullToEmpty(e.rootCauseType()),
                        nullToEmpty(e.rootCauseMessage()),
                        nullToEmpty(e.diagramContentHash()),
                        nullToEmpty(e.engineLevel()),
                        nullToEmpty(e.inputBody()),
                        nullToEmpty(e.outputBody()),
                        nullToEmpty(e.inputHeaders()),
                        nullToEmpty(e.outputHeaders()),
                        nullToEmpty(e.inputProperties()),
                        nullToEmpty(e.outputProperties()),
                        nullToEmpty(e.attributes()),
                        nullToEmpty(e.traceId()),
                        nullToEmpty(e.spanId()),
                        e.hasTraceData(),
                        e.isReplay(),
                        nullToEmpty(e.originalExchangeId()),
                        nullToEmpty(e.replayExchangeId())
                }).toList());
    }

    public void insertProcessorBatch(String tenantId, String executionId, String routeId,
                                     String applicationId, Instant execStartTime,
                                     List<FlatProcessorRecord> processors) {
        if (processors.isEmpty()) return;

        jdbc.batchUpdate("""
                INSERT INTO processor_executions (
                    tenant_id, execution_id, seq, parent_seq, parent_processor_id,
                    processor_id, processor_type, start_time, route_id, application_id,
                    iteration, iteration_size, status, end_time, duration_ms,
                    error_message, error_stacktrace, error_type, error_category,
                    root_cause_type, root_cause_message,
                    input_body, output_body, input_headers, output_headers,
                    input_properties, output_properties, attributes,
                    resolved_endpoint_uri, circuit_breaker_state,
                    fallback_triggered, filter_matched, duplicate_message
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                processors.stream().map(p -> new Object[]{
                        nullToEmpty(tenantId),
                        nullToEmpty(executionId),
                        p.getSeq(),
                        p.getParentSeq(),
                        nullToEmpty(p.getParentProcessorId()),
                        nullToEmpty(p.getProcessorId()),
                        nullToEmpty(p.getProcessorType()),
                        Timestamp.from(p.getStartTime() != null ? p.getStartTime() : execStartTime),
                        nullToEmpty(routeId),
                        nullToEmpty(applicationId),
                        p.getIteration(),
                        p.getIterationSize(),
                        p.getStatus() != null ? p.getStatus().name() : "",
                        computeEndTime(p.getStartTime(), p.getDurationMs()),
                        p.getDurationMs(),
                        nullToEmpty(p.getErrorMessage()),
                        nullToEmpty(p.getErrorStackTrace()),
                        nullToEmpty(p.getErrorType()),
                        nullToEmpty(p.getErrorCategory()),
                        nullToEmpty(p.getRootCauseType()),
                        nullToEmpty(p.getRootCauseMessage()),
                        nullToEmpty(p.getInputBody()),
                        nullToEmpty(p.getOutputBody()),
                        mapToJson(p.getInputHeaders()),
                        mapToJson(p.getOutputHeaders()),
                        mapToJson(p.getInputProperties()),
                        mapToJson(p.getOutputProperties()),
                        mapToJson(p.getAttributes()),
                        nullToEmpty(p.getResolvedEndpointUri()),
                        nullToEmpty(p.getCircuitBreakerState()),
                        boolOrFalse(p.getFallbackTriggered()),
                        boolOrFalse(p.getFilterMatched()),
                        boolOrFalse(p.getDuplicateMessage())
                }).toList());
    }

    public void insertProcessorBatches(List<ChunkAccumulator.ProcessorBatch> batches) {
        if (batches.isEmpty()) return;

        List<Object[]> allRows = batches.stream()
                .flatMap(batch -> batch.processors().stream().map(p -> new Object[]{
                        nullToEmpty(batch.tenantId()),
                        nullToEmpty(batch.executionId()),
                        p.getSeq(),
                        p.getParentSeq(),
                        nullToEmpty(p.getParentProcessorId()),
                        nullToEmpty(p.getProcessorId()),
                        nullToEmpty(p.getProcessorType()),
                        Timestamp.from(p.getStartTime() != null ? p.getStartTime() : batch.execStartTime()),
                        nullToEmpty(batch.routeId()),
                        nullToEmpty(batch.applicationId()),
                        p.getIteration(),
                        p.getIterationSize(),
                        p.getStatus() != null ? p.getStatus().name() : "",
                        computeEndTime(p.getStartTime(), p.getDurationMs()),
                        p.getDurationMs(),
                        nullToEmpty(p.getErrorMessage()),
                        nullToEmpty(p.getErrorStackTrace()),
                        nullToEmpty(p.getErrorType()),
                        nullToEmpty(p.getErrorCategory()),
                        nullToEmpty(p.getRootCauseType()),
                        nullToEmpty(p.getRootCauseMessage()),
                        nullToEmpty(p.getInputBody()),
                        nullToEmpty(p.getOutputBody()),
                        mapToJson(p.getInputHeaders()),
                        mapToJson(p.getOutputHeaders()),
                        mapToJson(p.getInputProperties()),
                        mapToJson(p.getOutputProperties()),
                        mapToJson(p.getAttributes()),
                        nullToEmpty(p.getResolvedEndpointUri()),
                        nullToEmpty(p.getCircuitBreakerState()),
                        boolOrFalse(p.getFallbackTriggered()),
                        boolOrFalse(p.getFilterMatched()),
                        boolOrFalse(p.getDuplicateMessage())
                }))
                .toList();

        jdbc.batchUpdate("""
                INSERT INTO processor_executions (
                    tenant_id, execution_id, seq, parent_seq, parent_processor_id,
                    processor_id, processor_type, start_time, route_id, application_id,
                    iteration, iteration_size, status, end_time, duration_ms,
                    error_message, error_stacktrace, error_type, error_category,
                    root_cause_type, root_cause_message,
                    input_body, output_body, input_headers, output_headers,
                    input_properties, output_properties, attributes,
                    resolved_endpoint_uri, circuit_breaker_state,
                    fallback_triggered, filter_matched, duplicate_message
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """, allRows);
    }

    // --- ExecutionStore interface: read methods ---

    @Override
    public Optional<ExecutionRecord> findById(String executionId) {
        List<ExecutionRecord> results = jdbc.query("""
                SELECT execution_id, route_id, instance_id, application_id, environment, status,
                       correlation_id, exchange_id, start_time, end_time, duration_ms,
                       error_message, error_stacktrace, diagram_content_hash, engine_level,
                       input_body, output_body, input_headers, output_headers,
                       input_properties, output_properties, attributes,
                       error_type, error_category, root_cause_type, root_cause_message,
                       trace_id, span_id, has_trace_data, is_replay
                FROM executions FINAL
                WHERE tenant_id = ? AND execution_id = ?
                LIMIT 1
                """,
                (rs, rowNum) -> mapExecutionRecord(rs),
                tenantId, executionId);
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }

    @Override
    public List<ProcessorRecord> findProcessors(String executionId) {
        return jdbc.query("""
                SELECT execution_id, seq, parent_seq, parent_processor_id,
                       processor_id, processor_type, start_time, route_id, application_id,
                       iteration, iteration_size, status, end_time, duration_ms,
                       error_message, error_stacktrace, error_type, error_category,
                       root_cause_type, root_cause_message,
                       input_body, output_body, input_headers, output_headers,
                       input_properties, output_properties, attributes,
                       resolved_endpoint_uri, circuit_breaker_state,
                       fallback_triggered, filter_matched, duplicate_message
                FROM processor_executions
                WHERE tenant_id = ? AND execution_id = ?
                ORDER BY seq
                """,
                (rs, rowNum) -> mapProcessorRecord(rs),
                tenantId, executionId);
    }

    @Override
    public Optional<ProcessorRecord> findProcessorById(String executionId, String processorId) {
        List<ProcessorRecord> results = jdbc.query("""
                SELECT execution_id, seq, parent_seq, parent_processor_id,
                       processor_id, processor_type, start_time, route_id, application_id,
                       iteration, iteration_size, status, end_time, duration_ms,
                       error_message, error_stacktrace, error_type, error_category,
                       root_cause_type, root_cause_message,
                       input_body, output_body, input_headers, output_headers,
                       input_properties, output_properties, attributes,
                       resolved_endpoint_uri, circuit_breaker_state,
                       fallback_triggered, filter_matched, duplicate_message
                FROM processor_executions
                WHERE tenant_id = ? AND execution_id = ? AND processor_id = ?
                LIMIT 1
                """,
                (rs, rowNum) -> mapProcessorRecord(rs),
                tenantId, executionId, processorId);
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }

    @Override
    public Optional<ProcessorRecord> findProcessorBySeq(String executionId, int seq) {
        List<ProcessorRecord> results = jdbc.query("""
                SELECT execution_id, seq, parent_seq, parent_processor_id,
                       processor_id, processor_type, start_time, route_id, application_id,
                       iteration, iteration_size, status, end_time, duration_ms,
                       error_message, error_stacktrace, error_type, error_category,
                       root_cause_type, root_cause_message,
                       input_body, output_body, input_headers, output_headers,
                       input_properties, output_properties, attributes,
                       resolved_endpoint_uri, circuit_breaker_state,
                       fallback_triggered, filter_matched, duplicate_message
                FROM processor_executions
                WHERE tenant_id = ? AND execution_id = ? AND seq = ?
                LIMIT 1
                """,
                (rs, rowNum) -> mapProcessorRecord(rs),
                tenantId, executionId, seq);
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }

    // --- Row mappers ---

    private static ExecutionRecord mapExecutionRecord(ResultSet rs) throws SQLException {
        return new ExecutionRecord(
                emptyToNull(rs.getString("execution_id")),
                emptyToNull(rs.getString("route_id")),
                emptyToNull(rs.getString("instance_id")),
                emptyToNull(rs.getString("application_id")),
                emptyToNull(rs.getString("environment")),
                emptyToNull(rs.getString("status")),
                emptyToNull(rs.getString("correlation_id")),
                emptyToNull(rs.getString("exchange_id")),
                toInstant(rs, "start_time"),
                toInstant(rs, "end_time"),
                rs.getObject("duration_ms") != null ? rs.getLong("duration_ms") : null,
                emptyToNull(rs.getString("error_message")),
                emptyToNull(rs.getString("error_stacktrace")),
                emptyToNull(rs.getString("diagram_content_hash")),
                emptyToNull(rs.getString("engine_level")),
                emptyToNull(rs.getString("input_body")),
                emptyToNull(rs.getString("output_body")),
                emptyToNull(rs.getString("input_headers")),
                emptyToNull(rs.getString("output_headers")),
                emptyToNull(rs.getString("input_properties")),
                emptyToNull(rs.getString("output_properties")),
                emptyToNull(rs.getString("attributes")),
                emptyToNull(rs.getString("error_type")),
                emptyToNull(rs.getString("error_category")),
                emptyToNull(rs.getString("root_cause_type")),
                emptyToNull(rs.getString("root_cause_message")),
                emptyToNull(rs.getString("trace_id")),
                emptyToNull(rs.getString("span_id")),
                null, // processorsJson not stored in ClickHouse
                rs.getBoolean("has_trace_data"),
                rs.getBoolean("is_replay")
        );
    }

    private static ProcessorRecord mapProcessorRecord(ResultSet rs) throws SQLException {
        return new ProcessorRecord(
                emptyToNull(rs.getString("execution_id")),
                emptyToNull(rs.getString("processor_id")),
                emptyToNull(rs.getString("processor_type")),
                emptyToNull(rs.getString("application_id")),
                emptyToNull(rs.getString("route_id")),
                0, // depth not stored in ClickHouse
                emptyToNull(rs.getString("parent_processor_id")),
                emptyToNull(rs.getString("status")),
                toInstant(rs, "start_time"),
                toInstant(rs, "end_time"),
                rs.getObject("duration_ms") != null ? rs.getLong("duration_ms") : null,
                emptyToNull(rs.getString("error_message")),
                emptyToNull(rs.getString("error_stacktrace")),
                emptyToNull(rs.getString("input_body")),
                emptyToNull(rs.getString("output_body")),
                emptyToNull(rs.getString("input_headers")),
                emptyToNull(rs.getString("output_headers")),
                emptyToNull(rs.getString("input_properties")),
                emptyToNull(rs.getString("output_properties")),
                emptyToNull(rs.getString("attributes")),
                null, // loopIndex
                null, // loopSize
                null, // splitIndex
                null, // splitSize
                null, // multicastIndex
                emptyToNull(rs.getString("resolved_endpoint_uri")),
                emptyToNull(rs.getString("error_type")),
                emptyToNull(rs.getString("error_category")),
                emptyToNull(rs.getString("root_cause_type")),
                emptyToNull(rs.getString("root_cause_message")),
                null, // errorHandlerType
                emptyToNull(rs.getString("circuit_breaker_state")),
                rs.getObject("fallback_triggered") != null ? rs.getBoolean("fallback_triggered") : null,
                rs.getObject("seq") != null ? rs.getInt("seq") : null,
                rs.getObject("parent_seq") != null ? rs.getInt("parent_seq") : null,
                rs.getObject("iteration") != null ? rs.getInt("iteration") : null,
                rs.getObject("iteration_size") != null ? rs.getInt("iteration_size") : null,
                rs.getObject("filter_matched") != null ? rs.getBoolean("filter_matched") : null,
                rs.getObject("duplicate_message") != null ? rs.getBoolean("duplicate_message") : null
        );
    }

    // --- Helpers ---

    private static String emptyToNull(String value) {
        return (value == null || value.isEmpty()) ? null : value;
    }

    private static Instant toInstant(ResultSet rs, String column) throws SQLException {
        Timestamp ts = rs.getTimestamp(column);
        return ts != null ? ts.toInstant() : null;
    }

    private static String nullToEmpty(String value) {
        return value != null ? value : "";
    }

    private static boolean boolOrFalse(Boolean value) {
        return value != null && value;
    }

    private static Timestamp computeEndTime(Instant startTime, long durationMs) {
        if (startTime != null && durationMs > 0) {
            return Timestamp.from(startTime.plusMillis(durationMs));
        }
        return null;
    }

    private String mapToJson(Map<String, String> map) {
        if (map == null || map.isEmpty()) return "";
        try {
            return objectMapper.writeValueAsString(map);
        } catch (JsonProcessingException e) {
            return "";
        }
    }
}
