// Package circuitbreaker implements a per-provider circuit breaker.
// States: Closed (normal) → Open (failing, rejects requests) → HalfOpen (testing recovery).
// Transition Closed→Open: after `threshold` consecutive failures.
// Transition Open→HalfOpen: after `openTTL` has elapsed.
// Transition HalfOpen→Closed: on the first successful request.
// Transition HalfOpen→Open: on failure during half-open test.
package circuitbreaker

import (
	"sync"
	"time"
)

// State represents the circuit breaker state for a provider.
type State int

const (
	Closed   State = iota // Normal — requests allowed
	Open                  // Tripped — requests rejected
	HalfOpen              // Recovery probe — one request allowed
)

func (s State) String() string {
	switch s {
	case Closed:
		return "closed"
	case Open:
		return "open"
	case HalfOpen:
		return "half_open"
	default:
		return "unknown"
	}
}

// Status is the read-only snapshot returned by the API.
type Status struct {
	Provider string `json:"provider"`
	State    string `json:"state"`
	Failures int    `json:"failures"`
	OpenedAt string `json:"opened_at,omitempty"` // RFC3339, only when Open/HalfOpen
}

type entry struct {
	state    State
	failures int
	openedAt time.Time
	// halfOpenInFlight prevents concurrent requests during HalfOpen probe.
	halfOpenInFlight bool
}

// Breaker is a thread-safe circuit breaker for multiple providers.
type Breaker struct {
	mu        sync.Mutex
	states    map[string]*entry
	threshold int
	openTTL   time.Duration
}

// New creates a Breaker.
//   - threshold: consecutive failures before opening the circuit.
//   - openTTL: how long to wait in Open state before transitioning to HalfOpen.
func New(threshold int, openTTL time.Duration) *Breaker {
	return &Breaker{
		states:    make(map[string]*entry),
		threshold: threshold,
		openTTL:   openTTL,
	}
}

func (b *Breaker) get(provider string) *entry {
	e, ok := b.states[provider]
	if !ok {
		e = &entry{state: Closed}
		b.states[provider] = e
	}
	return e
}

// Allow returns true if a request to the given provider should proceed.
// It also handles the Open→HalfOpen transition when the TTL has expired.
func (b *Breaker) Allow(provider string) bool {
	b.mu.Lock()
	defer b.mu.Unlock()

	e := b.get(provider)

	switch e.state {
	case Closed:
		return true

	case Open:
		if time.Since(e.openedAt) >= b.openTTL {
			// Transition to HalfOpen — allow exactly one probe.
			if !e.halfOpenInFlight {
				e.state = HalfOpen
				e.halfOpenInFlight = true
				return true
			}
		}
		return false

	case HalfOpen:
		// Only one in-flight request allowed during HalfOpen.
		if !e.halfOpenInFlight {
			e.halfOpenInFlight = true
			return true
		}
		return false
	}

	return true
}

// Success records a successful response from a provider.
// Any non-Open circuit resets the failure counter; HalfOpen transitions to Closed.
func (b *Breaker) Success(provider string) {
	b.mu.Lock()
	defer b.mu.Unlock()

	e := b.get(provider)
	e.failures = 0
	e.state = Closed
	e.halfOpenInFlight = false
}

// Failure records a failed response from a provider.
// If threshold is reached the circuit transitions to Open.
// A failure during HalfOpen re-opens the circuit immediately.
func (b *Breaker) Failure(provider string) {
	b.mu.Lock()
	defer b.mu.Unlock()

	e := b.get(provider)
	e.halfOpenInFlight = false

	switch e.state {
	case Closed:
		e.failures++
		if e.failures >= b.threshold {
			e.state = Open
			e.openedAt = time.Now()
		}
	case HalfOpen:
		// Re-open immediately.
		e.state = Open
		e.openedAt = time.Now()
		e.failures++
	}
}

// Status returns a read-only snapshot of the circuit state for a provider.
func (b *Breaker) Status(provider string) Status {
	b.mu.Lock()
	defer b.mu.Unlock()

	e := b.get(provider)
	s := Status{
		Provider: provider,
		State:    e.state.String(),
		Failures: e.failures,
	}
	if e.state == Open || e.state == HalfOpen {
		s.OpenedAt = e.openedAt.Format(time.RFC3339)
	}
	return s
}

// Statuses returns snapshots for all known providers.
func (b *Breaker) Statuses() []Status {
	b.mu.Lock()
	defer b.mu.Unlock()

	out := make([]Status, 0, len(b.states))
	for name, e := range b.states {
		s := Status{
			Provider: name,
			State:    e.state.String(),
			Failures: e.failures,
		}
		if e.state == Open || e.state == HalfOpen {
			s.OpenedAt = e.openedAt.Format(time.RFC3339)
		}
		out = append(out, s)
	}
	return out
}
