package com.cameleer.server.app.alerting.storage;

import com.cameleer.server.core.alerting.*;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.sql.Timestamp;
import java.time.Instant;
import java.util.*;

public class PostgresAlertRuleRepository implements AlertRuleRepository {

    private final JdbcTemplate jdbc;
    private final ObjectMapper om;

    public PostgresAlertRuleRepository(JdbcTemplate jdbc, ObjectMapper om) {
        this.jdbc = jdbc;
        this.om = om;
    }

    @Override
    public AlertRule save(AlertRule r) {
        String sql = """
            INSERT INTO alert_rules (id, environment_id, name, description, severity, enabled,
                condition_kind, condition, evaluation_interval_seconds, for_duration_seconds,
                re_notify_minutes, notification_title_tmpl, notification_message_tmpl,
                webhooks, next_evaluation_at, claimed_by, claimed_until, eval_state,
                created_at, created_by, updated_at, updated_by)
            VALUES (?, ?, ?, ?, ?::severity_enum, ?, ?::condition_kind_enum, ?::jsonb, ?, ?, ?, ?, ?, ?::jsonb,
                ?, ?, ?, ?::jsonb, ?, ?, ?, ?)
            ON CONFLICT (id) DO UPDATE SET
                name = EXCLUDED.name, description = EXCLUDED.description,
                severity = EXCLUDED.severity, enabled = EXCLUDED.enabled,
                condition_kind = EXCLUDED.condition_kind, condition = EXCLUDED.condition,
                evaluation_interval_seconds = EXCLUDED.evaluation_interval_seconds,
                for_duration_seconds = EXCLUDED.for_duration_seconds,
                re_notify_minutes = EXCLUDED.re_notify_minutes,
                notification_title_tmpl = EXCLUDED.notification_title_tmpl,
                notification_message_tmpl = EXCLUDED.notification_message_tmpl,
                webhooks = EXCLUDED.webhooks, eval_state = EXCLUDED.eval_state,
                updated_at = EXCLUDED.updated_at, updated_by = EXCLUDED.updated_by
            """;
        jdbc.update(sql,
            r.id(), r.environmentId(), r.name(), r.description(),
            r.severity().name(), r.enabled(), r.conditionKind().name(),
            writeJson(r.condition()),
            r.evaluationIntervalSeconds(), r.forDurationSeconds(), r.reNotifyMinutes(),
            r.notificationTitleTmpl(), r.notificationMessageTmpl(),
            writeJson(r.webhooks()),
            Timestamp.from(r.nextEvaluationAt()),
            r.claimedBy(),
            r.claimedUntil() == null ? null : Timestamp.from(r.claimedUntil()),
            writeJson(r.evalState()),
            Timestamp.from(r.createdAt()), r.createdBy(),
            Timestamp.from(r.updatedAt()), r.updatedBy());
        saveTargets(r.id(), r.targets());
        return r;
    }

    private void saveTargets(UUID ruleId, List<AlertRuleTarget> targets) {
        jdbc.update("DELETE FROM alert_rule_targets WHERE rule_id = ?", ruleId);
        if (targets == null || targets.isEmpty()) return;
        jdbc.batchUpdate(
            "INSERT INTO alert_rule_targets (id, rule_id, target_kind, target_id) VALUES (?, ?, ?::target_kind_enum, ?)",
            targets, targets.size(), (ps, t) -> {
                ps.setObject(1, t.id() != null ? t.id() : UUID.randomUUID());
                ps.setObject(2, ruleId);
                ps.setString(3, t.kind().name());
                ps.setString(4, t.targetId());
            });
    }

    @Override
    public Optional<AlertRule> findById(UUID id) {
        var list = jdbc.query("SELECT * FROM alert_rules WHERE id = ?", rowMapper(), id);
        if (list.isEmpty()) return Optional.empty();
        return Optional.of(withTargets(list).get(0));
    }

    @Override
    public List<AlertRule> listByEnvironment(UUID environmentId) {
        var list = jdbc.query(
            "SELECT * FROM alert_rules WHERE environment_id = ? ORDER BY created_at DESC",
            rowMapper(), environmentId);
        return withTargets(list);
    }

    @Override
    public List<AlertRule> findAllByOutboundConnectionId(UUID connectionId) {
        String sql = """
            SELECT * FROM alert_rules
             WHERE webhooks @> ?::jsonb
             ORDER BY created_at DESC
            """;
        String predicate = "[{\"outboundConnectionId\":\"" + connectionId + "\"}]";
        return jdbc.query(sql, rowMapper(), predicate);
    }

    @Override
    public List<UUID> findRuleIdsByOutboundConnectionId(UUID connectionId) {
        String sql = """
            SELECT id FROM alert_rules
             WHERE webhooks @> ?::jsonb
            """;
        String predicate = "[{\"outboundConnectionId\":\"" + connectionId + "\"}]";
        return jdbc.queryForList(sql, UUID.class, predicate);
    }

