// Package management provides unified server lifecycle and diagnostic operations.
// It consolidates duplicate logic from CLI, REST, and MCP interfaces into a single service layer.
package management

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"github.com/smart-mcp-proxy/mcpproxy-go/internal/storage"
	"go.uber.org/zap"

	"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/contracts"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/reqcontext"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/secret"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/upstream/core"
)

// BulkOperationResult holds the results of a bulk operation across multiple servers.
type BulkOperationResult struct {
	Total      int               `json:"total"`      // Total servers processed
	Successful int               `json:"successful"` // Number of successful operations
	Failed     int               `json:"failed"`     // Number of failed operations
	Errors     map[string]string `json:"errors"`     // Map of server name to error message
}

// Service defines the management interface for all server lifecycle and diagnostic operations.
// All CLI commands, REST endpoints, and MCP tools delegate to this service.
type Service interface {
	// Server Lifecycle Operations

	// ListServers returns all configured servers with their current status and aggregate statistics.
	// This method respects configuration gates but never blocks read operations.
	ListServers(ctx context.Context) ([]*contracts.Server, *contracts.ServerStats, error)

	// GetServerLogs retrieves recent log entries for a specific server.
	// The tail parameter controls how many recent entries to return.
	// Returns empty slice if server doesn't exist or has no logs.
	GetServerLogs(ctx context.Context, name string, tail int) ([]contracts.LogEntry, error)

	// EnableServer enables or disables a specific upstream server.
	// This operation respects disable_management and read_only configuration gates.
	// Emits "servers.changed" event on successful state change.
	EnableServer(ctx context.Context, name string, enabled bool) error

	// RestartServer stops and restarts the connection to a specific upstream server.
	// This operation respects disable_management and read_only configuration gates.
	// Emits "servers.changed" event on successful restart.
	RestartServer(ctx context.Context, name string) error

	// Bulk Operations

	// RestartAll restarts all configured servers sequentially.
	// Returns detailed results including success/failure counts and per-server errors.
	// Continues on partial failures, collecting all errors in the result.
	// This operation respects disable_management and read_only configuration gates.
	RestartAll(ctx context.Context) (*BulkOperationResult, error)

	// EnableAll enables all configured servers.
	// Returns detailed results including success/failure counts and per-server errors.
	// This operation respects disable_management and read_only configuration gates.
	EnableAll(ctx context.Context) (*BulkOperationResult, error)

	// DisableAll disables all configured servers.
	// Returns detailed results including success/failure counts and per-server errors.
	// This operation respects disable_management and read_only configuration gates.
	DisableAll(ctx context.Context) (*BulkOperationResult, error)

	// Diagnostics Operations

	// Doctor aggregates health diagnostics from all system components.
	// Returns comprehensive health information including:
	// - Upstream server connection errors
	// - OAuth authentication requirements
	// - Missing secrets referenced in configuration
	// - Docker daemon status (if isolation is enabled)
	// - General runtime warnings
	// Target completion time: <3 seconds for 20 servers.
	Doctor(ctx context.Context) (*contracts.Diagnostics, error)

	// AuthStatus returns detailed OAuth authentication status for a specific server.
	// Returns nil if server doesn't use OAuth or doesn't exist.
	AuthStatus(ctx context.Context, name string) (*contracts.AuthStatus, error)

	// Server Tool Operations

	// GetServerTools retrieves all available tools for a specific upstream MCP server.
	// Delegates to runtime's GetServerTools() which reads from StateView cache.
	// This is a read-only operation that completes in <10ms (in-memory cache read).
	// Returns empty array if server has no tools.
	// Returns error if server name is empty, server not found, or server not connected.
	GetServerTools(ctx context.Context, name string) ([]map[string]interface{}, error)

	// TriggerOAuthLogin initiates an OAuth 2.x authentication flow for a specific server.
	// Delegates to upstream manager's StartManualOAuth() which launches browser-based flow.
	// This operation respects disable_management and read_only configuration gates.
	// Emits "servers.changed" event on successful OAuth completion.
	// Method returns immediately after starting OAuth flow (actual completion is asynchronous).
	// Returns error if server name is empty, server not found, config gates block operation,
	// or server doesn't support OAuth.
	TriggerOAuthLogin(ctx context.Context, name string) error

	// TriggerOAuthLoginQuick initiates OAuth 2.x authentication flow and returns browser status immediately.
	// Unlike TriggerOAuthLogin which runs fully async, this returns actual browser_opened status.
	// Used by HTTP handler to return accurate OAuthStartResponse (Spec 020 fix).
	TriggerOAuthLoginQuick(ctx context.Context, name string) (*core.OAuthStartResult, error)

	// TriggerOAuthLogout clears OAuth token and disconnects a specific server.
	// This operation respects disable_management and read_only configuration gates.
	// Emits "servers.changed" event on successful logout.
	// Returns error if server name is empty, server not found, config gates block operation,
	// or server doesn't support OAuth.
	TriggerOAuthLogout(ctx context.Context, name string) error

	// LogoutAllOAuth clears OAuth tokens for all OAuth-enabled servers.
	// Returns BulkOperationResult with success/failure counts.
	// This operation respects disable_management and read_only configuration gates.
	LogoutAllOAuth(ctx context.Context) (*BulkOperationResult, error)

	// SetScanSummaryEnricher wires the SecurityScanEnricher used by
	// ListServers to populate Server.SecurityScan. The scanner is constructed
	// later in the boot sequence than the management service, so this is a
	// setter rather than a constructor parameter. Optional — callers without
	// a scanner can skip the call and ListServers will return SecurityScan=nil.
	SetScanSummaryEnricher(e SecurityScanEnricher)
}

