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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

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

public class PostgresAlertInstanceRepository implements AlertInstanceRepository {

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

    private final JdbcTemplate jdbc;
    private final ObjectMapper om;

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

    @Override
    public AlertInstance save(AlertInstance i) {
        String sql = """
            INSERT INTO alert_instances (
                id, rule_id, rule_snapshot, environment_id, state, severity,
                fired_at, acked_at, acked_by, resolved_at, last_notified_at,
                read_at, deleted_at,
                silenced, current_value, threshold, context, title, message,
                target_user_ids, target_group_ids, target_role_names)
            VALUES (?, ?, ?::jsonb, ?, ?::alert_state_enum, ?::severity_enum,
                    ?, ?, ?, ?, ?,
                    ?, ?,
                    ?, ?, ?, ?::jsonb, ?, ?,
                    ?, ?, ?)
            ON CONFLICT (id) DO UPDATE SET
                state             = EXCLUDED.state,
                acked_at          = EXCLUDED.acked_at,
                acked_by          = EXCLUDED.acked_by,
                resolved_at       = EXCLUDED.resolved_at,
                last_notified_at  = EXCLUDED.last_notified_at,
                read_at           = EXCLUDED.read_at,
                deleted_at        = EXCLUDED.deleted_at,
                silenced          = EXCLUDED.silenced,
                current_value     = EXCLUDED.current_value,
                threshold         = EXCLUDED.threshold,
                context           = EXCLUDED.context,
                title             = EXCLUDED.title,
                message           = EXCLUDED.message,
                target_user_ids   = EXCLUDED.target_user_ids,
                target_group_ids  = EXCLUDED.target_group_ids,
                target_role_names = EXCLUDED.target_role_names
            """;
        Array userIds  = toTextArray(i.targetUserIds());
        Array groupIds = toUuidArray(i.targetGroupIds());
        Array roleNames = toTextArray(i.targetRoleNames());

        try {
            jdbc.update(sql,
                i.id(), i.ruleId(), writeJson(i.ruleSnapshot()),
                i.environmentId(), i.state().name(), i.severity().name(),
                ts(i.firedAt()), ts(i.ackedAt()), i.ackedBy(),
                ts(i.resolvedAt()), ts(i.lastNotifiedAt()),
                ts(i.readAt()), ts(i.deletedAt()),
                i.silenced(), i.currentValue(), i.threshold(),
                writeJson(i.context()), i.title(), i.message(),
                userIds, groupIds, roleNames);
        } catch (DuplicateKeyException e) {
            log.info("Skipped duplicate open alert_instance for rule {}: {}", i.ruleId(), e.getMessage());
            return findOpenForRule(i.ruleId()).orElse(i);
        }
        return i;
    }

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

    @Override
    public Optional<AlertInstance> findOpenForRule(UUID ruleId) {
        var list = jdbc.query("""
            SELECT * FROM alert_instances
             WHERE rule_id = ?
               AND state IN ('PENDING','FIRING')
               AND deleted_at IS NULL
             LIMIT 1
            """, rowMapper(), ruleId);
        return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
    }

    @Override
    public List<AlertInstance> listForInbox(UUID environmentId,
                                            List<String> userGroupIdFilter,
                                            String userId,
                                            List<String> userRoleNames,
                                            List<AlertState> states,
                                            List<AlertSeverity> severities,
                                            Boolean acked,
                                            Boolean read,
                                            int limit) {
        Array groupArray = toUuidArrayFromStrings(userGroupIdFilter);
        Array roleArray  = toTextArray(userRoleNames);

        StringBuilder sql = new StringBuilder("""
            SELECT * FROM alert_instances
             WHERE environment_id = ?
               AND (
                   ? = ANY(target_user_ids)
                   OR target_group_ids && ?
                   OR target_role_names && ?
               )
            """);
        List<Object> args = new ArrayList<>(List.of(environmentId, userId, groupArray, roleArray));

        if (states != null && !states.isEmpty()) {
            Array stateArray = toTextArray(states.stream().map(Enum::name).toList());
            sql.append(" AND state::text = ANY(?)");
            args.add(stateArray);
        }
        if (severities != null && !severities.isEmpty()) {
            Array severityArray = toTextArray(severities.stream().map(Enum::name).toList());
            sql.append(" AND severity::text = ANY(?)");
            args.add(severityArray);
        }
        if (acked != null) {
            sql.append(acked ? " AND acked_at IS NOT NULL" : " AND acked_at IS NULL");
        }
        if (read != null) {
            sql.append(read ? " AND read_at IS NOT NULL" : " AND read_at IS NULL");
        }
        sql.append(" AND deleted_at IS NULL");
        sql.append(" ORDER BY fired_at DESC LIMIT ?");
        args.add(limit);

        return jdbc.query(sql.toString(), rowMapper(), args.toArray());
    }

