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

import com.cameleer.server.core.alerting.AlertState;
import com.cameleer.server.core.alerting.ConditionKind;
import com.cameleer.server.core.alerting.NotificationStatus;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;

/**
 * Micrometer-based metrics for the alerting subsystem.
 * <p>
 * Counters:
 * <ul>
 *   <li>{@code alerting_eval_errors_total{kind}} — evaluation errors by condition kind</li>
 *   <li>{@code alerting_circuit_opened_total{kind}} — circuit breaker open transitions by kind</li>
 *   <li>{@code alerting_notifications_total{status}} — notification outcomes by status</li>
 * </ul>
 * Timers:
 * <ul>
 *   <li>{@code alerting_eval_duration_seconds{kind}} — per-kind evaluation latency</li>
 *   <li>{@code alerting_webhook_delivery_duration_seconds} — webhook POST latency</li>
 * </ul>
 * Gauges (read from PostgreSQL, cached for {@link #DEFAULT_GAUGE_TTL} to amortise
 * Prometheus scrapes that may fire every few seconds):
 * <ul>
 *   <li>{@code alerting_rules_total{state=enabled|disabled}} — rule counts from {@code alert_rules}</li>
 *   <li>{@code alerting_instances_total{state}} — instance counts grouped from {@code alert_instances}</li>
 * </ul>
 */
@Component
public class AlertingMetrics {

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

    /** Default time-to-live for the gauge-supplier caches. */
    static final Duration DEFAULT_GAUGE_TTL = Duration.ofSeconds(30);

    private final MeterRegistry registry;

    // Cached counters per kind (lazy-initialized)
    private final ConcurrentMap<String, Counter> evalErrorCounters   = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, Counter> circuitOpenCounters = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, Timer>   evalDurationTimers  = new ConcurrentHashMap<>();

    // Notification outcome counter per status
    private final ConcurrentMap<String, Counter> notificationCounters = new ConcurrentHashMap<>();

    // Shared delivery timer
    private final Timer webhookDeliveryTimer;

    // TTL-cached gauge suppliers registered so tests can force a read cycle.
    private final TtlCache enabledRulesCache;
    private final TtlCache disabledRulesCache;
    private final Map<AlertState, TtlCache> instancesByStateCaches;

    /**
     * Production constructor: wraps the Postgres-backed gauge suppliers in a
     * 30-second TTL cache so Prometheus scrapes don't cause per-scrape DB queries.
     */
    @Autowired
    public AlertingMetrics(MeterRegistry registry, JdbcTemplate jdbc) {
        this(registry,
             () -> countRules(jdbc, true),
             () -> countRules(jdbc, false),
             state -> countInstances(jdbc, state),
             DEFAULT_GAUGE_TTL,
             Instant::now);
    }

    /**
     * Test-friendly constructor accepting the three gauge suppliers that are
     * exercised in the {@link AlertingMetricsCachingTest} plan sketch. The
     * {@code instancesSupplier} is used for every {@link AlertState}.
     */
    AlertingMetrics(MeterRegistry registry,
                    Supplier<Long> enabledRulesSupplier,
                    Supplier<Long> disabledRulesSupplier,
                    Supplier<Long> instancesSupplier,
                    Duration gaugeTtl,
                    Supplier<Instant> clock) {
        this(registry,
             enabledRulesSupplier,
             disabledRulesSupplier,
             state -> instancesSupplier.get(),
             gaugeTtl,
             clock);
    }

    /**
     * Core constructor: accepts per-state instance supplier so production can
     * query PostgreSQL with a different value per {@link AlertState}.
     */
    private AlertingMetrics(MeterRegistry registry,
                            Supplier<Long> enabledRulesSupplier,
                            Supplier<Long> disabledRulesSupplier,
                            java.util.function.Function<AlertState, Long> instancesSupplier,
                            Duration gaugeTtl,
                            Supplier<Instant> clock) {
        this.registry = registry;

        // ── Static timers ───────────────────────────────────────────────
        this.webhookDeliveryTimer = Timer.builder("alerting_webhook_delivery_duration_seconds")
                .description("Latency of outbound webhook POST requests")
                .register(registry);

        // ── Gauge: rules by enabled/disabled (cached) ───────────────────
        this.enabledRulesCache  = new TtlCache(enabledRulesSupplier,  gaugeTtl, clock);
        this.disabledRulesCache = new TtlCache(disabledRulesSupplier, gaugeTtl, clock);

        Gauge.builder("alerting_rules_total", enabledRulesCache, TtlCache::getAsDouble)
                .tag("state", "enabled")
                .description("Number of enabled alert rules")
                .register(registry);
        Gauge.builder("alerting_rules_total", disabledRulesCache, TtlCache::getAsDouble)
                .tag("state", "disabled")
                .description("Number of disabled alert rules")
                .register(registry);

        // ── Gauges: alert instances by state (cached) ───────────────────
        this.instancesByStateCaches = new EnumMap<>(AlertState.class);
        for (AlertState state : AlertState.values()) {
            AlertState captured = state;
            TtlCache cache = new TtlCache(() -> instancesSupplier.apply(captured), gaugeTtl, clock);
            this.instancesByStateCaches.put(state, cache);
            Gauge.builder("alerting_instances_total", cache, TtlCache::getAsDouble)
                    .tag("state", state.name().toLowerCase())
                    .description("Number of alert instances by state")
                    .register(registry);
        }
    }

