package profile

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"
	"sync"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

var (
	// ErrServerRuntimePreparing is returned when the daemon has explicitly
	// entered runtime mode but has not registered its runtime participant yet.
	// Mutations must fail closed in this window instead of silently behaving
	// like offline file edits.
	ErrServerRuntimePreparing = errors.New("server runtime participant is not active")

	// ErrOnlineServerSnapshotUpdate prevents callers from publishing a complete
	// server snapshot while the daemon is running. Online callers must use
	// field-level patches so a stale read cannot overwrite a concurrent identity
	// transition.
	ErrOnlineServerSnapshotUpdate = errors.New("full server snapshot updates are unavailable while the daemon is running")

	// ErrServerRuntimeHalted means a previous identity transaction could not
	// restore a trustworthy persisted/live pair. The server remains quarantined
	// until an explicit repair transaction succeeds.
	ErrServerRuntimeHalted = errors.New("server runtime is halted pending repair")

	// ErrServerRuntimeRepairUnsupported prevents a quarantined server from
	// reopening when the wired connection manager cannot perform residual
	// runtime cleanup and verified replacement.
	ErrServerRuntimeRepairUnsupported = errors.New("server runtime repair is unsupported")

	// ErrServerRuntimeNotHalted prevents the repair endpoint from becoming a
	// general-purpose restart operation.
	ErrServerRuntimeNotHalted = errors.New("server runtime is not halted")
)

// ServerLifecycleMode makes the offline-file and daemon-runtime mutation modes
// explicit. A missing participant is never interpreted as permission to
// downgrade an online mutation to an offline write.
type ServerLifecycleMode string

const (
	ServerLifecycleOffline   ServerLifecycleMode = "offline"
	ServerLifecyclePreparing ServerLifecycleMode = "runtime_preparing"
	ServerLifecycleActive    ServerLifecycleMode = "runtime_active"
)

// ServerTransition fences gateway work while one server's persisted and live
// identity changes. Complete reopens the server after a successful mutation or
// successful rollback. FailClosed keeps it unavailable after rollback failure.
type ServerTransition interface {
	Complete()
	FailClosed()
}

// ServerRuntimeParticipant is implemented by the daemon composition root. It
// deliberately exposes only the runtime side effects that the profile-owned
// mutation transaction needs.
type ServerRuntimeParticipant interface {
	BeginServerTransition(serverID string) ServerTransition
	ReconcileServer(ctx context.Context, previous, current *AgentTeamServer) error
	RepairServer(ctx context.Context, current AgentTeamServer) error
	InvalidateServer(serverID string)
}

// ServerUpdate is an online-safe, field-level server mutation. Nil fields are
// preserved from the current value read under the per-server transaction lock.
type ServerUpdate struct {
	Enabled          *bool
	Name             *string
	Description      *string
	AgentID          *string
	ServerURL        *string
	WorkspaceID      *string
	MachineID        *string
	APIKey           *string
	AuthToken        *string
	ConnectionStatus *string
	Config           *types.RawMessage
}

// ReplaceServer is the explicit offline full-snapshot mutation used by file
// tooling. It fails closed in daemon lifecycle modes; online callers must use
// Patch.
func (sm *ServerManager) ReplaceServer(server AgentTeamServer) error {
	if sm.LifecycleMode() != ServerLifecycleOffline {
		return ErrOnlineServerSnapshotUpdate
	}
	return sm.Update(server)
}

type serverRuntimeControl struct {
	mu          sync.RWMutex
	mode        ServerLifecycleMode
	participant ServerRuntimeParticipant
	stateMu     sync.RWMutex
	halted      map[string]struct{}
	transition  map[string]struct{}
}

// ErrExternalServerIdentityChange is returned when an active daemon observes
// a servers.yaml edit that would change runtime/gateway identity. Such edits
// must go through the daemon API so they can participate in fencing and
// rollback.
var ErrExternalServerIdentityChange = errors.New("external server identity changes require the daemon API")

func newServerRuntimeControl() *serverRuntimeControl {
	return &serverRuntimeControl{
		mode:       ServerLifecycleOffline,
		halted:     make(map[string]struct{}),
		transition: make(map[string]struct{}),
	}
}

// PrepareRuntimeMode explicitly closes the offline mutation path before daemon
// runtime wiring starts.
func (sm *ServerManager) PrepareRuntimeMode() error {
	sm.runtime.mu.Lock()
	defer sm.runtime.mu.Unlock()
	if sm.runtime.mode != ServerLifecycleOffline {
		return fmt.Errorf("prepare server runtime from %s", sm.runtime.mode)
	}
	sm.runtime.mode = ServerLifecyclePreparing
	sm.runtime.participant = nil
	return nil
}