    @Override
    public Map<AlertSeverity, Long> countUnreadBySeverity(UUID environmentId,
                                                          String userId,
                                                          List<String> groupIds,
                                                          List<String> roleNames) {
        Array groupArray = toUuidArrayFromStrings(groupIds);
        Array roleArray  = toTextArray(roleNames);
        String sql = """
            SELECT severity::text AS severity, COUNT(*) AS cnt
              FROM alert_instances
             WHERE environment_id = ?
               AND read_at IS NULL
               AND deleted_at IS NULL
               AND (
                   ? = ANY(target_user_ids)
                   OR target_group_ids && ?
                   OR target_role_names && ?
               )
             GROUP BY severity
            """;
        EnumMap<AlertSeverity, Long> counts = new EnumMap<>(AlertSeverity.class);
        for (AlertSeverity s : AlertSeverity.values()) counts.put(s, 0L);
        jdbc.query(sql, (org.springframework.jdbc.core.RowCallbackHandler) rs -> counts.put(
            AlertSeverity.valueOf(rs.getString("severity")), rs.getLong("cnt")
        ), environmentId, userId, groupArray, roleArray);
        return counts;
    }

    @Override
    public void ack(UUID id, String userId, Instant when) {
        jdbc.update("""
            UPDATE alert_instances
               SET acked_at = ?, acked_by = ?
             WHERE id = ? AND acked_at IS NULL AND deleted_at IS NULL
            """, Timestamp.from(when), userId, id);
    }

    @Override
    public void markRead(UUID id, Instant when) {
        jdbc.update("UPDATE alert_instances SET read_at = ? WHERE id = ? AND read_at IS NULL",
                Timestamp.from(when), id);
    }

    @Override
    public void bulkMarkRead(List<UUID> ids, Instant when) {
        if (ids == null || ids.isEmpty()) return;
        Array idArray = jdbc.execute((ConnectionCallback<Array>) c ->
            c.createArrayOf("uuid", ids.toArray()));
        jdbc.update("""
            UPDATE alert_instances SET read_at = ?
             WHERE id = ANY(?) AND read_at IS NULL AND deleted_at IS NULL
            """, Timestamp.from(when), idArray);
    }

    @Override
    public void softDelete(UUID id, Instant when) {
        jdbc.update("UPDATE alert_instances SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL",
                Timestamp.from(when), id);
    }

    @Override
    public void bulkSoftDelete(List<UUID> ids, Instant when) {
        if (ids == null || ids.isEmpty()) return;
        Array idArray = jdbc.execute((ConnectionCallback<Array>) c ->
            c.createArrayOf("uuid", ids.toArray()));
        jdbc.update("""
            UPDATE alert_instances SET deleted_at = ?
             WHERE id = ANY(?) AND deleted_at IS NULL
            """, Timestamp.from(when), idArray);
    }

    @Override
    public void restore(UUID id) {
        jdbc.update("UPDATE alert_instances SET deleted_at = NULL WHERE id = ?", id);
    }

    @Override
    public void bulkAck(List<UUID> ids, String userId, Instant when) {
        if (ids == null || ids.isEmpty()) return;
        Array idArray = jdbc.execute((ConnectionCallback<Array>) c ->
            c.createArrayOf("uuid", ids.toArray()));
        jdbc.update("""
            UPDATE alert_instances SET acked_at = ?, acked_by = ?
             WHERE id = ANY(?) AND acked_at IS NULL AND deleted_at IS NULL
            """, Timestamp.from(when), userId, idArray);
    }

    @Override
    public void resolve(UUID id, Instant when) {
        jdbc.update("""
            UPDATE alert_instances
               SET state = 'RESOLVED'::alert_state_enum,
                   resolved_at = ?
             WHERE id = ?
            """, Timestamp.from(when), id);
    }

    @Override
    public void markSilenced(UUID id, boolean silenced) {
        jdbc.update("UPDATE alert_instances SET silenced = ? WHERE id = ?", silenced, id);
    }

    @Override
    public List<AlertInstance> listFiringDueForReNotify(Instant now) {
        return jdbc.query("""
            SELECT ai.* FROM alert_instances ai
              JOIN alert_rules ar ON ar.id = ai.rule_id
             WHERE ai.state = 'FIRING'::alert_state_enum
               AND ai.silenced = false
               AND ar.enabled = true
               AND ar.re_notify_minutes > 0
               AND ai.last_notified_at IS NOT NULL
               AND ai.last_notified_at + make_interval(mins => ar.re_notify_minutes) <= ?
            """, rowMapper(), Timestamp.from(now));
    }