    // ── Public API ──────────────────────────────────────────────────────

    /**
     * Increment the evaluation error counter for the given condition kind and rule.
     */
    public void evalError(ConditionKind kind, UUID ruleId) {
        String key = kind.name();
        evalErrorCounters.computeIfAbsent(key, k ->
            Counter.builder("alerting_eval_errors_total")
                   .tag("kind", kind.name())
                   .description("Alerting evaluation errors by condition kind")
                   .register(registry))
            .increment();
        log.debug("Alerting eval error for kind={} ruleId={}", kind, ruleId);
    }

    /**
     * Increment the circuit-breaker opened counter for the given condition kind.
     */
    public void circuitOpened(ConditionKind kind) {
        String key = kind.name();
        circuitOpenCounters.computeIfAbsent(key, k ->
            Counter.builder("alerting_circuit_opened_total")
                   .tag("kind", kind.name())
                   .description("Circuit breaker open transitions by condition kind")
                   .register(registry))
            .increment();
    }

    /**
     * Return the eval duration timer for the given condition kind (creates lazily if absent).
     */
    public Timer evalDuration(ConditionKind kind) {
        return evalDurationTimers.computeIfAbsent(kind.name(), k ->
            Timer.builder("alerting_eval_duration_seconds")
                 .tag("kind", kind.name())
                 .description("Alerting condition evaluation latency by kind")
                 .register(registry));
    }

    /**
     * The shared webhook delivery duration timer.
     */
    public Timer webhookDeliveryDuration() {
        return webhookDeliveryTimer;
    }

    /**
     * Increment the notification outcome counter for the given status.
     */
    public void notificationOutcome(NotificationStatus status) {
        String key = status.name();
        notificationCounters.computeIfAbsent(key, k ->
            Counter.builder("alerting_notifications_total")
                   .tag("status", status.name().toLowerCase())
                   .description("Alerting notification outcomes by status")
                   .register(registry))
            .increment();
    }

    /**
     * Force a read of every TTL-cached gauge supplier. Used by tests to simulate
     * a Prometheus scrape without needing a real registry scrape pipeline.
     */
    void snapshotAllGauges() {
        List<TtlCache> all = new ArrayList<>();
        all.add(enabledRulesCache);
        all.add(disabledRulesCache);
        all.addAll(instancesByStateCaches.values());
        for (TtlCache c : all) {
            c.getAsDouble();
        }
    }

    // ── Gauge suppliers (queried at most once per TTL) ──────────────────

    private static long countRules(JdbcTemplate jdbc, boolean enabled) {
        try {
            Long count = jdbc.queryForObject(
                "SELECT COUNT(*) FROM alert_rules WHERE enabled = ?", Long.class, enabled);
            return count == null ? 0L : count;
        } catch (Exception e) {
            log.debug("alerting_rules gauge query failed: {}", e.getMessage());
            return 0L;
        }
    }

    private static long countInstances(JdbcTemplate jdbc, AlertState state) {
        try {
            Long count = jdbc.queryForObject(
                "SELECT COUNT(*) FROM alert_instances WHERE state = ?::alert_state_enum",
                Long.class, state.name());
            return count == null ? 0L : count;
        } catch (Exception e) {
            log.debug("alerting_instances gauge query failed: {}", e.getMessage());
            return 0L;
        }
    }

    /**
     * Lightweight TTL cache around a {@code Supplier<Long>}. Every call to
     * {@link #getAsDouble()} either returns the cached value (if {@code clock.get()
     * - lastRead < ttl}) or invokes the delegate and refreshes the cache.
     *
     * <p>Used to amortise Postgres queries behind Prometheus gauges over a
     * 30-second TTL (see {@link AlertingMetrics#DEFAULT_GAUGE_TTL}).
     */
    static final class TtlCache {
        private final Supplier<Long> delegate;
        private final Duration ttl;
        private final Supplier<Instant> clock;
        private volatile Instant lastRead = Instant.MIN;
        private volatile long cached = 0L;

        TtlCache(Supplier<Long> delegate, Duration ttl, Supplier<Instant> clock) {
            this.delegate = delegate;
            this.ttl = ttl;
            this.clock = clock;
        }

        synchronized double getAsDouble() {
            Instant now = clock.get();
            if (lastRead == Instant.MIN || Duration.between(lastRead, now).compareTo(ttl) >= 0) {
                cached = delegate.get();
                lastRead = now;
            }
            return cached;
        }
    }
}
