// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

// Package circuitbreaker implements the circuit breaker pattern for external service calls.
// This prevents cascading failures by temporarily blocking requests to failing services.
package circuitbreaker

import (
	"context"
	"errors"
	"sync"
	"time"

	"code.gitea.io/gitea/modules/log"
)

// State represents the circuit breaker state
type State int

const (
	// StateClosed means the circuit is closed and requests flow through
	StateClosed State = iota

	// StateOpen means the circuit is open and requests are blocked
	StateOpen

	// StateHalfOpen means the circuit is testing if the service has recovered
	StateHalfOpen
)

func (s State) String() string {
	switch s {
	case StateClosed:
		return "closed"
	case StateOpen:
		return "open"
	case StateHalfOpen:
		return "half-open"
	default:
		return "unknown"
	}
}

// Common errors
var (
	ErrCircuitOpen    = errors.New("circuit breaker is open")
	ErrTooManyRequests = errors.New("too many requests in half-open state")
)

// Config configures the circuit breaker behavior
type Config struct {
	// Name identifies this circuit breaker
	Name string

	// MaxFailures is the number of failures before opening the circuit
	MaxFailures int

	// SuccessThreshold is the number of successes needed to close from half-open
	SuccessThreshold int

	// Timeout is how long the circuit stays open before testing
	Timeout time.Duration

	// MaxConcurrentInHalfOpen is the max concurrent requests when half-open
	MaxConcurrentInHalfOpen int

	// OnStateChange is called when the state changes
	OnStateChange func(name string, from, to State)
}

// DefaultConfig returns a sensible default configuration
func DefaultConfig(name string) Config {
	return Config{
		Name:                    name,
		MaxFailures:             5,
		SuccessThreshold:        2,
		Timeout:                 30 * time.Second,
		MaxConcurrentInHalfOpen: 1,
	}
}

// CircuitBreaker implements the circuit breaker pattern
type CircuitBreaker struct {
	mu               sync.RWMutex
	config           Config
	state            State
	failures         int
	successes        int
	lastFailure      time.Time
	lastStateChange  time.Time
	halfOpenRequests int
	generation       uint64 // prevents counting old requests
}

// New creates a new circuit breaker
func New(config Config) *CircuitBreaker {
	if config.MaxFailures <= 0 {
		config.MaxFailures = 5
	}
	if config.SuccessThreshold <= 0 {
		config.SuccessThreshold = 2
	}
	if config.Timeout <= 0 {
		config.Timeout = 30 * time.Second
	}
	if config.MaxConcurrentInHalfOpen <= 0 {
		config.MaxConcurrentInHalfOpen = 1
	}

	return &CircuitBreaker{
		config:          config,
		state:           StateClosed,
		lastStateChange: time.Now(),
	}
}

// Execute runs the given function if the circuit allows
func (cb *CircuitBreaker) Execute(ctx context.Context, fn func(context.Context) error) error {
	generation, err := cb.beforeRequest()
	if err != nil {
		return err
	}

	// Check context
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	// Execute the function
	err = fn(ctx)

	cb.afterRequest(generation, err == nil)
	return err
}

// beforeRequest checks if the request should proceed
func (cb *CircuitBreaker) beforeRequest() (uint64, error) {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	now := time.Now()

	switch cb.state {
	case StateClosed:
		return cb.generation, nil

	case StateOpen:
		// Check if timeout has elapsed
		if now.Sub(cb.lastStateChange) >= cb.config.Timeout {
			cb.toHalfOpen()
			cb.halfOpenRequests++
			return cb.generation, nil
		}
		return 0, ErrCircuitOpen

	case StateHalfOpen:
		if cb.halfOpenRequests >= cb.config.MaxConcurrentInHalfOpen {
			return 0, ErrTooManyRequests
		}
		cb.halfOpenRequests++
		return cb.generation, nil
	}

	return cb.generation, nil
}

// afterRequest processes the result of a request
func (cb *CircuitBreaker) afterRequest(generation uint64, success bool) {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	// Ignore results from old generations
	if generation != cb.generation {
		return
	}

	if success {
		cb.onSuccess()
	} else {
		cb.onFailure()
	}
}

func (cb *CircuitBreaker) onSuccess() {
	switch cb.state {
	case StateClosed:
		cb.failures = 0

	case StateHalfOpen:
		cb.halfOpenRequests--
		cb.successes++
		if cb.successes >= cb.config.SuccessThreshold {
			cb.toClosed()
		}
	}
}

func (cb *CircuitBreaker) onFailure() {
	switch cb.state {
	case StateClosed:
		cb.failures++
		cb.lastFailure = time.Now()
		if cb.failures >= cb.config.MaxFailures {
			cb.toOpen()
		}

	case StateHalfOpen:
		cb.halfOpenRequests--
		cb.toOpen()
	}
}

