package com.cameleer.server.app.search;

import com.cameleer.server.core.alerting.AlertMatchSpec;
import com.cameleer.server.core.search.AttributeFilter;
import com.cameleer.server.core.search.ExecutionSummary;
import com.cameleer.server.core.search.SearchRequest;
import com.cameleer.server.core.search.SearchResult;
import com.cameleer.server.core.storage.SearchIndex;
import com.cameleer.server.core.storage.model.ExecutionDocument;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.*;

/**
 * ClickHouse-backed implementation of {@link SearchIndex}.
 * <p>
 * Queries the {@code executions} and {@code processor_executions} tables directly
 * using SQL with ngram bloom-filter indexes for full-text search acceleration.
 * <p>
 * The {@link #index} and {@link #delete} methods are no-ops because data is
 * written by the accumulator/store pipeline, not the search index.
 */
public class ClickHouseSearchIndex implements SearchIndex {

    private static final Logger log = LoggerFactory.getLogger(ClickHouseSearchIndex.class);
    private static final ObjectMapper JSON = new ObjectMapper();
    private static final TypeReference<Map<String, String>> STR_MAP = new TypeReference<>() {};
    private static final int HIGHLIGHT_CONTEXT_CHARS = 120;

    private static final Map<String, String> SORT_FIELD_MAP = Map.of(
            "startTime", "start_time",
            "durationMs", "duration_ms",
            "status", "status",
            "instanceId", "instance_id",
            "routeId", "route_id",
            "correlationId", "correlation_id",
            "executionId", "execution_id",
            "applicationId", "application_id"
    );

    private final String tenantId;
    private final JdbcTemplate jdbc;

    public ClickHouseSearchIndex(String tenantId, JdbcTemplate jdbc) {
        this.tenantId = tenantId;
        this.jdbc = jdbc;
    }

    @Override
    public void index(ExecutionDocument document) {
        // No-op: data is written by ClickHouseExecutionStore
    }

    @Override
    public void delete(String executionId) {
        // No-op: ClickHouse ReplacingMergeTree handles versioning
    }

    @Override
    public SearchResult<ExecutionSummary> search(SearchRequest request) {
        try {
            List<Object> params = new ArrayList<>();
            String whereClause = buildWhereClause(request, params);
            String searchTerm = request.text();

            // Count query
            String countSql = "SELECT count() FROM executions FINAL WHERE " + whereClause;
            Long total = jdbc.queryForObject(countSql, Long.class, params.toArray());
            if (total == null || total == 0) {
                return SearchResult.empty(request.offset(), request.limit());
            }

            // Data query
            String sortColumn = SORT_FIELD_MAP.getOrDefault(request.sortField(), "start_time");
            String sortDir = "asc".equalsIgnoreCase(request.sortDir()) ? "ASC" : "DESC";

            // Composite-cursor callers (afterExecutionId set) need a deterministic tiebreak inside
            // same-millisecond groups so the client-side last-row pick matches ClickHouse's row order.
            // Without this, a same-start_time tail >LIMIT can silently drop rows: the page ends mid-ms,
            // the cursor advances past the returned lastRowId, and the skipped rows with smaller
            // execution_id values never reappear. Other callers (UI/stats) keep the unchanged
            // single-column ORDER BY — they don't use the composite cursor.
            String orderBy = sortColumn + " " + sortDir;
            if (request.afterExecutionId() != null) {
                orderBy += ", execution_id " + sortDir;
            }

            String dataSql = "SELECT execution_id, route_id, instance_id, application_id, "
                    + "status, start_time, end_time, duration_ms, correlation_id, "
                    + "error_message, error_stacktrace, diagram_content_hash, attributes, "
                    + "has_trace_data, is_replay, "
                    + "input_body, output_body, input_headers, output_headers, root_cause_message "
                    + "FROM executions FINAL WHERE " + whereClause
                    + " ORDER BY " + orderBy
                    + " LIMIT ? OFFSET ?";

            List<Object> dataParams = new ArrayList<>(params);
            dataParams.add(request.limit());
            dataParams.add(request.offset());

            List<ExecutionSummary> data = jdbc.query(
                    dataSql, dataParams.toArray(),
                    (rs, rowNum) -> mapRow(rs, searchTerm));

            return new SearchResult<>(data, total, request.offset(), request.limit());
        } catch (Exception e) {
            log.error("ClickHouse search failed", e);
            return SearchResult.empty(request.offset(), request.limit());
        }
    }