// ActivateRuntimeMode installs the daemon runtime participant. PrepareRuntimeMode
// must have been called first so startup cannot contain an implicit offline
// fallback window.
func (sm *ServerManager) ActivateRuntimeMode(participant ServerRuntimeParticipant) error {
	if participant == nil {
		return ErrServerRuntimePreparing
	}
	sm.runtime.mu.Lock()
	defer sm.runtime.mu.Unlock()
	if sm.runtime.mode != ServerLifecyclePreparing {
		return fmt.Errorf("activate server runtime from %s", sm.runtime.mode)
	}
	sm.runtime.participant = participant
	sm.runtime.mode = ServerLifecycleActive
	return nil
}

// SuspendRuntimeMode closes online mutations during daemon shutdown while
// retaining an explicit non-offline lifecycle state. It waits for in-flight
// transactions before returning.
func (sm *ServerManager) SuspendRuntimeMode() error {
	sm.runtime.mu.Lock()
	defer sm.runtime.mu.Unlock()
	if sm.runtime.mode != ServerLifecycleActive {
		return fmt.Errorf("suspend server runtime from %s", sm.runtime.mode)
	}
	sm.runtime.mode = ServerLifecyclePreparing
	sm.runtime.participant = nil
	return nil
}

// DeactivateRuntimeMode returns the manager to explicit offline operation.
// Taking the write lock waits for in-flight runtime transactions to finish.
func (sm *ServerManager) DeactivateRuntimeMode() {
	sm.runtime.mu.Lock()
	sm.runtime.participant = nil
	sm.runtime.mode = ServerLifecycleOffline
	sm.runtime.mu.Unlock()
	sm.runtime.stateMu.Lock()
	clear(sm.runtime.halted)
	clear(sm.runtime.transition)
	sm.runtime.stateMu.Unlock()
}

// ServerAvailableForSelection reports whether a persisted server is enabled
// and its daemon runtime is not quarantined or inside an identity transition.
func (sm *ServerManager) ServerAvailableForSelection(id string) bool {
	id = strings.TrimSpace(id)
	sm.runtime.mu.RLock()
	if sm.runtime.mode == ServerLifecyclePreparing {
		sm.runtime.mu.RUnlock()
		return false
	}
	sm.runtime.mu.RUnlock()
	sm.runtime.stateMu.RLock()
	defer sm.runtime.stateMu.RUnlock()
	_, halted := sm.runtime.halted[id]
	_, transitioning := sm.runtime.transition[id]
	return !halted && !transitioning
}

// ServerRuntimeHalted reports whether rollback failure quarantined one server.
func (sm *ServerManager) ServerRuntimeHalted(id string) bool {
	sm.runtime.stateMu.RLock()
	defer sm.runtime.stateMu.RUnlock()
	_, halted := sm.runtime.halted[strings.TrimSpace(id)]
	return halted
}

func (sm *ServerManager) markServerRuntimeHalted(id string) {
	sm.runtime.stateMu.Lock()
	sm.runtime.halted[strings.TrimSpace(id)] = struct{}{}
	sm.runtime.stateMu.Unlock()
}

func (sm *ServerManager) clearServerRuntimeHalted(id string) {
	sm.runtime.stateMu.Lock()
	delete(sm.runtime.halted, strings.TrimSpace(id))
	sm.runtime.stateMu.Unlock()
}

func (sm *ServerManager) markServerRuntimeTransitioning(id string) {
	sm.runtime.stateMu.Lock()
	sm.runtime.transition[strings.TrimSpace(id)] = struct{}{}
	sm.runtime.stateMu.Unlock()
}

func (sm *ServerManager) clearServerRuntimeTransitioning(id string) {
	sm.runtime.stateMu.Lock()
	delete(sm.runtime.transition, strings.TrimSpace(id))
	sm.runtime.stateMu.Unlock()
}

// LifecycleMode returns the current explicit server mutation mode.
func (sm *ServerManager) LifecycleMode() ServerLifecycleMode {
	sm.runtime.mu.RLock()
	defer sm.runtime.mu.RUnlock()
	return sm.runtime.mode
}

type runtimeMutationContext struct {
	mode        ServerLifecycleMode
	participant ServerRuntimeParticipant
	release     func()
}

func (sm *ServerManager) mutationContext() (runtimeMutationContext, error) {
	sm.runtime.mu.RLock()
	switch sm.runtime.mode {
	case ServerLifecycleOffline:
		return runtimeMutationContext{
			mode:    ServerLifecycleOffline,
			release: sm.runtime.mu.RUnlock,
		}, nil
	case ServerLifecycleActive:
		if sm.runtime.participant == nil {
			sm.runtime.mu.RUnlock()
			return runtimeMutationContext{}, ErrServerRuntimePreparing
		}
		return runtimeMutationContext{
			mode:        ServerLifecycleActive,
			participant: sm.runtime.participant,
			release:     sm.runtime.mu.RUnlock,
		}, nil
	default:
		sm.runtime.mu.RUnlock()
		return runtimeMutationContext{}, ErrServerRuntimePreparing
	}
}