func (cb *CircuitBreaker) toOpen() {
	if cb.state == StateOpen {
		return
	}

	from := cb.state
	cb.state = StateOpen
	cb.lastStateChange = time.Now()
	cb.generation++

	log.Warn("Circuit breaker %s: %s -> open (failures: %d)", cb.config.Name, from, cb.failures)

	if cb.config.OnStateChange != nil {
		go cb.config.OnStateChange(cb.config.Name, from, StateOpen)
	}
}

func (cb *CircuitBreaker) toClosed() {
	if cb.state == StateClosed {
		return
	}

	from := cb.state
	cb.state = StateClosed
	cb.failures = 0
	cb.successes = 0
	cb.halfOpenRequests = 0
	cb.lastStateChange = time.Now()
	cb.generation++

	log.Info("Circuit breaker %s: %s -> closed", cb.config.Name, from)

	if cb.config.OnStateChange != nil {
		go cb.config.OnStateChange(cb.config.Name, from, StateClosed)
	}
}

func (cb *CircuitBreaker) toHalfOpen() {
	if cb.state == StateHalfOpen {
		return
	}

	from := cb.state
	cb.state = StateHalfOpen
	cb.successes = 0
	cb.halfOpenRequests = 0
	cb.lastStateChange = time.Now()
	cb.generation++

	log.Debug("Circuit breaker %s: %s -> half-open", cb.config.Name, from)

	if cb.config.OnStateChange != nil {
		go cb.config.OnStateChange(cb.config.Name, from, StateHalfOpen)
	}
}

// State returns the current state
func (cb *CircuitBreaker) State() State {
	cb.mu.RLock()
	defer cb.mu.RUnlock()
	return cb.state
}

// Stats returns statistics about the circuit breaker
type Stats struct {
	Name            string    `json:"name"`
	State           string    `json:"state"`
	Failures        int       `json:"failures"`
	Successes       int       `json:"successes"`
	LastFailure     time.Time `json:"last_failure,omitempty"`
	LastStateChange time.Time `json:"last_state_change"`
	Generation      uint64    `json:"generation"`
}

// Stats returns the current statistics
func (cb *CircuitBreaker) Stats() Stats {
	cb.mu.RLock()
	defer cb.mu.RUnlock()

	return Stats{
		Name:            cb.config.Name,
		State:           cb.state.String(),
		Failures:        cb.failures,
		Successes:       cb.successes,
		LastFailure:     cb.lastFailure,
		LastStateChange: cb.lastStateChange,
		Generation:      cb.generation,
	}
}

// Reset forces the circuit breaker back to closed state
func (cb *CircuitBreaker) Reset() {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	cb.state = StateClosed
	cb.failures = 0
	cb.successes = 0
	cb.halfOpenRequests = 0
	cb.lastStateChange = time.Now()
	cb.generation++

	log.Info("Circuit breaker %s: manually reset to closed", cb.config.Name)
}

// Registry manages multiple circuit breakers
type Registry struct {
	mu       sync.RWMutex
	breakers map[string]*CircuitBreaker
}

var (
	defaultRegistry *Registry
	registryOnce    sync.Once
)

// GetRegistry returns the default circuit breaker registry
func GetRegistry() *Registry {
	registryOnce.Do(func() {
		defaultRegistry = &Registry{
			breakers: make(map[string]*CircuitBreaker),
		}
	})
	return defaultRegistry
}

// Get returns a circuit breaker by name, creating it if necessary
func (r *Registry) Get(name string, config ...Config) *CircuitBreaker {
	r.mu.RLock()
	cb, ok := r.breakers[name]
	r.mu.RUnlock()

	if ok {
		return cb
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	// Double-check after acquiring write lock
	if cb, ok := r.breakers[name]; ok {
		return cb
	}

	var cfg Config
	if len(config) > 0 {
		cfg = config[0]
		cfg.Name = name
	} else {
		cfg = DefaultConfig(name)
	}

	cb = New(cfg)
	r.breakers[name] = cb
	return cb
}

// Stats returns stats for all circuit breakers
func (r *Registry) Stats() map[string]Stats {
	r.mu.RLock()
	defer r.mu.RUnlock()

	stats := make(map[string]Stats)
	for name, cb := range r.breakers {
		stats[name] = cb.Stats()
	}
	return stats
}

// ResetAll resets all circuit breakers
func (r *Registry) ResetAll() {
	r.mu.RLock()
	defer r.mu.RUnlock()

	for _, cb := range r.breakers {
		cb.Reset()
	}
}
