package com.cameleer.server.app.storage;

import com.cameleer.server.core.agent.AgentEventPage;
import com.cameleer.server.core.agent.AgentEventRecord;
import com.cameleer.server.core.agent.AgentEventRepository;
import org.springframework.jdbc.core.JdbcTemplate;

import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

/**
 * ClickHouse implementation of {@link AgentEventRepository}.
 * <p>
 * The ClickHouse table has no {@code id} column (no BIGSERIAL equivalent),
 * so all returned {@link AgentEventRecord} instances have {@code id = 0}.
 */
public class ClickHouseAgentEventRepository implements AgentEventRepository {

    private static final String INSERT_SQL =
            "INSERT INTO agent_events (tenant_id, instance_id, application_id, environment, event_type, detail) VALUES (?, ?, ?, ?, ?, ?)";

    private static final String SELECT_BASE =
            "SELECT 0 AS id, instance_id, application_id, event_type, detail, timestamp, toString(insert_id) AS insert_id_str FROM agent_events WHERE tenant_id = ?";

    private final String tenantId;
    private final JdbcTemplate jdbc;

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

    @Override
    public void insert(String instanceId, String applicationId, String environment, String eventType, String detail) {
        jdbc.update(INSERT_SQL, tenantId, instanceId, applicationId,
                environment != null ? environment : "default", eventType, detail);
    }

    @Override
    public AgentEventPage queryPage(String applicationId, String instanceId, String environment,
                                     Instant from, Instant to, String cursor, int limit) {
        var sql = new StringBuilder(SELECT_BASE);
        var params = new ArrayList<Object>();
        params.add(tenantId);

        if (applicationId != null) { sql.append(" AND application_id = ?"); params.add(applicationId); }
        if (instanceId != null) { sql.append(" AND instance_id = ?"); params.add(instanceId); }
        if (environment != null) { sql.append(" AND environment = ?"); params.add(environment); }
        if (from != null) { sql.append(" AND timestamp >= ?"); params.add(Timestamp.from(from)); }
        if (to != null) { sql.append(" AND timestamp < ?"); params.add(Timestamp.from(to)); }

        if (cursor != null && !cursor.isEmpty()) {
            String decoded = new String(Base64.getUrlDecoder().decode(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 cursorInsertId = decoded.substring(bar + 1);
            sql.append(" AND (timestamp < ? OR (timestamp = ? AND insert_id < toUUID(?)))");
            params.add(Timestamp.from(cursorTs));
            params.add(Timestamp.from(cursorTs));
            params.add(cursorInsertId);
        }

        sql.append(" ORDER BY timestamp DESC, insert_id DESC LIMIT ?");
        int fetchLimit = limit + 1;
        params.add(fetchLimit);

        List<String> insertIds = new ArrayList<>();
        List<AgentEventRecord> results = new ArrayList<>(jdbc.query(sql.toString(),
                (rs, rowNum) -> {
                    insertIds.add(rs.getString("insert_id_str"));
                    return new AgentEventRecord(
                            rs.getLong("id"),
                            rs.getString("instance_id"),
                            rs.getString("application_id"),
                            rs.getString("event_type"),
                            rs.getString("detail"),
                            rs.getTimestamp("timestamp").toInstant()
                    );
                }, params.toArray()));

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

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

        return new AgentEventPage(results, nextCursor, hasMore);
    }

    @Override
    public List<AgentEventRecord> findInWindow(String environment,
                                               String applicationId,
                                               String instanceId,
                                               List<String> eventTypes,
                                               Instant fromInclusive,
                                               Instant toExclusive,
                                               int limit) {
        if (eventTypes == null || eventTypes.isEmpty()) {
            throw new IllegalArgumentException("eventTypes must not be empty");
        }
        if (fromInclusive == null || toExclusive == null) {
            throw new IllegalArgumentException("from/to must not be null");
        }

        // `event_type IN (?, ?, …)` — one placeholder per type.
        String placeholders = String.join(",", java.util.Collections.nCopies(eventTypes.size(), "?"));
        var sql = new StringBuilder(SELECT_BASE);
        var params = new ArrayList<Object>();
        params.add(tenantId);

        if (environment != null) {
            sql.append(" AND environment = ?");
            params.add(environment);
        }
        if (applicationId != null) {
            sql.append(" AND application_id = ?");
            params.add(applicationId);
        }
        if (instanceId != null) {
            sql.append(" AND instance_id = ?");
            params.add(instanceId);
        }
        sql.append(" AND event_type IN (").append(placeholders).append(")");
        params.addAll(eventTypes);
        sql.append(" AND timestamp >= ? AND timestamp < ?");
        params.add(Timestamp.from(fromInclusive));
        params.add(Timestamp.from(toExclusive));
        sql.append(" ORDER BY timestamp ASC, insert_id ASC LIMIT ?");
        params.add(limit);

        return jdbc.query(sql.toString(),
                (rs, rowNum) -> new AgentEventRecord(
                        rs.getLong("id"),
                        rs.getString("instance_id"),
                        rs.getString("application_id"),
                        rs.getString("event_type"),
                        rs.getString("detail"),
                        rs.getTimestamp("timestamp").toInstant()
                ),
                params.toArray());
    }
}
