package daemon

import (
	"errors"
	"log/slog"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
)

const (
	// A review page open fires a burst of concurrent read RPCs (snapshot,
	// fingerprint, files, per-file diffs) plus workspace traffic; 3 slots
	// saturated on cold load and turned overflow into "Desktop is busy" 503s.
	// 8 gives headroom for the burst without unbounded connection growth.
	defaultElasticRequestLinkMaxPerMachine     = 8
	defaultElasticRequestLinkHardMaxPerMachine = 12
	defaultElasticRequestLinkConnectTimeout    = 5 * time.Second
	defaultElasticRequestLinkIdleTimeout       = 30 * time.Second
	defaultElasticRequestLinkMaxAge            = 5 * time.Minute
	defaultElasticRequestLinkCleanupInterval   = time.Second
	defaultElasticRequestLinkProtocolVersion   = 1
	defaultWarmPoolActiveTTL                   = 75 * time.Second
	defaultWarmPoolTargetIdleFloor             = 1
	defaultWarmPoolMaxWarmLinks                = 2
	defaultWarmPoolCooldownBase                = 5 * time.Second
	defaultWarmPoolCooldownMax                 = time.Minute
)

type ElasticRequestLinkManagerConfig struct {
	NodeID                  string
	TokenSecret             []byte
	MaxPerMachine           int
	HardMaxPerMachine       int
	ConnectTimeout          time.Duration
	IdleTimeout             time.Duration
	MaxAge                  time.Duration
	CleanupInterval         time.Duration
	ProtocolVersion         int
	EndpointRegistryVersion string
	EmergencyDisabled       bool
	WarmActiveTTL           time.Duration
	WarmTargetIdleFloor     int
	WarmMaxLinks            int
	WarmCooldownBase        time.Duration
	WarmCooldownMax         time.Duration
	WarmOpenSender          func(client *clientState, open ElasticRequestLinkOpenCommand) error
	Now                     func() time.Time
	NewID                   func(prefix string) string
	Metrics                 DaemonTransportMetrics
}

type DaemonTransportMetrics interface {
	RecordRemoteRequest(endpoint string, linkMode string, outcome string, fallback string, duration time.Duration)
	RecordRouteDecision(endpoint string, linkPolicy string, mode string, reason string)
	RecordElasticEvent(event string, reason string)
	RecordWarmEvent(origin string, profile string, event string, reason string)
	SetElasticPool(state string, value float64)
}

type elasticRequestLinkSender interface {
	SendJSON(message any) error
	Close() error
}

type websocketElasticRequestLinkSender struct {
	conn websocketConn
}

// websocketConn is the small subset of gorilla/websocket.Conn used by the
// Elastic Request Link manager. The alias keeps tests independent from the
// concrete carrier while the production route still passes the WebSocket.
type websocketConn interface {
	WriteJSON(v any) error
	WriteControl(messageType int, data []byte, deadline time.Time) error
	Close() error
}

func (sender websocketElasticRequestLinkSender) SendJSON(message any) error {
	if sender.conn == nil {
		return errors.New("nil elastic request link carrier")
	}
	return sender.conn.WriteJSON(message)
}

func (sender websocketElasticRequestLinkSender) Close() error {
	if sender.conn == nil {
		return nil
	}
	return sender.conn.Close()
}

type ElasticRequestLinkManager struct {
	mu       sync.Mutex
	logger   *slog.Logger
	config   ElasticRequestLinkManagerConfig
	registry *RemoteEndpointRegistry

	grants          map[string]*elasticRequestLinkGrantState
	links           map[string]*elasticRequestLinkState
	linksByMachine  map[string]map[string]struct{}
	assignmentsByID map[string]*elasticRequestAssignmentState
	warm            *WarmPoolController
}

func NewElasticRequestLinkManager(config ElasticRequestLinkManagerConfig, registry *RemoteEndpointRegistry, logger *slog.Logger) *ElasticRequestLinkManager {
	config = normalizeElasticRequestLinkManagerConfig(config)
	if registry == nil {
		registry = defaultRemoteEndpointRegistry()
	}
	if logger == nil {
		logger = slog.Default()
	}
	return &ElasticRequestLinkManager{
		logger:          logger,
		config:          config,
		registry:        registry,
		grants:          make(map[string]*elasticRequestLinkGrantState),
		links:           make(map[string]*elasticRequestLinkState),
		linksByMachine:  make(map[string]map[string]struct{}),
		assignmentsByID: make(map[string]*elasticRequestAssignmentState),
		warm:            newWarmPoolController(),
	}
}

func normalizeElasticRequestLinkManagerConfig(config ElasticRequestLinkManagerConfig) ElasticRequestLinkManagerConfig {
	config.NodeID = strings.TrimSpace(config.NodeID)
	if config.NodeID == "" {
		config.NodeID = "local"
	}
	if len(config.TokenSecret) == 0 {
		config.TokenSecret = randomTokenSecret()
	}
	if config.MaxPerMachine <= 0 {
		config.MaxPerMachine = defaultElasticRequestLinkMaxPerMachine
	}
	if config.HardMaxPerMachine <= 0 {
		config.HardMaxPerMachine = defaultElasticRequestLinkHardMaxPerMachine
	}
	if config.HardMaxPerMachine < config.MaxPerMachine {
		config.HardMaxPerMachine = config.MaxPerMachine
	}
	if config.MaxPerMachine > config.HardMaxPerMachine {
		config.MaxPerMachine = config.HardMaxPerMachine
	}
	if config.ConnectTimeout <= 0 {
		config.ConnectTimeout = defaultElasticRequestLinkConnectTimeout
	}
	if config.IdleTimeout <= 0 {
		config.IdleTimeout = defaultElasticRequestLinkIdleTimeout
	}
	if config.MaxAge <= 0 {
		config.MaxAge = defaultElasticRequestLinkMaxAge
	}
	if config.CleanupInterval <= 0 {
		config.CleanupInterval = defaultElasticRequestLinkCleanupInterval
	}
	if config.WarmActiveTTL <= 0 {
		config.WarmActiveTTL = defaultWarmPoolActiveTTL
	}
	if config.WarmTargetIdleFloor <= 0 {
		config.WarmTargetIdleFloor = defaultWarmPoolTargetIdleFloor
	}
	if config.WarmMaxLinks <= 0 {
		config.WarmMaxLinks = defaultWarmPoolMaxWarmLinks
	}
	if config.WarmCooldownBase <= 0 {
		config.WarmCooldownBase = defaultWarmPoolCooldownBase
	}
	if config.WarmCooldownMax <= 0 {
		config.WarmCooldownMax = defaultWarmPoolCooldownMax
	}
	if config.WarmCooldownMax < config.WarmCooldownBase {
		config.WarmCooldownMax = config.WarmCooldownBase
	}
	if config.ProtocolVersion <= 0 {
		config.ProtocolVersion = defaultElasticRequestLinkProtocolVersion
	}
	config.EndpointRegistryVersion = strings.TrimSpace(config.EndpointRegistryVersion)
	if config.EndpointRegistryVersion == "" {
		config.EndpointRegistryVersion = defaultEndpointRegistryVersion
	}
	if config.Now == nil {
		config.Now = time.Now
	}
	if config.NewID == nil {
		config.NewID = func(prefix string) string { return strings.TrimSpace(prefix) + uuid.NewString() }
	}
	return config
}