// EventEmitter defines the interface for emitting runtime events.
// This is used by the service to notify subscribers of state changes.
type EventEmitter interface {
	EmitServersChanged(reason string, extra map[string]any)
}

// SecurityScanEnricher provides the current security scan summary for a
// server, shaped for inclusion in contracts.Server.SecurityScan. nil means
// "no summary yet for this server" (never scanned, no cached result).
//
// ListServers calls this once per server when wired so REST and the SSE
// servers.changed embed (which goes through runtime.buildServersChangedPayload
// → lister.ListServers) share one enrichment site. Without that parity, the
// Web UI's mergeServers (which treats incoming server data as authoritative
// and deletes absent keys) silently strips security_scan from the store on
// every SSE delivery — same bug class as the pre-existing quarantine-stats
// staleness fixed by PR #463.
type SecurityScanEnricher interface {
	GetSecurityScanSummary(ctx context.Context, serverName string) *contracts.SecurityScanSummary
}

// RuntimeOperations defines the interface for runtime operations needed by the service.
// This allows the service to delegate to runtime without a direct dependency.
type RuntimeOperations interface {
	EnableServer(serverName string, enabled bool) error
	RestartServer(serverName string) error
	GetAllServers() ([]map[string]interface{}, error)
	BulkEnableServers(serverNames []string, enabled bool) (map[string]error, error)
	GetServerTools(serverName string) ([]map[string]interface{}, error)
	GetToolApproval(serverName, toolName string) (*storage.ToolApprovalRecord, error)
	TriggerOAuthLogin(serverName string) error
	// TriggerOAuthLoginQuick returns browser status immediately (Spec 020 fix)
	TriggerOAuthLoginQuick(serverName string) (*core.OAuthStartResult, error)
	TriggerOAuthLogout(serverName string) error
	RefreshOAuthToken(serverName string) error
}

// service implements the Service interface with dependency injection.
type service struct {
	runtime        RuntimeOperations
	config         *config.Config
	configPath     string
	eventEmitter   EventEmitter
	secretResolver *secret.Resolver
	logger         *zap.SugaredLogger

	// scanEnricher is the optional adapter that populates each server's
	// SecurityScan field on ListServers. Wired by internal/server after
	// the scanner.Service is constructed (the scanner is created later
	// in the boot sequence than the management service). Read-mostly,
	// guarded by scanEnricherMu so wiring is concurrency-safe.
	scanEnricher   SecurityScanEnricher
	scanEnricherMu sync.RWMutex
}

// SetScanSummaryEnricher installs the SecurityScanEnricher used by
// ListServers to populate Server.SecurityScan. Called once during wire-up
// in internal/server.NewServerWithConfigPath right after the scanner is
// constructed. nil disables enrichment.
func (s *service) SetScanSummaryEnricher(e SecurityScanEnricher) {
	s.scanEnricherMu.Lock()
	s.scanEnricher = e
	s.scanEnricherMu.Unlock()
}

func (s *service) getScanEnricher() SecurityScanEnricher {
	s.scanEnricherMu.RLock()
	defer s.scanEnricherMu.RUnlock()
	return s.scanEnricher
}

// NewService creates a new management service with the given dependencies.
// The runtime parameter should implement RuntimeOperations (typically *runtime.Runtime).
func NewService(
	runtime RuntimeOperations,
	cfg *config.Config,
	configPath string,
	eventEmitter EventEmitter,
	secretResolver *secret.Resolver,
	logger *zap.SugaredLogger,
) Service {
	return &service{
		runtime:        runtime,
		config:         cfg,
		configPath:     configPath,
		eventEmitter:   eventEmitter,
		secretResolver: secretResolver,
		logger:         logger,
	}
}

// checkWriteGates verifies if write operations are allowed based on configuration.
// Returns an error if disable_management or read_only mode is enabled.
func (s *service) checkWriteGates() error {
	if s.config.DisableManagement {
		return fmt.Errorf("management operations are disabled (disable_management=true)")
	}
	if s.config.ReadOnlyMode {
		return fmt.Errorf("management operations are disabled (read_only_mode=true)")
	}
	return nil
}