    @Override
    public void delete(UUID id) {
        jdbc.update("DELETE FROM alert_rules WHERE id = ?", id);
    }

    @Override
    public List<AlertRule> claimDueRules(String instanceId, int batchSize, int claimTtlSeconds) {
        String sql = """
            UPDATE alert_rules
               SET claimed_by = ?, claimed_until = now() + (? || ' seconds')::interval
             WHERE id IN (
                 SELECT id FROM alert_rules
                  WHERE enabled = true
                    AND next_evaluation_at <= now()
                    AND (claimed_until IS NULL OR claimed_until < now())
                  ORDER BY next_evaluation_at
                  LIMIT ?
                  FOR UPDATE SKIP LOCKED
             )
             RETURNING *
            """;
        List<AlertRule> rules = jdbc.query(sql, rowMapper(), instanceId, claimTtlSeconds, batchSize);
        return withTargets(rules);
    }

    /** Batch-loads targets for the given rules and returns new rule instances with targets populated. */
    private List<AlertRule> withTargets(List<AlertRule> rules) {
        if (rules.isEmpty()) return rules;
        // Build IN clause
        String inClause = rules.stream()
            .map(r -> "'" + r.id() + "'")
            .collect(java.util.stream.Collectors.joining(","));
        String sql = "SELECT * FROM alert_rule_targets WHERE rule_id IN (" + inClause + ")";
        Map<UUID, List<AlertRuleTarget>> byRuleId = new HashMap<>();
        jdbc.query(sql, rs -> {
            UUID ruleId = (UUID) rs.getObject("rule_id");
            AlertRuleTarget t = new AlertRuleTarget(
                (UUID) rs.getObject("id"),
                ruleId,
                TargetKind.valueOf(rs.getString("target_kind")),
                rs.getString("target_id"));
            byRuleId.computeIfAbsent(ruleId, k -> new ArrayList<>()).add(t);
        });
        return rules.stream()
            .map(r -> new AlertRule(
                r.id(), r.environmentId(), r.name(), r.description(),
                r.severity(), r.enabled(), r.conditionKind(), r.condition(),
                r.evaluationIntervalSeconds(), r.forDurationSeconds(), r.reNotifyMinutes(),
                r.notificationTitleTmpl(), r.notificationMessageTmpl(),
                r.webhooks(), byRuleId.getOrDefault(r.id(), List.of()),
                r.nextEvaluationAt(), r.claimedBy(), r.claimedUntil(), r.evalState(),
                r.createdAt(), r.createdBy(), r.updatedAt(), r.updatedBy()))
            .toList();
    }

    @Override
    public void releaseClaim(UUID ruleId, Instant nextEvaluationAt, Map<String, Object> evalState) {
        jdbc.update("""
            UPDATE alert_rules
               SET claimed_by = NULL, claimed_until = NULL,
                   next_evaluation_at = ?, eval_state = ?::jsonb
             WHERE id = ?
            """,
            Timestamp.from(nextEvaluationAt), writeJson(evalState), ruleId);
    }

    private RowMapper<AlertRule> rowMapper() {
        return (rs, i) -> {
            try {
                ConditionKind kind = ConditionKind.valueOf(rs.getString("condition_kind"));
                AlertCondition cond = om.readValue(rs.getString("condition"), AlertCondition.class);
                List<WebhookBinding> webhooks = om.readValue(
                    rs.getString("webhooks"), new TypeReference<>() {});
                Map<String, Object> evalState = om.readValue(
                    rs.getString("eval_state"), new TypeReference<>() {});

                Timestamp cu = rs.getTimestamp("claimed_until");
                return new AlertRule(
                    (UUID) rs.getObject("id"),
                    (UUID) rs.getObject("environment_id"),
                    rs.getString("name"),
                    rs.getString("description"),
                    AlertSeverity.valueOf(rs.getString("severity")),
                    rs.getBoolean("enabled"),
                    kind, cond,
                    rs.getInt("evaluation_interval_seconds"),
                    rs.getInt("for_duration_seconds"),
                    rs.getInt("re_notify_minutes"),
                    rs.getString("notification_title_tmpl"),
                    rs.getString("notification_message_tmpl"),
                    webhooks, List.of(),
                    rs.getTimestamp("next_evaluation_at").toInstant(),
                    rs.getString("claimed_by"),
                    cu == null ? null : cu.toInstant(),
                    evalState,
                    rs.getTimestamp("created_at").toInstant(),
                    rs.getString("created_by"),
                    rs.getTimestamp("updated_at").toInstant(),
                    rs.getString("updated_by"));
            } catch (Exception e) {
                throw new IllegalStateException("Failed to map alert_rules row", e);
            }
        };
    }

    private String writeJson(Object o) {
        try {
            return om.writeValueAsString(o);
        } catch (Exception e) {
            throw new IllegalStateException("Failed to serialize to JSON", e);
        }
    }
}
