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

import com.cameleer.server.core.alerting.AlertNotification;
import com.cameleer.server.core.alerting.AlertNotificationRepository;
import com.cameleer.server.core.alerting.NotificationStatus;
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.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

public class PostgresAlertNotificationRepository implements AlertNotificationRepository {

    private final JdbcTemplate jdbc;
    private final ObjectMapper om;

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

    @Override
    public AlertNotification save(AlertNotification n) {
        jdbc.update("""
            INSERT INTO alert_notifications (
                id, alert_instance_id, webhook_id, outbound_connection_id,
                status, attempts, next_attempt_at, claimed_by, claimed_until,
                last_response_status, last_response_snippet, payload, delivered_at, created_at)
            VALUES (?, ?, ?, ?,
                    ?::notification_status_enum, ?, ?, ?, ?,
                    ?, ?, ?::jsonb, ?, ?)
            ON CONFLICT (id) DO UPDATE SET
                status                = EXCLUDED.status,
                attempts              = EXCLUDED.attempts,
                next_attempt_at       = EXCLUDED.next_attempt_at,
                claimed_by            = EXCLUDED.claimed_by,
                claimed_until         = EXCLUDED.claimed_until,
                last_response_status  = EXCLUDED.last_response_status,
                last_response_snippet = EXCLUDED.last_response_snippet,
                payload               = EXCLUDED.payload,
                delivered_at          = EXCLUDED.delivered_at
            """,
            n.id(), n.alertInstanceId(), n.webhookId(), n.outboundConnectionId(),
            n.status().name(), n.attempts(), Timestamp.from(n.nextAttemptAt()),
            n.claimedBy(), n.claimedUntil() == null ? null : Timestamp.from(n.claimedUntil()),
            n.lastResponseStatus(), n.lastResponseSnippet(),
            writeJson(n.payload()),
            n.deliveredAt() == null ? null : Timestamp.from(n.deliveredAt()),
            Timestamp.from(n.createdAt()));
        return n;
    }

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

    @Override
    public List<AlertNotification> listForInstance(UUID alertInstanceId) {
        return jdbc.query("""
            SELECT * FROM alert_notifications
             WHERE alert_instance_id = ?
             ORDER BY created_at DESC
            """, rowMapper(), alertInstanceId);
    }

    @Override
    public List<AlertNotification> claimDueNotifications(String instanceId, int batchSize, int claimTtlSeconds) {
        String sql = """
            UPDATE alert_notifications
               SET claimed_by = ?, claimed_until = now() + (? || ' seconds')::interval
             WHERE id IN (
                 SELECT id FROM alert_notifications
                  WHERE status = 'PENDING'::notification_status_enum
                    AND next_attempt_at <= now()
                    AND (claimed_until IS NULL OR claimed_until < now())
                  ORDER BY next_attempt_at
                  LIMIT ?
                  FOR UPDATE SKIP LOCKED
             )
             RETURNING *
            """;
        return jdbc.query(sql, rowMapper(), instanceId, claimTtlSeconds, batchSize);
    }

    @Override
    public void markDelivered(UUID id, int status, String snippet, Instant when) {
        jdbc.update("""
            UPDATE alert_notifications
               SET status = 'DELIVERED'::notification_status_enum,
                   last_response_status  = ?,
                   last_response_snippet = ?,
                   delivered_at          = ?,
                   claimed_by            = NULL,
                   claimed_until         = NULL
             WHERE id = ?
            """, status, snippet, Timestamp.from(when), id);
    }

    @Override
    public void scheduleRetry(UUID id, Instant nextAttemptAt, int status, String snippet) {
        jdbc.update("""
            UPDATE alert_notifications
               SET attempts              = attempts + 1,
                   next_attempt_at       = ?,
                   last_response_status  = ?,
                   last_response_snippet = ?,
                   claimed_by            = NULL,
                   claimed_until         = NULL
             WHERE id = ?
            """, Timestamp.from(nextAttemptAt), status, snippet, id);
    }

    @Override
    public void resetForRetry(UUID id, Instant nextAttemptAt) {
        jdbc.update("""
            UPDATE alert_notifications
               SET attempts              = 0,
                   status               = 'PENDING'::notification_status_enum,
                   next_attempt_at      = ?,
                   claimed_by           = NULL,
                   claimed_until        = NULL,
                   last_response_status = NULL,
                   last_response_snippet = NULL
             WHERE id = ?
            """, Timestamp.from(nextAttemptAt), id);
    }

    @Override
    public void markFailed(UUID id, int status, String snippet) {
        jdbc.update("""
            UPDATE alert_notifications
               SET status                = 'FAILED'::notification_status_enum,
                   attempts              = attempts + 1,
                   last_response_status  = ?,
                   last_response_snippet = ?,
                   claimed_by            = NULL,
                   claimed_until         = NULL
             WHERE id = ?
            """, status, snippet, id);
    }

    @Override
    public void deleteSettledBefore(Instant cutoff) {
        jdbc.update("""
            DELETE FROM alert_notifications
             WHERE status IN ('DELIVERED'::notification_status_enum, 'FAILED'::notification_status_enum)
               AND created_at < ?
            """, Timestamp.from(cutoff));
    }

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

    private RowMapper<AlertNotification> rowMapper() {
        return (rs, i) -> {
            try {
                Map<String, Object> payload = om.readValue(
                    rs.getString("payload"), new TypeReference<>() {});
                Timestamp claimedUntil = rs.getTimestamp("claimed_until");
                Timestamp deliveredAt = rs.getTimestamp("delivered_at");
                Object lastStatus = rs.getObject("last_response_status");

                Object webhookIdObj = rs.getObject("webhook_id");
                UUID webhookId = webhookIdObj == null ? null : (UUID) webhookIdObj;
                Object connIdObj = rs.getObject("outbound_connection_id");
                UUID connId = connIdObj == null ? null : (UUID) connIdObj;

                return new AlertNotification(
                    (UUID) rs.getObject("id"),
                    (UUID) rs.getObject("alert_instance_id"),
                    webhookId,
                    connId,
                    NotificationStatus.valueOf(rs.getString("status")),
                    rs.getInt("attempts"),
                    rs.getTimestamp("next_attempt_at").toInstant(),
                    rs.getString("claimed_by"),
                    claimedUntil == null ? null : claimedUntil.toInstant(),
                    lastStatus == null ? null : ((Number) lastStatus).intValue(),
                    rs.getString("last_response_snippet"),
                    payload,
                    deliveredAt == null ? null : deliveredAt.toInstant(),
                    rs.getTimestamp("created_at").toInstant());
            } catch (Exception e) {
                throw new IllegalStateException("Failed to map alert_notifications row", e);
            }
        };
    }

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