// ListServers returns all configured servers with aggregate statistics.
// This is a read operation and never blocked by configuration gates.
// stringifyDiagnosticField coerces a diagnostic code/severity field to a
// plain string regardless of whether it was encoded as a Go named-string
// type (diagnostics.Code, diagnostics.Severity) or a plain string after a
// JSON round-trip. Spec 044.
func stringifyDiagnosticField(v interface{}) string {
	switch x := v.(type) {
	case nil:
		return ""
	case string:
		return x
	case fmt.Stringer:
		return x.String()
	default:
		// Named string types (e.g. `type Code string`) won't match `string`
		// above, but their underlying value is still a string. Use the
		// generic `%v` formatter to extract it safely.
		return fmt.Sprintf("%v", x)
	}
}

func (s *service) ListServers(ctx context.Context) ([]*contracts.Server, *contracts.ServerStats, error) {
	// Get servers from runtime
	serversRaw, err := s.runtime.GetAllServers()
	if err != nil {
		s.logger.Errorw("Failed to list servers", "error", err)
		return nil, nil, fmt.Errorf("failed to list servers: %w", err)
	}

	// Convert to contracts.Server format
	servers := make([]*contracts.Server, 0, len(serversRaw))
	stats := &contracts.ServerStats{}

	for _, srvRaw := range serversRaw {
		// Convert map to Server struct
		srv := &contracts.Server{}

		// Extract basic fields
		if name, ok := srvRaw["name"].(string); ok {
			srv.Name = name
		}
		if id, ok := srvRaw["id"].(string); ok {
			srv.ID = id
		}
		// Fallback: use name as ID if id is empty
		if srv.ID == "" {
			srv.ID = srv.Name
		}
		if protocol, ok := srvRaw["protocol"].(string); ok {
			srv.Protocol = protocol
		}
		if enabled, ok := srvRaw["enabled"].(bool); ok {
			srv.Enabled = enabled
		}
		if connected, ok := srvRaw["connected"].(bool); ok {
			srv.Connected = connected
		}
		if connecting, ok := srvRaw["connecting"].(bool); ok {
			srv.Connecting = connecting
		}
		if quarantined, ok := srvRaw["quarantined"].(bool); ok {
			srv.Quarantined = quarantined
		}
		if status, ok := srvRaw["status"].(string); ok {
			srv.Status = status
		}
		if lastError, ok := srvRaw["last_error"].(string); ok {
			srv.LastError = lastError
		}
		if url, ok := srvRaw["url"].(string); ok {
			srv.URL = url
		}
		if command, ok := srvRaw["command"].(string); ok {
			srv.Command = command
		}
		if args, ok := srvRaw["args"].([]string); ok {
			srv.Args = args
		} else if argsI, ok := srvRaw["args"].([]interface{}); ok {
			srv.Args = make([]string, 0, len(argsI))
			for _, a := range argsI {
				if s, ok := a.(string); ok {
					srv.Args = append(srv.Args, s)
				}
			}
		}
		if workingDir, ok := srvRaw["working_dir"].(string); ok {
			srv.WorkingDir = workingDir
		}

		// Extract headers and env. Accept both typed (map[string]string,
		// the shape used by the in-process StateView path) and generic
		// (map[string]interface{}, the shape after a JSON round-trip)
		// so this works regardless of how the runtime layer delivered
		// the server map. Header redaction is applied later in the HTTP
		// layer (see httpapi/server.go:redactServerHeaders).
		if headersTyped, ok := srvRaw["headers"].(map[string]string); ok && len(headersTyped) > 0 {
			srv.Headers = headersTyped
		} else if headersGeneric, ok := srvRaw["headers"].(map[string]interface{}); ok && len(headersGeneric) > 0 {
			srv.Headers = make(map[string]string, len(headersGeneric))
			for k, v := range headersGeneric {
				if vStr, ok := v.(string); ok {
					srv.Headers[k] = vStr
				}
			}
		}
		if envTyped, ok := srvRaw["env"].(map[string]string); ok && len(envTyped) > 0 {
			srv.Env = envTyped
		} else if envGeneric, ok := srvRaw["env"].(map[string]interface{}); ok && len(envGeneric) > 0 {
			srv.Env = make(map[string]string, len(envGeneric))
			for k, v := range envGeneric {
				if vStr, ok := v.(string); ok {
					srv.Env[k] = vStr
				}
			}
		}

		// Extract isolation overrides if present. Like OAuth above, both
		// typed and generic map shapes are accepted so this path works
		// whether the runtime provides a map[string]interface{} directly
		// or goes through a json round-trip first.
		if isoRaw, ok := srvRaw["isolation"].(map[string]interface{}); ok && isoRaw != nil {
			iso := &contracts.IsolationConfig{}
			if enabled, ok := isoRaw["enabled"].(bool); ok {
				iso.Enabled = enabled
			}
			if img, ok := isoRaw["image"].(string); ok {
				iso.Image = img
			}
			if nm, ok := isoRaw["network_mode"].(string); ok {
				iso.NetworkMode = nm
			}
			if extra, ok := isoRaw["extra_args"].([]string); ok {
				iso.ExtraArgs = extra
			} else if extraI, ok := isoRaw["extra_args"].([]interface{}); ok {
				iso.ExtraArgs = make([]string, 0, len(extraI))
				for _, a := range extraI {
					if s, ok := a.(string); ok {
						iso.ExtraArgs = append(iso.ExtraArgs, s)
					}
				}
			}
			if wd, ok := isoRaw["working_dir"].(string); ok {
				iso.WorkingDir = wd
			}
			srv.Isolation = iso
		}

		// Populate resolved isolation defaults so UI clients (macOS tray,
		// web UI) can render meaningful placeholders for the override
		// fields. Only meaningful for stdio servers — HTTP servers don't
		// run in containers. We compute this regardless of whether the
		// server is currently isolated; the UI uses it as a hint.
		if srv.Protocol == "stdio" && srv.Command != "" && s.config != nil && s.config.DockerIsolation != nil {
			im := core.NewIsolationManager(s.config.DockerIsolation)
			tmpCfg := &config.ServerConfig{Name: srv.Name, Command: srv.Command}
			if defaults := im.ResolveDefaults(tmpCfg); defaults != nil {
				srv.IsolationDefaults = &contracts.IsolationDefaults{
					RuntimeType: defaults.RuntimeType,
					Image:       defaults.Image,
					NetworkMode: defaults.NetworkMode,
					ExtraArgs:   defaults.ExtraArgs,
					WorkingDir:  defaults.ContainerWorkingDir,
				}
			}
		}

		if authenticated, ok := srvRaw["authenticated"].(bool); ok {
			srv.Authenticated = authenticated
		}

		// Extract OAuth config if present
		if oauthRaw, ok := srvRaw["oauth"].(map[string]interface{}); ok && oauthRaw != nil {
			oauthCfg := &contracts.OAuthConfig{}
			if clientID, ok := oauthRaw["client_id"].(string); ok {
				oauthCfg.ClientID = clientID
			}
			// Try both []string (from runtime) and []interface{} (from generic conversion)
			if scopes, ok := oauthRaw["scopes"].([]string); ok {
				oauthCfg.Scopes = scopes
			} else if scopes, ok := oauthRaw["scopes"].([]interface{}); ok {
				oauthCfg.Scopes = make([]string, 0, len(scopes))
				for _, scope := range scopes {
					if scopeStr, ok := scope.(string); ok {
						oauthCfg.Scopes = append(oauthCfg.Scopes, scopeStr)
					}
				}
			}
			if authURL, ok := oauthRaw["auth_url"].(string); ok {
				oauthCfg.AuthURL = authURL
			}
			if tokenURL, ok := oauthRaw["token_url"].(string); ok {
				oauthCfg.TokenURL = tokenURL
			}
			// Try both map[string]string (from runtime) and map[string]interface{} (from generic conversion)
			if extraParams, ok := oauthRaw["extra_params"].(map[string]string); ok {
				oauthCfg.ExtraParams = extraParams
			} else if extraParams, ok := oauthRaw["extra_params"].(map[string]interface{}); ok {
				oauthCfg.ExtraParams = make(map[string]string)
				for k, v := range extraParams {
					if vStr, ok := v.(string); ok {
						oauthCfg.ExtraParams[k] = vStr
					}
				}
			}
			if redirectPort, ok := oauthRaw["redirect_port"].(int); ok {
				oauthCfg.RedirectPort = redirectPort
			}
			if pkceEnabled, ok := oauthRaw["pkce_enabled"].(bool); ok {
				oauthCfg.PKCEEnabled = pkceEnabled
			}
			if tokenExpiresAt, ok := oauthRaw["token_expires_at"].(string); ok && tokenExpiresAt != "" {
				if parsedTime, err := time.Parse(time.RFC3339, tokenExpiresAt); err == nil {
					oauthCfg.TokenExpiresAt = &parsedTime
				}
			}
			if tokenValid, ok := oauthRaw["token_valid"].(bool); ok {
				oauthCfg.TokenValid = tokenValid
			}
			srv.OAuth = oauthCfg
		}

		// Extract numeric fields
		if toolCount, ok := srvRaw["tool_count"].(int); ok {
			srv.ToolCount = toolCount
			// Only count tools from enabled servers in the total
			if srv.Enabled {
				stats.TotalTools += toolCount
			}
		}
		if retryCount, ok := srvRaw["retry_count"].(int); ok {
			srv.ReconnectCount = retryCount
		}

		// Extract timestamp fields
		if created, ok := srvRaw["created"].(time.Time); ok {
			srv.Created = created
		}
		if updated, ok := srvRaw["updated"].(time.Time); ok {
			srv.Updated = updated
		}

		// Extract reconnect_on_use
		if reconnectOnUse, ok := srvRaw["reconnect_on_use"].(bool); ok {
			srv.ReconnectOnUse = reconnectOnUse
		}

		// MCP-2940: project the per-server auto-approve intent. Tri-state
		// *bool — only set the pointer when the key is present so an unset
		// flag stays nil and the Web UI toggle can tell unset from false.
		if autoApprove, ok := srvRaw["auto_approve_tool_changes"].(bool); ok {
			v := autoApprove
			srv.AutoApproveToolChanges = &v
		}

		// MCP-3322: project the per-server init_timeout override. The runtime
		// emits it as a duration string; accept a typed *config.Duration too in
		// case the map was delivered without a JSON round-trip.
		if its, ok := srvRaw["init_timeout"].(string); ok && its != "" {
			if d, err := time.ParseDuration(its); err == nil {
				v := config.Duration(d)
				srv.InitTimeout = &v
			}
		} else if itd, ok := srvRaw["init_timeout"].(*config.Duration); ok && itd != nil {
			v := *itd
			srv.InitTimeout = &v
		}

		// Extract unified health status
		if health, ok := srvRaw["health"].(*contracts.HealthStatus); ok {
			srv.Health = health
		}

		// Spec 044 — extract structured diagnostic + stable error code.
		// The runtime layer already encodes these as a map[string]interface{}
		// alongside the raw code string.
		if errCode, ok := srvRaw["error_code"].(string); ok && errCode != "" {
			srv.ErrorCode = errCode
		}
		if diagRaw, ok := srvRaw["diagnostic"].(map[string]interface{}); ok && diagRaw != nil {
			d := &contracts.Diagnostic{}
			// Code arrives as diagnostics.Code (a named string type) when the
			// map is produced directly by runtime.GetAllServers, and as a
			// plain string after a JSON round-trip. Handle both.
			d.Code = stringifyDiagnosticField(diagRaw["code"])
			d.Severity = stringifyDiagnosticField(diagRaw["severity"])
			if cause, ok := diagRaw["cause"].(string); ok {
				d.Cause = cause
			}
			if detected, ok := diagRaw["detected_at"].(time.Time); ok && !detected.IsZero() {
				t := detected
				d.DetectedAt = &t
			}
			if um, ok := diagRaw["user_message"].(string); ok {
				d.UserMessage = um
			}
			if docs, ok := diagRaw["docs_url"].(string); ok {
				d.DocsURL = docs
			}
			// fix_steps arrives as the concrete []diagnostics.FixStep slice
			// from runtime.GetAllServers, or as []interface{} after a JSON
			// round-trip. Use a JSON round-trip that works for both shapes —
			// it's cheap and sidesteps the type-assertion explosion.
			if steps, ok := diagRaw["fix_steps"]; ok && steps != nil {
				if raw, err := json.Marshal(steps); err == nil {
					_ = json.Unmarshal(raw, &d.FixSteps)
				}
			}
			srv.Diagnostic = d
		}

		// MCP-901 — project registry provenance so the approval/quarantine
		// view can show a server's origin. The SSE servers.changed embed
		// shares this projection (buildServersChangedPayload → ListServers),
		// so it stays in parity automatically.
		if regID, ok := srvRaw["source_registry_id"].(string); ok && regID != "" {
			srv.SourceRegistryID = regID
		}
		if prov, ok := srvRaw["source_registry_provenance"].(string); ok && prov != "" {
			srv.SourceRegistryProvenance = prov
		}

		servers = append(servers, srv)

		// Update stats
		stats.TotalServers++
		if srv.Connected {
			stats.ConnectedServers++
		}
		if srv.Quarantined {
			stats.QuarantinedServers++
		}
	}

	// Enrich with security scan summary so REST and the SSE servers.changed
	// embed share a single enrichment site. mergeServers on the Web UI
	// deletes absent fields from incoming payloads, so any field added later
	// on only one path silently goes stale on the other. Lives here so the
	// runtime's emitServersChanged → buildServersChangedPayload chain (which
	// calls ListServers) carries SecurityScan automatically.
	if enricher := s.getScanEnricher(); enricher != nil {
		for _, srv := range servers {
			if srv == nil {
				continue
			}
			if summary := enricher.GetSecurityScanSummary(ctx, srv.Name); summary != nil {
				srv.SecurityScan = summary
			}
		}
	}

	return servers, stats, nil
}