    @Override
    public long count(SearchRequest request) {
        try {
            List<Object> params = new ArrayList<>();
            String whereClause = buildWhereClause(request, params);
            String sql = "SELECT count() FROM executions FINAL WHERE " + whereClause;
            Long result = jdbc.queryForObject(sql, Long.class, params.toArray());
            return result != null ? result : 0L;
        } catch (Exception e) {
            log.error("ClickHouse count failed", e);
            return 0L;
        }
    }

    private String buildWhereClause(SearchRequest request, List<Object> params) {
        List<String> conditions = new ArrayList<>();
        conditions.add("tenant_id = ?");
        params.add(tenantId);

        if (request.timeFrom() != null && request.afterExecutionId() != null) {
            // composite predicate: strictly-after in (start_time, execution_id) tuple order
            conditions.add("(start_time > ? OR (start_time = ? AND execution_id > ?))");
            params.add(Timestamp.from(request.timeFrom()));
            params.add(Timestamp.from(request.timeFrom()));
            params.add(request.afterExecutionId());
        } else if (request.timeFrom() != null) {
            conditions.add("start_time >= ?");
            params.add(Timestamp.from(request.timeFrom()));
        }
        if (request.timeTo() != null) {
            conditions.add("start_time <= ?");
            params.add(Timestamp.from(request.timeTo()));
        }

        if (request.status() != null && !request.status().isBlank()) {
            String[] statuses = request.status().split(",");
            if (statuses.length == 1) {
                conditions.add("status = ?");
                params.add(statuses[0].trim());
            } else {
                String placeholders = String.join(", ", Collections.nCopies(statuses.length, "?"));
                conditions.add("status IN (" + placeholders + ")");
                for (String s : statuses) {
                    params.add(s.trim());
                }
            }
        }

        if (request.routeId() != null) {
            conditions.add("route_id = ?");
            params.add(request.routeId());
        }

        if (request.instanceId() != null) {
            conditions.add("instance_id = ?");
            params.add(request.instanceId());
        }

        if (request.correlationId() != null) {
            conditions.add("correlation_id = ?");
            params.add(request.correlationId());
        }

        if (request.applicationId() != null && !request.applicationId().isBlank()) {
            conditions.add("application_id = ?");
            params.add(request.applicationId());
        }

        if (request.instanceIds() != null && !request.instanceIds().isEmpty()) {
            String placeholders = String.join(", ", Collections.nCopies(request.instanceIds().size(), "?"));
            conditions.add("instance_id IN (" + placeholders + ")");
            params.addAll(request.instanceIds());
        }

        if (request.durationMin() != null) {
            conditions.add("duration_ms >= ?");
            params.add(request.durationMin());
        }

        if (request.durationMax() != null) {
            conditions.add("duration_ms <= ?");
            params.add(request.durationMax());
        }

        if (request.environment() != null && !request.environment().isBlank()) {
            conditions.add("environment = ?");
            params.add(request.environment());
        }

        // Global full-text search: exact ID match, full-text on execution + processor level
        if (request.text() != null && !request.text().isBlank()) {
            String term = escapeLike(request.text());
            String likeTerm = "%" + term + "%";
            conditions.add("(execution_id = ? OR correlation_id = ? OR exchange_id = ?"
                    + " OR _search_text LIKE ? OR execution_id IN ("
                    + "SELECT DISTINCT execution_id FROM processor_executions "
                    + "WHERE tenant_id = ? AND _search_text LIKE ?))");
            params.add(term);
            params.add(term);
            params.add(term);
            params.add(likeTerm);
            params.add(tenantId);
            params.add(likeTerm);
        }

        // Scoped body search in processor_executions
        if (request.textInBody() != null && !request.textInBody().isBlank()) {
            String likeTerm = "%" + escapeLike(request.textInBody()) + "%";
            conditions.add("execution_id IN ("
                    + "SELECT DISTINCT execution_id FROM processor_executions "
                    + "WHERE tenant_id = ? AND (input_body LIKE ? OR output_body LIKE ?))");
            params.add(tenantId);
            params.add(likeTerm);
            params.add(likeTerm);
        }

        // Scoped headers search in processor_executions
        if (request.textInHeaders() != null && !request.textInHeaders().isBlank()) {
            String likeTerm = "%" + escapeLike(request.textInHeaders()) + "%";
            conditions.add("execution_id IN ("
                    + "SELECT DISTINCT execution_id FROM processor_executions "
                    + "WHERE tenant_id = ? AND (input_headers LIKE ? OR output_headers LIKE ?))");
            params.add(tenantId);
            params.add(likeTerm);
            params.add(likeTerm);
        }

        // Scoped error search: execution-level + processor-level
        if (request.textInErrors() != null && !request.textInErrors().isBlank()) {
            String likeTerm = "%" + escapeLike(request.textInErrors()) + "%";
            conditions.add("(error_message LIKE ? OR error_stacktrace LIKE ? OR execution_id IN ("
                    + "SELECT DISTINCT execution_id FROM processor_executions "
                    + "WHERE tenant_id = ? AND (error_message LIKE ? OR error_stacktrace LIKE ?)))");
            params.add(likeTerm);
            params.add(likeTerm);
            params.add(tenantId);
            params.add(likeTerm);
            params.add(likeTerm);
        }

        // Structured attribute filters. Keys were validated at AttributeFilter construction
        // time against ^[a-zA-Z0-9._-]+$ so they are safe to single-quote-inline; the JSON path
        // argument of JSONExtractString does not accept a ? placeholder in ClickHouse JDBC
        // (same constraint as countExecutionsForAlerting below). Values are parameter-bound.
        for (AttributeFilter filter : request.attributeFilters()) {
            String escapedKey = filter.key().replace("'", "\\'");
            if (filter.isKeyOnly()) {
                conditions.add("JSONHas(attributes, '" + escapedKey + "')");
            } else if (filter.isWildcard()) {
                conditions.add("JSONExtractString(attributes, '" + escapedKey + "') LIKE ?");
                params.add(filter.toLikePattern());
            } else {
                conditions.add("JSONExtractString(attributes, '" + escapedKey + "') = ?");
                params.add(filter.value());
            }
        }

        return String.join(" AND ", conditions);
    }

