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

import com.cameleer.server.app.alerting.metrics.AlertingMetrics;
import com.cameleer.server.core.alerting.ConditionKind;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.ConcurrentHashMap;

public class PerKindCircuitBreaker {

    private record State(Deque<Instant> failures, Instant openUntil) {}

    private final int threshold;
    private final Duration window;
    private final Duration cooldown;
    private final Clock clock;
    private final ConcurrentHashMap<ConditionKind, State> byKind = new ConcurrentHashMap<>();

    /** Optional metrics — set via {@link #setMetrics} after construction (avoids circular bean deps). */
    private volatile AlertingMetrics metrics;

    /** Production constructor — uses system clock. */
    public PerKindCircuitBreaker(int threshold, int windowSeconds, int cooldownSeconds) {
        this(threshold, windowSeconds, cooldownSeconds, Clock.systemDefaultZone());
    }

    /** Test constructor — allows a fixed/controllable clock. */
    public PerKindCircuitBreaker(int threshold, int windowSeconds, int cooldownSeconds, Clock clock) {
        this.threshold = threshold;
        this.window    = Duration.ofSeconds(windowSeconds);
        this.cooldown  = Duration.ofSeconds(cooldownSeconds);
        this.clock     = clock;
    }

    /** Wire metrics after construction to avoid circular Spring dependency. */
    public void setMetrics(AlertingMetrics metrics) {
        this.metrics = metrics;
    }

    public void recordFailure(ConditionKind kind) {
        final boolean[] justOpened = {false};
        byKind.compute(kind, (k, s) -> {
            Deque<Instant> deque = (s == null) ? new ArrayDeque<>() : new ArrayDeque<>(s.failures());
            Instant now    = Instant.now(clock);
            Instant cutoff = now.minus(window);
            while (!deque.isEmpty() && deque.peekFirst().isBefore(cutoff)) deque.pollFirst();
            deque.addLast(now);
            boolean wasOpen = s != null && s.openUntil() != null && now.isBefore(s.openUntil());
            Instant openUntil = (deque.size() >= threshold) ? now.plus(cooldown) : null;
            if (openUntil != null && !wasOpen) {
                justOpened[0] = true;
            }
            return new State(deque, openUntil);
        });
        if (justOpened[0] && metrics != null) {
            metrics.circuitOpened(kind);
        }
    }

    public boolean isOpen(ConditionKind kind) {
        State s = byKind.get(kind);
        return s != null && s.openUntil() != null && Instant.now(clock).isBefore(s.openUntil());
    }

    public void recordSuccess(ConditionKind kind) {
        byKind.compute(kind, (k, s) -> new State(new ArrayDeque<>(), null));
    }
}