// GetServerLogs retrieves recent log entries for a specific server.
// This is a read operation and never blocked by configuration gates.
func (s *service) GetServerLogs(ctx context.Context, name string, tail int) ([]contracts.LogEntry, error) {
	// TODO: Implement later (not in critical path)
	return nil, fmt.Errorf("not implemented")
}

// EnableServer enables or disables a specific upstream server.
func (s *service) EnableServer(ctx context.Context, name string, enabled bool) error {
	// Check configuration gates
	if err := s.checkWriteGates(); err != nil {
		s.logger.Warnw("EnableServer blocked by configuration gate",
			"server", name,
			"enabled", enabled,
			"error", err)
		return err
	}

	// Delegate to runtime
	if err := s.runtime.EnableServer(name, enabled); err != nil {
		s.logger.Errorw("Failed to enable/disable server",
			"server", name,
			"enabled", enabled,
			"error", err)
		return fmt.Errorf("failed to enable server '%s': %w", name, err)
	}

	s.logger.Infow("Successfully changed server enabled state",
		"server", name,
		"enabled", enabled)

	// Note: Runtime already emits the event, so we don't duplicate it here
	return nil
}

// RestartServer stops and restarts a specific upstream server connection.
func (s *service) RestartServer(ctx context.Context, name string) error {
	// Check configuration gates
	if err := s.checkWriteGates(); err != nil {
		s.logger.Warnw("RestartServer blocked by configuration gate",
			"server", name,
			"error", err)
		return err
	}

	// Delegate to runtime
	if err := s.runtime.RestartServer(name); err != nil {
		s.logger.Errorw("Failed to restart server",
			"server", name,
			"error", err)
		return fmt.Errorf("failed to restart server '%s': %w", name, err)
	}

	s.logger.Infow("Successfully restarted server", "server", name)

	// Note: Runtime already emits the event, so we don't duplicate it here
	return nil
}

