package com.cameleer.server.app.search;

import com.cameleer.common.model.LogEntry;
import com.cameleer.server.core.ingestion.BufferedLogEntry;
import com.cameleer.server.core.search.LogSearchRequest;
import com.cameleer.server.core.search.LogSearchResponse;
import com.cameleer.server.core.storage.LogEntryResult;
import com.cameleer.server.core.storage.LogIndex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * ClickHouse-backed implementation of {@link LogIndex}.
 * Stores application logs in the {@code logs} MergeTree table with
 * ngram bloom-filter indexes for efficient substring search.
 */
public class ClickHouseLogStore implements LogIndex {

    private static final Logger log = LoggerFactory.getLogger(ClickHouseLogStore.class);

    private final String tenantId;
    private final JdbcTemplate jdbc;

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

    @Override
    public void indexBatch(String instanceId, String applicationId, List<LogEntry> entries) {
        if (entries == null || entries.isEmpty()) {
            return;
        }

        String sql = "INSERT INTO logs (tenant_id, timestamp, application, instance_id, level, " +
                "logger_name, message, thread_name, stack_trace, exchange_id, mdc, source) " +
                "VALUES (?, parseDateTime64BestEffort(?, 3), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

        jdbc.batchUpdate(sql, entries, entries.size(), (ps, entry) -> {
            Instant ts = entry.getTimestamp() != null ? entry.getTimestamp() : Instant.now();
            ps.setString(1, tenantId);
            ps.setString(2, ts.toString());
            ps.setString(3, applicationId);
            ps.setString(4, instanceId);
            ps.setString(5, entry.getLevel() != null ? entry.getLevel() : "");
            ps.setString(6, entry.getLoggerName() != null ? entry.getLoggerName() : "");
            ps.setString(7, entry.getMessage() != null ? entry.getMessage() : "");
            ps.setString(8, entry.getThreadName() != null ? entry.getThreadName() : "");
            ps.setString(9, entry.getStackTrace() != null ? entry.getStackTrace() : "");

            Map<String, String> mdc = entry.getMdc() != null ? entry.getMdc() : Collections.emptyMap();
            String exchangeId = mdc.getOrDefault("cameleer.exchangeId", mdc.getOrDefault("camel.exchangeId", ""));
            ps.setString(10, exchangeId);
            ps.setObject(11, mdc);
            ps.setString(12, entry.getSource() != null ? entry.getSource() : "app");
        });

        log.debug("Indexed {} log entries for instance={}, app={}", entries.size(), instanceId, applicationId);
    }

    public void insertBufferedBatch(List<BufferedLogEntry> entries) {
        if (entries.isEmpty()) return;

        String sql = "INSERT INTO logs (tenant_id, environment, timestamp, application, instance_id, level, " +
                "logger_name, message, thread_name, stack_trace, exchange_id, mdc, source) " +
                "VALUES (?, ?, parseDateTime64BestEffort(?, 3), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

        jdbc.batchUpdate(sql, entries, entries.size(), (ps, ble) -> {
            LogEntry entry = ble.entry();
            Instant ts = entry.getTimestamp() != null ? entry.getTimestamp() : Instant.now();
            ps.setString(1, ble.tenantId() != null ? ble.tenantId() : tenantId);
            ps.setString(2, ble.environment() != null ? ble.environment() : "default");
            ps.setString(3, ts.toString());
            ps.setString(4, ble.applicationId());
            ps.setString(5, ble.instanceId());
            ps.setString(6, entry.getLevel() != null ? entry.getLevel() : "");
            ps.setString(7, entry.getLoggerName() != null ? entry.getLoggerName() : "");
            ps.setString(8, entry.getMessage() != null ? entry.getMessage() : "");
            ps.setString(9, entry.getThreadName() != null ? entry.getThreadName() : "");
            ps.setString(10, entry.getStackTrace() != null ? entry.getStackTrace() : "");

            Map<String, String> mdc = entry.getMdc() != null ? entry.getMdc() : Collections.emptyMap();
            String exchangeId = mdc.getOrDefault("cameleer.exchangeId", mdc.getOrDefault("camel.exchangeId", ""));
            ps.setString(11, exchangeId);
            ps.setObject(12, mdc);
            ps.setString(13, entry.getSource() != null ? entry.getSource() : "app");
        });

        log.debug("Flushed {} buffered log entries to ClickHouse", entries.size());
    }

