package node

import (
	"context"
	"errors"
	"sync"
)

// ErrConcurrencyLimitExceeded is returned when a run is rejected because
// admission could not be granted under the current concurrency policy.
var ErrConcurrencyLimitExceeded = errors.New("concurrency limit exceeded")

type runHandle struct {
	runID     string
	adapter   string
	target    string
	sessionID string
	cancel    context.CancelFunc
	done      chan struct{}
}

type runManager struct {
	mu   sync.RWMutex
	runs map[string]*runHandle
}

func newRunManager() *runManager {
	return &runManager{runs: make(map[string]*runHandle)}
}

func (m *runManager) register(h *runHandle) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.runs[h.runID] = h
}

func (m *runManager) deregister(runID string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.runs, runID)
}

func (m *runManager) cancelRun(runID string) bool {
	m.mu.RLock()
	h, ok := m.runs[runID]
	m.mu.RUnlock()
	if !ok {
		return false
	}
	h.cancel()
	return true
}

func (m *runManager) hasActiveRunsForAdapter(adapterKey string) bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	for _, r := range m.runs {
		if r.adapter == adapterKey {
			return true
		}
	}
	return false
}

func (m *runManager) hasAnyActiveRuns() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return len(m.runs) > 0
}

// snapshotActive returns a copy of all currently active run handles so callers
// can wait for pre-swap runs to drain without holding the manager lock.
func (m *runManager) snapshotActive() []*runHandle {
	m.mu.RLock()
	defer m.mu.RUnlock()
	out := make([]*runHandle, 0, len(m.runs))
	for _, h := range m.runs {
		out = append(out, h)
	}
	return out
}

// waitHandles blocks until every handle's run has finished or ctx is cancelled.
func waitHandles(ctx context.Context, handles []*runHandle) {
	for _, h := range handles {
		select {
		case <-h.done:
		case <-ctx.Done():
			return
		}
	}
}

// rejectReason classifies why a run could not be admitted.
type rejectReason string

const (
	// reasonConcurrencyUnavailable means admission failed because the concurrency limit was exceeded.
	reasonConcurrencyUnavailable rejectReason = "concurrency_unavailable"
)

// admissionError carries the concrete rejection reason while still satisfying
// errors.Is(err, ErrConcurrencyLimitExceeded) for callers relying on the legacy sentinel.
type admissionError struct {
	reason rejectReason
	msg    string
}

func (e *admissionError) Error() string { return e.msg }

func (e *admissionError) Is(target error) bool { return target == ErrConcurrencyLimitExceeded }

func newConcurrencyUnavailableError() *admissionError {
	return &admissionError{
		reason: reasonConcurrencyUnavailable,
		msg:    "concurrency unavailable: admission rejected because safety limit exceeded",
	}
}

// admissionRejectReason extracts the rejectReason from an admission error.
func admissionRejectReason(err error) rejectReason {
	var ae *admissionError
	if errors.As(err, &ae) {
		return ae.reason
	}
	return reasonConcurrencyUnavailable
}

// admissionMessage extracts the human-readable rejection message.
func admissionMessage(err error) string {
	var ae *admissionError
	if errors.As(err, &ae) {
		return ae.msg
	}
	if err != nil {
		return err.Error()
	}
	return string(reasonConcurrencyUnavailable)
}

// fifoGate is a simple in-flight concurrency semaphore (no queue).
type fifoGate struct {
	mu       sync.Mutex
	capacity int
	inFlight int
}

func newFifoGate(capacity int) *fifoGate {
	return &fifoGate{
		capacity: capacity,
	}
}

func (g *fifoGate) acquire() bool {
	g.mu.Lock()
	defer g.mu.Unlock()
	if g.capacity <= 0 || g.inFlight < g.capacity {
		g.inFlight++
		return true
	}
	return false
}

func (g *fifoGate) release() {
	g.mu.Lock()
	defer g.mu.Unlock()
	if g.inFlight > 0 {
		g.inFlight--
	}
}

func (g *fifoGate) activeCount() int {
	g.mu.Lock()
	defer g.mu.Unlock()
	return g.inFlight
}

func (g *fifoGate) updateCapacity(capacity int) {
	g.mu.Lock()
	defer g.mu.Unlock()
	g.capacity = capacity
}

// currentCapacity returns the current concurrency capacity (0 means unlimited).
func (g *fifoGate) currentCapacity() int {
	g.mu.Lock()
	defer g.mu.Unlock()
	return g.capacity
}

func (g *fifoGate) queuedCount() int {
	return 0
}

// admissionManager tracks the optional gates held by a run admission.
type admissionManager struct {
	global  *fifoGate
	adapter *fifoGate
}

type admissionTicket struct {
	mgr         *admissionManager
	adapterHeld bool
	globalHeld  bool
}

func (a *admissionManager) acquire() (*admissionTicket, error) {
	if a.adapter != nil {
		if !a.adapter.acquire() {
			return nil, newConcurrencyUnavailableError()
		}
	}
	if a.global != nil {
		if !a.global.acquire() {
			if a.adapter != nil {
				a.adapter.release()
			}
			return nil, newConcurrencyUnavailableError()
		}
	}
	return &admissionTicket{
		mgr:         a,
		adapterHeld: a.adapter != nil,
		globalHeld:  a.global != nil,
	}, nil
}

func (t *admissionTicket) release() {
	if t.globalHeld && t.mgr.global != nil {
		t.mgr.global.release()
		t.globalHeld = false
	}
	if t.adapterHeld && t.mgr.adapter != nil {
		t.mgr.adapter.release()
		t.adapterHeld = false
	}
}