// T070: RestartAll restarts all configured servers sequentially.
// Continues on partial failures and returns detailed results.
func (s *service) RestartAll(ctx context.Context) (*BulkOperationResult, error) {
	startTime := time.Now()
	correlationID := reqcontext.GetCorrelationID(ctx)
	source := reqcontext.GetRequestSource(ctx)
	maxWorkers := 4

	s.logger.Infow("Bulk operation initiated",
		"operation", "restart_all",
		"correlation_id", correlationID,
		"source", source)

	// Check configuration gates
	if err := s.checkWriteGates(); err != nil {
		s.logger.Warnw("RestartAll blocked by configuration gate",
			"correlation_id", correlationID,
			"source", source,
			"error", err)
		return nil, err
	}

	// Get all servers
	servers, err := s.runtime.GetAllServers()
	if err != nil {
		s.logger.Errorw("Failed to get servers for RestartAll",
			"correlation_id", correlationID,
			"source", source,
			"error", err)
		return nil, fmt.Errorf("failed to get servers: %w", err)
	}

	result := &BulkOperationResult{
		Errors: make(map[string]string),
	}

	// Collect valid server names
	targetServers := make([]string, 0, len(servers))
	for _, server := range servers {
		name, ok := server["name"].(string)
		if !ok {
			s.logger.Warnw("Server missing name field, skipping",
				"correlation_id", correlationID,
				"server", server)
			continue
		}
		targetServers = append(targetServers, name)
	}

	result.Total = len(targetServers)
	if len(targetServers) == 0 {
		return result, nil
	}

	// Parallelize restarts with a small worker pool
	sem := make(chan struct{}, maxWorkers)
	var mu sync.Mutex
	var wg sync.WaitGroup

	for _, name := range targetServers {
		// Respect context cancellation before spawning
		select {
		case <-ctx.Done():
			mu.Lock()
			result.Errors[name] = ctx.Err().Error()
			result.Failed++
			mu.Unlock()
			continue
		default:
		}

		wg.Add(1)
		sem <- struct{}{}
		go func(serverName string) {
			defer wg.Done()
			defer func() { <-sem }()

			if err := s.runtime.RestartServer(serverName); err != nil {
				s.logger.Errorw("Failed to restart server in bulk operation",
					"correlation_id", correlationID,
					"server", serverName,
					"error", err)
				mu.Lock()
				result.Failed++
				result.Errors[serverName] = err.Error()
				mu.Unlock()
				return
			}

			s.logger.Infow("Successfully restarted server in bulk operation",
				"correlation_id", correlationID,
				"server", serverName)
			mu.Lock()
			result.Successful++
			mu.Unlock()
		}(name)
	}

	wg.Wait()

	duration := time.Since(startTime)
	s.logger.Infow("RestartAll completed",
		"correlation_id", correlationID,
		"source", source,
		"duration_ms", duration.Milliseconds(),
		"total", result.Total,
		"successful", result.Successful,
		"failed", result.Failed)

	return result, nil
}