    @Override
    public List<UUID> filterInEnvLive(List<UUID> ids, UUID environmentId) {
        if (ids == null || ids.isEmpty()) return List.of();
        Array idArray = jdbc.execute((ConnectionCallback<Array>) c ->
            c.createArrayOf("uuid", ids.toArray()));
        return jdbc.query("""
            SELECT id FROM alert_instances
             WHERE id = ANY(?) AND environment_id = ? AND deleted_at IS NULL
            """, (rs, i) -> (UUID) rs.getObject("id"), idArray, environmentId);
    }

    @Override
    public void deleteResolvedBefore(Instant cutoff) {
        jdbc.update("""
            DELETE FROM alert_instances
             WHERE state = 'RESOLVED'::alert_state_enum
               AND resolved_at < ?
            """, Timestamp.from(cutoff));
    }

    // -------------------------------------------------------------------------

    private RowMapper<AlertInstance> rowMapper() {
        return (rs, i) -> {
            try {
                Map<String, Object> snapshot = om.readValue(
                    rs.getString("rule_snapshot"), new TypeReference<>() {});
                Map<String, Object> context = om.readValue(
                    rs.getString("context"), new TypeReference<>() {});

                Timestamp ackedAt = rs.getTimestamp("acked_at");
                Timestamp resolvedAt = rs.getTimestamp("resolved_at");
                Timestamp lastNotifiedAt = rs.getTimestamp("last_notified_at");
                Timestamp readAt = rs.getTimestamp("read_at");
                Timestamp deletedAt = rs.getTimestamp("deleted_at");

                Object cvObj = rs.getObject("current_value");
                Double currentValue = cvObj == null ? null : ((Number) cvObj).doubleValue();
                Object thObj = rs.getObject("threshold");
                Double threshold = thObj == null ? null : ((Number) thObj).doubleValue();

                UUID ruleId = rs.getObject("rule_id") == null ? null : (UUID) rs.getObject("rule_id");

                return new AlertInstance(
                    (UUID) rs.getObject("id"),
                    ruleId,
                    snapshot,
                    (UUID) rs.getObject("environment_id"),
                    AlertState.valueOf(rs.getString("state")),
                    AlertSeverity.valueOf(rs.getString("severity")),
                    rs.getTimestamp("fired_at").toInstant(),
                    ackedAt == null ? null : ackedAt.toInstant(),
                    rs.getString("acked_by"),
                    resolvedAt == null ? null : resolvedAt.toInstant(),
                    lastNotifiedAt == null ? null : lastNotifiedAt.toInstant(),
                    readAt == null ? null : readAt.toInstant(),
                    deletedAt == null ? null : deletedAt.toInstant(),
                    rs.getBoolean("silenced"),
                    currentValue,
                    threshold,
                    context,
                    rs.getString("title"),
                    rs.getString("message"),
                    readTextArray(rs.getArray("target_user_ids")),
                    readUuidArray(rs.getArray("target_group_ids")),
                    readTextArray(rs.getArray("target_role_names")));
            } catch (Exception e) {
                throw new IllegalStateException("Failed to map alert_instances row", e);
            }
        };
    }

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

    private Timestamp ts(Instant instant) {
        return instant == null ? null : Timestamp.from(instant);
    }

    private Array toTextArray(List<String> items) {
        return jdbc.execute((ConnectionCallback<Array>) conn ->
            conn.createArrayOf("text", items.toArray()));
    }

    private Array toUuidArray(List<UUID> ids) {
        return jdbc.execute((ConnectionCallback<Array>) conn ->
            conn.createArrayOf("uuid", ids.toArray()));
    }

    private Array toUuidArrayFromStrings(List<String> ids) {
        return jdbc.execute((ConnectionCallback<Array>) conn ->
            conn.createArrayOf("uuid",
                ids.stream().map(UUID::fromString).toArray()));
    }

    private List<String> readTextArray(Array arr) throws SQLException {
        if (arr == null) return List.of();
        Object[] raw = (Object[]) arr.getArray();
        List<String> out = new ArrayList<>(raw.length);
        for (Object o : raw) out.add((String) o);
        return out;
    }

    private List<UUID> readUuidArray(Array arr) throws SQLException {
        if (arr == null) return List.of();
        Object[] raw = (Object[]) arr.getArray();
        List<UUID> out = new ArrayList<>(raw.length);
        for (Object o : raw) out.add((UUID) o);
        return out;
    }
}