    private ExecutionSummary mapRow(ResultSet rs, String searchTerm) throws SQLException {
        String executionId = rs.getString("execution_id");
        String routeId = rs.getString("route_id");
        String instanceId = rs.getString("instance_id");
        String applicationId = rs.getString("application_id");
        String status = rs.getString("status");

        Timestamp startTs = rs.getTimestamp("start_time");
        Instant startTime = startTs != null ? startTs.toInstant() : null;

        Timestamp endTs = rs.getTimestamp("end_time");
        Instant endTime = endTs != null ? endTs.toInstant() : null;

        long durationMs = rs.getLong("duration_ms");
        String correlationId = rs.getString("correlation_id");
        String errorMessage = rs.getString("error_message");
        String errorStacktrace = rs.getString("error_stacktrace");
        String diagramContentHash = rs.getString("diagram_content_hash");
        String attributesJson = rs.getString("attributes");
        boolean hasTraceData = rs.getBoolean("has_trace_data");
        boolean isReplay = rs.getBoolean("is_replay");
        String inputBody = rs.getString("input_body");
        String outputBody = rs.getString("output_body");
        String inputHeaders = rs.getString("input_headers");
        String outputHeaders = rs.getString("output_headers");
        String rootCauseMessage = rs.getString("root_cause_message");

        Map<String, String> attributes = parseAttributesJson(attributesJson);

        // Application-side highlighting
        String highlight = null;
        if (searchTerm != null && !searchTerm.isBlank()) {
            highlight = findHighlight(searchTerm, errorMessage, errorStacktrace,
                    inputBody, outputBody, inputHeaders, outputHeaders, attributesJson, rootCauseMessage);
        }

        return new ExecutionSummary(
                executionId, routeId, instanceId, applicationId, status,
                startTime, endTime, durationMs,
                correlationId, errorMessage, diagramContentHash,
                highlight, attributes, hasTraceData, isReplay
        );
    }