// T071: EnableAll enables all configured servers.
// Continues on partial failures and returns detailed results.
func (s *service) EnableAll(ctx context.Context) (*BulkOperationResult, error) {
	startTime := time.Now()
	correlationID := reqcontext.GetCorrelationID(ctx)
	source := reqcontext.GetRequestSource(ctx)

	s.logger.Infow("Bulk operation initiated",
		"operation", "enable_all",
		"correlation_id", correlationID,
		"source", source)

	// Check configuration gates
	if err := s.checkWriteGates(); err != nil {
		s.logger.Warnw("EnableAll blocked by configuration gate",
			"correlation_id", correlationID,
			"source", source,
			"error", err)
		return nil, err
	}

	// Get all servers
	servers, err := s.runtime.GetAllServers()
	if err != nil {
		s.logger.Errorw("Failed to get servers for EnableAll",
			"correlation_id", correlationID,
			"source", source,
			"error", err)
		return nil, fmt.Errorf("failed to get servers: %w", err)
	}

	// Filter to only servers that actually need an update
	targetServers := make([]string, 0, len(servers))
	for _, server := range servers {
		name, ok := server["name"].(string)
		enabled, hasEnabled := server["enabled"].(bool)
		if !ok {
			s.logger.Warnw("Server missing name field, skipping",
				"correlation_id", correlationID,
				"server", server)
			continue
		}
		if hasEnabled && enabled {
			continue // Already enabled; skip
		}
		targetServers = append(targetServers, name)
	}

	// Short-circuit if there's nothing to do
	if len(targetServers) == 0 {
		return &BulkOperationResult{
			Total:      0,
			Successful: 0,
			Failed:     0,
			Errors:     map[string]string{},
		}, nil
	}

	// Apply changes in one batch to reduce config writes
	perServerErrs, opErr := s.runtime.BulkEnableServers(targetServers, true)
	if opErr != nil {
		s.logger.Errorw("Failed to enable servers in bulk operation",
			"correlation_id", correlationID,
			"source", source,
			"error", opErr)
		return nil, opErr
	}

	result := &BulkOperationResult{
		Total:  len(targetServers),
		Errors: make(map[string]string),
	}

	for _, name := range targetServers {
		if errMsg, exists := perServerErrs[name]; exists && errMsg != nil {
			result.Failed++
			result.Errors[name] = errMsg.Error()
		} else {
			result.Successful++
		}
	}

	duration := time.Since(startTime)
	s.logger.Infow("EnableAll completed",
		"correlation_id", correlationID,
		"source", source,
		"duration_ms", duration.Milliseconds(),
		"total", result.Total,
		"successful", result.Successful,
		"failed", result.Failed)

	return result, nil
}