    @Override
    public LogSearchResponse search(LogSearchRequest request) {
        // Build shared WHERE conditions (used by both data and count queries)
        List<String> baseConditions = new ArrayList<>();
        List<Object> baseParams = new ArrayList<>();
        baseConditions.add("tenant_id = ?");
        baseParams.add(tenantId);

        if (request.environment() != null && !request.environment().isEmpty()) {
            baseConditions.add("environment = ?");
            baseParams.add(request.environment());
        }

        if (request.application() != null && !request.application().isEmpty()) {
            baseConditions.add("application = ?");
            baseParams.add(request.application());
        }

        if (request.instanceId() != null && !request.instanceId().isEmpty()) {
            baseConditions.add("instance_id = ?");
            baseParams.add(request.instanceId());
        }

        if (!request.instanceIds().isEmpty()) {
            String placeholders = String.join(", ", Collections.nCopies(request.instanceIds().size(), "?"));
            baseConditions.add("instance_id IN (" + placeholders + ")");
            for (String id : request.instanceIds()) {
                baseParams.add(id);
            }
        }

        if (request.exchangeId() != null && !request.exchangeId().isEmpty()) {
            baseConditions.add("(exchange_id = ?" +
                    " OR (mapContains(mdc, 'cameleer.exchangeId') AND mdc['cameleer.exchangeId'] = ?)" +
                    " OR (mapContains(mdc, 'camel.exchangeId') AND mdc['camel.exchangeId'] = ?))");
            baseParams.add(request.exchangeId());
            baseParams.add(request.exchangeId());
            baseParams.add(request.exchangeId());
        }

        if (request.q() != null && !request.q().isEmpty()) {
            String term = "%" + escapeLike(request.q()) + "%";
            baseConditions.add("(message ILIKE ? OR stack_trace ILIKE ?)");
            baseParams.add(term);
            baseParams.add(term);
        }

        if (request.logger() != null && !request.logger().isEmpty()) {
            baseConditions.add("logger_name ILIKE ?");
            baseParams.add("%" + escapeLike(request.logger()) + "%");
        }

        if (request.sources() != null && !request.sources().isEmpty()) {
            String placeholders = String.join(", ", Collections.nCopies(request.sources().size(), "?"));
            baseConditions.add("source IN (" + placeholders + ")");
            for (String s : request.sources()) {
                baseParams.add(s);
            }
        }

        if (request.from() != null) {
            baseConditions.add("timestamp >= parseDateTime64BestEffort(?, 3)");
            baseParams.add(request.from().toString());
        }

        if (request.to() != null) {
            baseConditions.add("timestamp <= parseDateTime64BestEffort(?, 3)");
            baseParams.add(request.to().toString());
        }

        // Level counts query: uses base conditions WITHOUT level filter and cursor
        String baseWhere = String.join(" AND ", baseConditions);
        Map<String, Long> levelCounts = queryLevelCounts(baseWhere, baseParams);

        // Data query conditions: add level filter and cursor on top of base
        List<String> dataConditions = new ArrayList<>(baseConditions);
        List<Object> dataParams = new ArrayList<>(baseParams);

        if (request.levels() != null && !request.levels().isEmpty()) {
            String placeholders = String.join(", ", Collections.nCopies(request.levels().size(), "?"));
            dataConditions.add("level IN (" + placeholders + ")");
            for (String lvl : request.levels()) {
                dataParams.add(lvl.toUpperCase());
            }
        }

        if (request.cursor() != null && !request.cursor().isEmpty()) {
            String decoded = new String(Base64.getUrlDecoder().decode(request.cursor()),
                    StandardCharsets.UTF_8);
            int bar = decoded.indexOf('|');
            if (bar <= 0 || bar == decoded.length() - 1) {
                throw new IllegalArgumentException("Malformed cursor");
            }
            Instant cursorTs;
            try {
                cursorTs = Instant.parse(decoded.substring(0, bar));
            } catch (java.time.format.DateTimeParseException e) {
                throw new IllegalArgumentException("Malformed cursor", e);
            }
            String cursorId = decoded.substring(bar + 1);
            String cmp = "asc".equalsIgnoreCase(request.sort()) ? ">" : "<";
            dataConditions.add(
                    "(timestamp " + cmp + " parseDateTime64BestEffort(?, 3)" +
                    " OR (timestamp = parseDateTime64BestEffort(?, 3) AND insert_id " + cmp + " toUUID(?)))");
            dataParams.add(cursorTs.toString());
            dataParams.add(cursorTs.toString());
            dataParams.add(cursorId);
        }

        String dataWhere = String.join(" AND ", dataConditions);
        String orderDir = "asc".equalsIgnoreCase(request.sort()) ? "ASC" : "DESC";
        int fetchLimit = request.limit() + 1; // fetch N+1 to detect hasMore

        String dataSql = "SELECT formatDateTime(timestamp, '%Y-%m-%dT%H:%i:%S', 'UTC') AS ts_utc," +
                " toUnixTimestamp64Milli(timestamp) AS ts_millis," +
                " level, logger_name, message, thread_name, stack_trace, " +
                "exchange_id, instance_id, application, mdc, source, toString(insert_id) AS insert_id_str " +
                "FROM logs WHERE " + dataWhere +
                " ORDER BY timestamp " + orderDir + ", insert_id " + orderDir + " LIMIT ?";
        dataParams.add(fetchLimit);

        List<String> insertIds = new ArrayList<>();
        List<LogEntryResult> results = jdbc.query(dataSql, dataParams.toArray(), (rs, rowNum) -> {
            long tsMillis = rs.getLong("ts_millis");
            String timestampStr = Instant.ofEpochMilli(tsMillis).toString();

            @SuppressWarnings("unchecked")
            Map<String, String> mdc = (Map<String, String>) rs.getObject("mdc");
            if (mdc == null) mdc = Collections.emptyMap();

            String source = rs.getString("source");

            insertIds.add(rs.getString("insert_id_str"));

            return new LogEntryResult(
                    timestampStr,
                    rs.getString("level"),
                    rs.getString("logger_name"),
                    rs.getString("message"),
                    rs.getString("thread_name"),
                    rs.getString("stack_trace"),
                    rs.getString("exchange_id"),
                    rs.getString("instance_id"),
                    rs.getString("application"),
                    mdc,
                    source
            );
        });

        boolean hasMore = results.size() > request.limit();
        if (hasMore) {
            results = new ArrayList<>(results.subList(0, request.limit()));
        }

        String nextCursor = null;
        if (hasMore && !results.isEmpty()) {
            int lastIdx = results.size() - 1;
            String raw = results.get(lastIdx).timestamp() + "|" + insertIds.get(lastIdx);
            nextCursor = Base64.getUrlEncoder().withoutPadding()
                    .encodeToString(raw.getBytes(StandardCharsets.UTF_8));
        }

        return new LogSearchResponse(results, nextCursor, hasMore, levelCounts);
    }