    private String findHighlight(String searchTerm, String... fields) {
        for (String field : fields) {
            String snippet = extractSnippet(field, searchTerm, HIGHLIGHT_CONTEXT_CHARS);
            if (snippet != null) {
                return snippet;
            }
        }
        return null;
    }

    static String extractSnippet(String text, String searchTerm, int contextChars) {
        if (text == null || text.isEmpty() || searchTerm == null) return null;
        int idx = text.toLowerCase().indexOf(searchTerm.toLowerCase());
        if (idx < 0) return null;
        int start = Math.max(0, idx - contextChars / 2);
        int end = Math.min(text.length(), idx + searchTerm.length() + contextChars / 2);
        String before = escapeHtml(text.substring(start, idx));
        String match = escapeHtml(text.substring(idx, idx + searchTerm.length()));
        String after = escapeHtml(text.substring(idx + searchTerm.length(), end));
        return (start > 0 ? "..." : "") + before + "<mark>" + match + "</mark>" + after + (end < text.length() ? "..." : "");
    }

    private static String escapeHtml(String s) {
        return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
    }

    private static String escapeLike(String term) {
        return term.replace("\\", "\\\\")
                   .replace("%", "\\%")
                   .replace("_", "\\_");
    }

    /**
     * Counts executions matching the given alerting spec — no {@code FINAL}, no text subqueries.
     * Attributes are stored as a JSON string column; use {@code JSONExtractString} for key=value filters.
     */
    public long countExecutionsForAlerting(AlertMatchSpec spec) {
        List<String> conditions = new ArrayList<>();
        List<Object> args = new ArrayList<>();

        conditions.add("tenant_id = ?");
        args.add(spec.tenantId());
        conditions.add("environment = ?");
        args.add(spec.environment());
        conditions.add("start_time >= ?");
        args.add(Timestamp.from(spec.from()));
        conditions.add("start_time <= ?");
        args.add(Timestamp.from(spec.to()));

        if (spec.applicationId() != null) {
            conditions.add("application_id = ?");
            args.add(spec.applicationId());
        }
        if (spec.routeId() != null) {
            conditions.add("route_id = ?");
            args.add(spec.routeId());
        }
        if (spec.status() != null) {
            conditions.add("status = ?");
            args.add(spec.status());
        }
        if (spec.after() != null) {
            conditions.add("start_time > ?");
            args.add(Timestamp.from(spec.after()));
        }

        // attributes is a JSON String column. JSONExtractString does not accept a ? placeholder for
        // the key argument via ClickHouse JDBC — inline the key as a single-quoted literal.
        // Attribute KEYS originate from user-authored rule JSONB (via ExchangeMatchCondition.filter.attributes);
        // they are validated at rule save time by AlertRuleController to match ^[a-zA-Z0-9._-]+$
        // before ever reaching this point. Values are parameter-bound.
        for (Map.Entry<String, String> entry : spec.attributes().entrySet()) {
            String escapedKey = entry.getKey().replace("'", "\\'");
            conditions.add("JSONExtractString(attributes, '" + escapedKey + "') = ?");
            args.add(entry.getValue());
        }

        String sql = "SELECT count() FROM executions WHERE " + String.join(" AND ", conditions); // NO FINAL
        Long result = jdbc.queryForObject(sql, Long.class, args.toArray());
        return result != null ? result : 0L;
    }

    @Override
    public List<String> distinctAttributeKeys(String environment) {
        try {
            return jdbc.queryForList("""
                    SELECT DISTINCT arrayJoin(JSONExtractKeys(attributes)) AS attr_key
                    FROM executions FINAL
                    WHERE tenant_id = ? AND environment = ? AND attributes != '' AND attributes != '{}'
                    ORDER BY attr_key
                    """, String.class, tenantId, environment);
        } catch (Exception e) {
            log.error("Failed to query distinct attribute keys", e);
            return List.of();
        }
    }

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