// T072: DisableAll disables all configured servers.
// Continues on partial failures and returns detailed results.
func (s *service) DisableAll(ctx context.Context) (*BulkOperationResult, error) {
	startTime := time.Now()
	correlationID := reqcontext.GetCorrelationID(ctx)
	source := reqcontext.GetRequestSource(ctx)

	s.logger.Infow("Bulk operation initiated",
		"operation", "disable_all",
		"correlation_id", correlationID,
		"source", source)

	// Check configuration gates
	if err := s.checkWriteGates(); err != nil {
		s.logger.Warnw("DisableAll blocked by configuration gate",
			"correlation_id", correlationID,
			"source", source,
			"error", err)
		return nil, err
	}

	// Get all servers
	servers, err := s.runtime.GetAllServers()
	if err != nil {
		s.logger.Errorw("Failed to get servers for DisableAll",
			"correlation_id", correlationID,
			"source", source,
			"error", err)
		return nil, fmt.Errorf("failed to get servers: %w", err)
	}

	// Filter to only servers that actually need to be disabled
	targetServers := make([]string, 0, len(servers))
	for _, server := range servers {
		name, ok := server["name"].(string)
		enabled, hasEnabled := server["enabled"].(bool)
		if !ok {
			s.logger.Warnw("Server missing name field, skipping",
				"correlation_id", correlationID,
				"server", server)
			continue
		}
		if hasEnabled && !enabled {
			continue // Already disabled
		}
		targetServers = append(targetServers, name)
	}

	if len(targetServers) == 0 {
		return &BulkOperationResult{
			Total:      0,
			Successful: 0,
			Failed:     0,
			Errors:     map[string]string{},
		}, nil
	}

	perServerErrs, opErr := s.runtime.BulkEnableServers(targetServers, false)
	if opErr != nil {
		s.logger.Errorw("Failed to disable servers in bulk operation",
			"correlation_id", correlationID,
			"source", source,
			"error", opErr)
		return nil, opErr
	}

	result := &BulkOperationResult{
		Total:  len(targetServers),
		Errors: make(map[string]string),
	}

	for _, name := range targetServers {
		if errMsg, exists := perServerErrs[name]; exists && errMsg != nil {
			result.Failed++
			result.Errors[name] = errMsg.Error()
		} else {
			result.Successful++
		}
	}

	duration := time.Since(startTime)
	s.logger.Infow("DisableAll completed",
		"correlation_id", correlationID,
		"source", source,
		"duration_ms", duration.Milliseconds(),
		"total", result.Total,
		"successful", result.Successful,
		"failed", result.Failed)

	return result, nil
}

// Doctor is now implemented in diagnostics.go (T040-T044)

// GetServerTools retrieves all tools for a specific upstream server (T013).
// This method delegates to runtime's GetServerTools() which reads from StateView cache.
func (s *service) GetServerTools(ctx context.Context, name string) ([]map[string]interface{}, error) {
	// Validate input
	if name == "" {
		return nil, fmt.Errorf("server name required")
	}

	// Delegate to runtime (existing implementation)
	tools, err := s.runtime.GetServerTools(name)
	if err != nil {
		return nil, fmt.Errorf("failed to get tools: %w", err)
	}

	// Enrich with approval status from storage
	for i, tool := range tools {
		toolName, _ := tool["name"].(string)
		if toolName == "" {
			continue
		}
		if record, err := s.runtime.GetToolApproval(name, toolName); err == nil && record != nil {
			tools[i]["approval_status"] = string(record.Status)
		}
	}

	return tools, nil
}

// TriggerOAuthLogin initiates OAuth authentication flow for a server (T014).
// This method checks config gates, delegates to runtime, and emits events on completion.
func (s *service) TriggerOAuthLogin(ctx context.Context, name string) error {
	// Validate input
	if name == "" {
		return fmt.Errorf("server name required")
	}

	// Check configuration gates (T015)
	if err := s.checkWriteGates(); err != nil {
		return err
	}

	// Delegate to runtime (existing implementation)
	if err := s.runtime.TriggerOAuthLogin(name); err != nil {
		return fmt.Errorf("failed to start OAuth: %w", err)
	}

	// Event will be emitted by upstream manager on OAuth completion
	// (existing behavior - no changes needed)

	return nil
}