    /**
     * Counts log entries matching the given request — no {@code FINAL}, no cursor/sort/limit.
     * Intended for alerting evaluators (LogPatternEvaluator) which tolerate brief duplicate counts.
     */
    public long countLogs(LogSearchRequest request) {
        List<String> conditions = new ArrayList<>();
        List<Object> params = new ArrayList<>();
        conditions.add("tenant_id = ?");
        params.add(tenantId);

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

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

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

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

        if (request.exchangeId() != null && !request.exchangeId().isEmpty()) {
            conditions.add("(exchange_id = ?" +
                    " OR (mapContains(mdc, 'cameleer.exchangeId') AND mdc['cameleer.exchangeId'] = ?)" +
                    " OR (mapContains(mdc, 'camel.exchangeId') AND mdc['camel.exchangeId'] = ?))");
            params.add(request.exchangeId());
            params.add(request.exchangeId());
            params.add(request.exchangeId());
        }

        if (request.q() != null && !request.q().isEmpty()) {
            String term = "%" + escapeLike(request.q()) + "%";
            conditions.add("(message ILIKE ? OR stack_trace ILIKE ?)");
            params.add(term);
            params.add(term);
        }

        if (request.logger() != null && !request.logger().isEmpty()) {
            conditions.add("logger_name ILIKE ?");
            params.add("%" + escapeLike(request.logger()) + "%");
        }

        if (request.sources() != null && !request.sources().isEmpty()) {
            String placeholders = String.join(", ", Collections.nCopies(request.sources().size(), "?"));
            conditions.add("source IN (" + placeholders + ")");
            for (String s : request.sources()) {
                params.add(s);
            }
        }

        if (request.levels() != null && !request.levels().isEmpty()) {
            String placeholders = String.join(", ", Collections.nCopies(request.levels().size(), "?"));
            conditions.add("level IN (" + placeholders + ")");
            for (String lvl : request.levels()) {
                params.add(lvl.toUpperCase());
            }
        }

        if (request.from() != null) {
            conditions.add("timestamp >= parseDateTime64BestEffort(?, 3)");
            params.add(request.from().toString());
        }

        if (request.to() != null) {
            conditions.add("timestamp <= parseDateTime64BestEffort(?, 3)");
            params.add(request.to().toString());
        }

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

    private Map<String, Long> queryLevelCounts(String baseWhere, List<Object> baseParams) {
        String sql = "SELECT level, count() AS cnt FROM logs WHERE " + baseWhere + " GROUP BY level";
        Map<String, Long> counts = new LinkedHashMap<>();
        try {
            jdbc.query(sql, baseParams.toArray(), (rs, rowNum) -> {
                counts.put(rs.getString("level"), rs.getLong("cnt"));
                return null;
            });
        } catch (Exception e) {
            log.warn("Failed to query level counts", e);
        }
        return counts;
    }

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