"""Thread-safe per-data-source circuit breaker."""

import threading
import time
from contextlib import contextmanager

from app.core.data_source.errors import DataSourceCircuitOpen


class CircuitBreaker:
    def __init__(
        self,
        *,
        failure_threshold=3,
        recovery_seconds=30,
        clock=None,
    ):
        self.failure_threshold = int(failure_threshold)
        self.recovery_seconds = float(recovery_seconds)
        if self.failure_threshold < 1 or self.recovery_seconds <= 0:
            raise ValueError("invalid circuit breaker settings")
        self._clock = clock or time.monotonic
        self._lock = threading.Lock()
        self._state = "closed"
        self._consecutive_failures = 0
        self._open_until = 0.0
        self._half_open_in_flight = False

    @property
    def state(self):
        with self._lock:
            return self._state

    @property
    def consecutive_failures(self):
        with self._lock:
            return self._consecutive_failures

    @property
    def open_until(self):
        with self._lock:
            return self._open_until

    def record_success(self):
        with self._lock:
            self._state = "closed"
            self._consecutive_failures = 0
            self._open_until = 0.0
            self._half_open_in_flight = False

    def record_failure(self):
        with self._lock:
            self._consecutive_failures += 1
            if (
                self._state == "half_open"
                or self._consecutive_failures >= self.failure_threshold
            ):
                self._state = "open"
                self._open_until = self._clock() + self.recovery_seconds
                self._half_open_in_flight = False

    @contextmanager
    def probe_permission(self):
        half_open_owner = False
        with self._lock:
            now = self._clock()
            if self._state == "open":
                if now < self._open_until:
                    raise DataSourceCircuitOpen()
                if self._half_open_in_flight:
                    raise DataSourceCircuitOpen()
                self._state = "half_open"
                self._half_open_in_flight = True
                half_open_owner = True
            elif self._state == "half_open":
                raise DataSourceCircuitOpen()

        try:
            yield
        except Exception:
            if half_open_owner:
                self.record_failure()
            raise
        finally:
            if half_open_owner:
                with self._lock:
                    if self._state == "half_open":
                        self._state = "open"
                        self._open_until = (
                            self._clock() + self.recovery_seconds
                        )
                        self._half_open_in_flight = False

    def snapshot(self):
        with self._lock:
            return {
                "state": self._state,
                "consecutive_failures": self._consecutive_failures,
                "open_until": self._open_until,
                "half_open_in_flight": self._half_open_in_flight,
            }