// TriggerOAuthLoginQuick initiates OAuth and returns browser status immediately (Spec 020 fix).
// Unlike TriggerOAuthLogin which runs fully async, this returns actual browser_opened, auth_url status.
func (s *service) TriggerOAuthLoginQuick(ctx context.Context, name string) (*core.OAuthStartResult, error) {
	// Validate input
	if name == "" {
		return nil, fmt.Errorf("server name required")
	}

	// Check configuration gates (T015)
	if err := s.checkWriteGates(); err != nil {
		return nil, err
	}

	// Delegate to runtime's quick OAuth method
	result, err := s.runtime.TriggerOAuthLoginQuick(name)
	if err != nil {
		return result, fmt.Errorf("failed to start OAuth: %w", err)
	}

	// Event will be emitted by upstream manager on OAuth completion
	return result, nil
}

// AuthStatus returns detailed OAuth authentication status for a specific server.
func (s *service) AuthStatus(ctx context.Context, name string) (*contracts.AuthStatus, error) {
	// TODO: Implement later (not in critical path)
	return nil, fmt.Errorf("not implemented")
}

// TriggerOAuthLogout clears OAuth token and disconnects a specific server.
// This method checks config gates, delegates to runtime, and emits events on completion.
func (s *service) TriggerOAuthLogout(ctx context.Context, name string) error {
	correlationID := reqcontext.GetCorrelationID(ctx)
	source := reqcontext.GetRequestSource(ctx)

	// Validate input
	if name == "" {
		return fmt.Errorf("server name required")
	}

	s.logger.Infow("OAuth logout initiated",
		"correlation_id", correlationID,
		"source", source,
		"server", name)

	// Check configuration gates
	if err := s.checkWriteGates(); err != nil {
		s.logger.Warnw("TriggerOAuthLogout blocked by configuration gate",
			"correlation_id", correlationID,
			"source", source,
			"server", name,
			"error", err)
		return err
	}

	// Delegate to runtime
	if err := s.runtime.TriggerOAuthLogout(name); err != nil {
		s.logger.Errorw("Failed to trigger OAuth logout",
			"correlation_id", correlationID,
			"source", source,
			"server", name,
			"error", err)
		return fmt.Errorf("failed to logout: %w", err)
	}

	s.logger.Infow("OAuth logout completed successfully",
		"correlation_id", correlationID,
		"source", source,
		"server", name)

	// Emit event for UI updates
	if s.eventEmitter != nil {
		s.eventEmitter.EmitServersChanged("oauth_logout", map[string]any{"server": name})
	}

	return nil
}

// LogoutAllOAuth clears OAuth tokens for all OAuth-enabled servers.
// Returns BulkOperationResult with success/failure counts.
func (s *service) LogoutAllOAuth(ctx context.Context) (*BulkOperationResult, error) {
	startTime := time.Now()
	correlationID := reqcontext.GetCorrelationID(ctx)
	source := reqcontext.GetRequestSource(ctx)

	s.logger.Infow("Bulk OAuth logout initiated",
		"correlation_id", correlationID,
		"source", source)

	// Check configuration gates
	if err := s.checkWriteGates(); err != nil {
		s.logger.Warnw("LogoutAllOAuth blocked by configuration gate",
			"correlation_id", correlationID,
			"source", source,
			"error", err)
		return nil, err
	}

	// Get all servers
	servers, err := s.runtime.GetAllServers()
	if err != nil {
		s.logger.Errorw("Failed to get servers for LogoutAllOAuth",
			"correlation_id", correlationID,
			"source", source,
			"error", err)
		return nil, fmt.Errorf("failed to get servers: %w", err)
	}

	result := &BulkOperationResult{
		Errors: make(map[string]string),
	}

	// Filter to only OAuth-enabled servers and attempt logout
	for _, server := range servers {
		name, ok := server["name"].(string)
		if !ok {
			continue
		}

		// Check if server has OAuth config
		if _, hasOAuth := server["oauth"]; !hasOAuth {
			continue
		}

		result.Total++

		if err := s.runtime.TriggerOAuthLogout(name); err != nil {
			s.logger.Warnw("Failed to logout OAuth server in bulk operation",
				"correlation_id", correlationID,
				"server", name,
				"error", err)
			result.Failed++
			result.Errors[name] = err.Error()
		} else {
			result.Successful++
		}
	}

	duration := time.Since(startTime)
	s.logger.Infow("LogoutAllOAuth completed",
		"correlation_id", correlationID,
		"source", source,
		"duration_ms", duration.Milliseconds(),
		"total", result.Total,
		"successful", result.Successful,
		"failed", result.Failed)

	// Emit single event for all changes
	if s.eventEmitter != nil && result.Successful > 0 {
		s.eventEmitter.EmitServersChanged("oauth_logout_all", map[string]any{
			"count": result.Successful,
		})
	}

	return result, nil
}