func (sm *ServerManager) lockServer(id string) func() {
	id = strings.TrimSpace(id)
	sm.locksMu.Lock()
	if sm.locks == nil {
		sm.locks = make(map[string]*sync.Mutex)
	}
	lock := sm.locks[id]
	if lock == nil {
		lock = &sync.Mutex{}
		sm.locks[id] = lock
	}
	sm.locksMu.Unlock()
	lock.Lock()
	return lock.Unlock
}

func (sm *ServerManager) lockServers(ids []string) func() {
	normalized := make([]string, 0, len(ids))
	seen := make(map[string]struct{}, len(ids))
	for _, id := range ids {
		id = strings.TrimSpace(id)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		normalized = append(normalized, id)
	}
	sort.Strings(normalized)
	unlocks := make([]func(), 0, len(normalized))
	for _, id := range normalized {
		unlocks = append(unlocks, sm.lockServer(id))
	}
	return func() {
		for index := len(unlocks) - 1; index >= 0; index-- {
			unlocks[index]()
		}
	}
}

func applyServerUpdate(current AgentTeamServer, update ServerUpdate) AgentTeamServer {
	next := current
	if update.Enabled != nil {
		next.Enabled = *update.Enabled
	}
	if update.Name != nil {
		next.Name = *update.Name
	}
	if update.Description != nil {
		next.Description = *update.Description
	}
	if update.AgentID != nil {
		next.AgentID = *update.AgentID
	}
	if update.ServerURL != nil {
		next.ServerURL = *update.ServerURL
	}
	if update.WorkspaceID != nil {
		next.WorkspaceID = *update.WorkspaceID
	}
	if update.MachineID != nil {
		next.MachineID = *update.MachineID
	}
	if update.APIKey != nil {
		next.APIKey = *update.APIKey
	}
	if update.AuthToken != nil {
		next.AuthToken = *update.AuthToken
	}
	if update.ConnectionStatus != nil {
		next.ConnectionStatus = *update.ConnectionStatus
	}
	if update.Config != nil {
		next.Config = append(types.RawMessage(nil), (*update.Config)...)
	}
	return next
}

// ServerGatewayIdentityChanged reports whether the fields that select or
// authenticate a gateway upstream changed. Display metadata, enabled state,
// and local connection status are intentionally excluded.
func ServerGatewayIdentityChanged(previous, current AgentTeamServer) bool {
	return strings.TrimSpace(previous.ServerURL) != strings.TrimSpace(current.ServerURL) ||
		strings.TrimSpace(previous.WorkspaceID) != strings.TrimSpace(current.WorkspaceID) ||
		strings.TrimSpace(previous.MachineID) != strings.TrimSpace(current.MachineID) ||
		strings.TrimSpace(previous.APIKey) != strings.TrimSpace(current.APIKey) ||
		strings.TrimSpace(previous.AuthToken) != strings.TrimSpace(current.AuthToken) ||
		strings.TrimSpace(previous.AgentID) != strings.TrimSpace(current.AgentID)
}

func serverRuntimeChanged(previous, current *AgentTeamServer) bool {
	switch {
	case previous == nil:
		return current != nil && current.Enabled
	case current == nil:
		return previous.Enabled
	case previous.Enabled != current.Enabled:
		return true
	case !current.Enabled:
		return false
	default:
		return ServerGatewayIdentityChanged(*previous, *current)
	}
}

func cloneAgentTeamServerPointer(server *AgentTeamServer) *AgentTeamServer {
	if server == nil {
		return nil
	}
	cloned := *server
	cloned.Config = append(types.RawMessage(nil), server.Config...)
	return &cloned
}

func serverMapByID(servers []AgentTeamServer) map[string]AgentTeamServer {
	result := make(map[string]AgentTeamServer, len(servers))
	for _, server := range servers {
		result[strings.TrimSpace(server.ID)] = server
	}
	return result
}

func serverIDsForReload(current, candidate []AgentTeamServer) []string {
	ids := make([]string, 0, len(current)+len(candidate))
	for _, server := range current {
		ids = append(ids, server.ID)
	}
	for _, server := range candidate {
		ids = append(ids, server.ID)
	}
	return ids
}

func sameServerIDSet(left, right []AgentTeamServer) bool {
	leftByID := serverMapByID(left)
	rightByID := serverMapByID(right)
	if len(leftByID) != len(rightByID) {
		return false
	}
	for id := range leftByID {
		if _, ok := rightByID[id]; !ok {
			return false
		}
	}
	return true
}

func externalServerIdentityChanged(current, candidate []AgentTeamServer) bool {
	currentByID := serverMapByID(current)
	candidateByID := serverMapByID(candidate)
	if len(currentByID) != len(candidateByID) {
		return true
	}
	for id, previous := range currentByID {
		next, ok := candidateByID[id]
		if !ok || previous.Enabled != next.Enabled ||
			ServerGatewayIdentityChanged(previous, next) {
			return true
		}
	}
	return false
}
