package api

import (
	"encoding/json"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/Temikus/denkeeper/internal/agent"
	"github.com/Temikus/denkeeper/internal/config"
	"github.com/Temikus/denkeeper/internal/llm"
	"github.com/Temikus/denkeeper/internal/security"
)

// agentConfigUpdateInput holds the mutable fields for PATCH /api/v1/agents/{name}.
type agentConfigUpdateInput struct {
	Name                      *string                  `json:"name,omitempty"`
	SessionTier               *string                  `json:"session_tier,omitempty"`
	LLMProvider               *string                  `json:"llm_provider,omitempty"`
	LLMModel                  *string                  `json:"llm_model,omitempty"`
	Description               *string                  `json:"description,omitempty"`
	MaxToolRounds             *int                     `json:"max_tool_rounds,omitempty"`
	BrowserURLAllowlist       *[]string                `json:"browser_url_allowlist,omitempty"`
	Fallbacks                 *[]config.FallbackConfig `json:"fallbacks,omitempty"`
	CostLimitSoft             *float64                 `json:"cost_limit_soft,omitempty"`
	CostLimitHard             *float64                 `json:"cost_limit_hard,omitempty"`
	Supervisor                *string                  `json:"supervisor,omitempty"` // empty string to clear
	SupervisorTimeout         *string                  `json:"supervisor_timeout,omitempty"`
	SupervisorContextMessages *int                     `json:"supervisor_context_messages,omitempty"`
	SupervisorBodyExcerptLen  *int                     `json:"supervisor_body_excerpt_len,omitempty"`
	SupervisorToolDescLen     *int                     `json:"supervisor_tool_desc_len,omitempty"`
	ReviewerModel             *string                  `json:"reviewer_model,omitempty"`
	ReviewerProvider          *string                  `json:"reviewer_provider,omitempty"`
	ReviewMaxIterations       *int                     `json:"review_max_iterations,omitempty"`
	ReviewTimeout             *string                  `json:"review_timeout,omitempty"`
	NudgeMemoryInterval       *int                     `json:"nudge_memory_interval,omitempty"`
	NudgeSkillInterval        *int                     `json:"nudge_skill_interval,omitempty"`
}

// handleAgentConfigUpdate godoc
// @Summary Update agent configuration
// @Description Mutates agent settings: tier, provider, model, cost limits, fallbacks, supervisor, etc.
// @Tags agents
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param name path string true "Agent name"
// @Param body body agentConfigUpdateInput true "Fields to update"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /agents/{name} [patch]
func (s *Server) handleAgentConfigUpdate(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	if name == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing agent name"})
		return
	}

	e := s.deps.Dispatcher.Agent(name)
	if e == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
		return
	}

	var input agentConfigUpdateInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
		return
	}

	if errMsg := validateAgentInput(&input); errMsg != "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": errMsg})
		return
	}

	// Handle rename before other mutations.
	if input.Name != nil && *input.Name != name {
		code, msg := s.handleAgentRename(name, *input.Name)
		if code != 0 {
			writeJSON(w, code, map[string]string{"error": msg})
			return
		}
		name = *input.Name
		e = s.deps.Dispatcher.Agent(name)
	}

	// Apply runtime changes to the engine.
	if httpStatus, errMsg := applyAgentRuntimeChanges(e, &input); httpStatus != 0 {
		writeJSON(w, httpStatus, map[string]string{"error": errMsg})
		return
	}

	// Validate and apply supervisor changes.
	if errMsg := s.applySupervisorChanges(name, e, &input); errMsg != "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": errMsg})
		return
	}

	// Sync per-agent cost limits to the live CostTracker.
	if input.CostLimitSoft != nil || input.CostLimitHard != nil {
		s.syncAgentCostLimits(name, &input)
	}

	s.persistAgentConfig(name, &input)
	s.updateInMemoryAgentConfig(name, &input)

	writeJSON(w, http.StatusOK, map[string]string{
		"name":   name,
		"status": "updated",
	})
}

// validateAgentInput checks all input fields for validity.
// Returns an error message or empty string on success.
func validateAgentInput(input *agentConfigUpdateInput) string {
	if input.SessionTier != nil && !security.ValidTier(*input.SessionTier) {
		return "invalid session_tier: must be autonomous, supervised, or restricted"
	}
	if input.Fallbacks != nil {
		config.MigrateFallbacks("api:agent_patch", *input.Fallbacks)
		if err := config.ValidateFallbacks(*input.Fallbacks); err != nil {
			return err.Error()
		}
	}
	if input.CostLimitSoft != nil && *input.CostLimitSoft < 0 {
		return "cost_limit_soft must be >= 0"
	}
	if input.CostLimitHard != nil && *input.CostLimitHard < 0 {
		return "cost_limit_hard must be >= 0"
	}
	if msg := validateSupervisorFields(input); msg != "" {
		return msg
	}
	if msg := validateReviewerFields(input); msg != "" {
		return msg
	}
	return ""
}

func validateSupervisorFields(input *agentConfigUpdateInput) string {
	if input.SupervisorTimeout != nil && *input.SupervisorTimeout != "" {
		if _, err := time.ParseDuration(*input.SupervisorTimeout); err != nil {
			return "invalid supervisor_timeout: " + err.Error()
		}
	}
	if input.SupervisorContextMessages != nil && *input.SupervisorContextMessages < 0 {
		return "supervisor_context_messages must be >= 0"
	}
	if input.SupervisorBodyExcerptLen != nil && *input.SupervisorBodyExcerptLen < 0 {
		return "supervisor_body_excerpt_len must be >= 0"
	}
	if input.SupervisorToolDescLen != nil && *input.SupervisorToolDescLen < 0 {
		return "supervisor_tool_desc_len must be >= 0"
	}
	return ""
}

func validateReviewerFields(input *agentConfigUpdateInput) string {
	if input.ReviewTimeout != nil && *input.ReviewTimeout != "" {
		if _, err := time.ParseDuration(*input.ReviewTimeout); err != nil {
			return "invalid review_timeout: " + err.Error()
		}
	}
	if input.ReviewMaxIterations != nil && *input.ReviewMaxIterations < 0 {
		return "review_max_iterations must be >= 0"
	}
	if input.NudgeMemoryInterval != nil && *input.NudgeMemoryInterval < 0 {
		return "nudge_memory_interval must be >= 0"
	}
	if input.NudgeSkillInterval != nil && *input.NudgeSkillInterval < 0 {
		return "nudge_skill_interval must be >= 0"
	}
	return ""
}

// applyAgentRuntimeChanges mutates the engine for the fields present in input.
// Returns (0, "") on success or (httpStatus, errorMessage) on failure.
func applyAgentRuntimeChanges(e *agent.Engine, input *agentConfigUpdateInput) (int, string) {
	if input.SessionTier != nil {
		if err := e.SetPermissionTier(*input.SessionTier); err != nil {
			return http.StatusInternalServerError, "setting permission tier: " + err.Error()
		}
	}
	if input.LLMProvider != nil {
		if err := e.SetProvider(*input.LLMProvider); err != nil {
			return http.StatusBadRequest, "invalid llm_provider: " + err.Error()
		}
	}
	if input.LLMModel != nil {
		e.SetModel(*input.LLMModel)
	}
	if input.MaxToolRounds != nil {
		if *input.MaxToolRounds <= 0 {
			return http.StatusBadRequest, "max_tool_rounds must be >= 1"
		}
		e.SetMaxToolRounds(*input.MaxToolRounds)
	}
	if input.Fallbacks != nil {
		e.LLMRouter().SetFallbacks(convertFallbackConfigs(*input.Fallbacks))
	}
	applyReviewerRuntime(e, input)
	return 0, ""
}

func applyReviewerRuntime(e *agent.Engine, input *agentConfigUpdateInput) {
	if input.NudgeMemoryInterval != nil || input.NudgeSkillInterval != nil {
		memInterval := 0
		skillInterval := 0
		if input.NudgeMemoryInterval != nil {
			memInterval = *input.NudgeMemoryInterval
		}
		if input.NudgeSkillInterval != nil {
			skillInterval = *input.NudgeSkillInterval
		}
		e.SetNudgeConfig(memInterval, skillInterval)
	}
	if input.ReviewMaxIterations != nil || input.ReviewTimeout != nil {
		var revTimeout time.Duration
		if input.ReviewTimeout != nil && *input.ReviewTimeout != "" {
			revTimeout, _ = time.ParseDuration(*input.ReviewTimeout)
		}
		maxIter := 0
		if input.ReviewMaxIterations != nil {
			maxIter = *input.ReviewMaxIterations
		}
		e.SetReviewerConfig(maxIter, revTimeout)
	}
}

// syncAgentCostLimits propagates cost limit changes to the live CostTracker.
func (s *Server) syncAgentCostLimits(name string, input *agentConfigUpdateInput) {
	defaults := s.deps.CostTracker.DefaultLimits()
	limits := defaults
	for _, ac := range s.deps.Config.Agents {
		if ac.Name == name {
			if ac.CostLimitSoft != nil {
				limits.Soft = *ac.CostLimitSoft
			}
			if ac.CostLimitHard != nil {
				limits.Hard = *ac.CostLimitHard
			}
			break
		}
	}
	if input.CostLimitSoft != nil {
		limits.Soft = *input.CostLimitSoft
	}
	if input.CostLimitHard != nil {
		limits.Hard = *input.CostLimitHard
	}
	s.deps.CostTracker.SetAgentLimits(name, limits)
}

// handleAgentRename validates and executes an agent rename.
// Returns (0, "") on success, or (httpStatus, errorMessage) on failure.
func (s *Server) handleAgentRename(oldName, newName string) (int, string) {
	if !config.ValidResourceName(newName) {
		return http.StatusBadRequest, "invalid agent name: must be lowercase alphanumeric with hyphens, max 64 chars"
	}
	if s.deps.Dispatcher.Agent(newName) != nil {
		return http.StatusConflict, "agent name already exists"
	}
	if err := s.deps.Dispatcher.RenameAgent(oldName, newName); err != nil {
		return http.StatusInternalServerError, "renaming agent: " + err.Error()
	}
	if s.deps.ConfigPath != "" {
		if err := config.RenameAgentInConfig(s.deps.ConfigPath, oldName, newName); err != nil {
			s.logger.Warn("failed to persist agent rename", "old", oldName, "new", newName, "error", err)
		}
	}
	s.renameInMemoryAgent(oldName, newName)
	return 0, ""
}

// convertFallbackConfigs converts config fallback rules to LLM router rules.
func convertFallbackConfigs(cfgs []config.FallbackConfig) []llm.FallbackRule {
	rules := make([]llm.FallbackRule, len(cfgs))
	for i, f := range cfgs {
		rules[i] = llm.FallbackRule{
			Trigger:    f.Trigger,
			Action:     f.Action,
			Provider:   f.Provider,
			Model:      f.Model,
			Scope:      f.Scope,
			MaxRetries: f.MaxRetries,
			Backoff:    f.Backoff,
		}
	}
	return rules
}

// persistAgentConfig writes changed fields to the TOML config file.
func (s *Server) persistAgentConfig(name string, input *agentConfigUpdateInput) {
	if s.deps.ConfigPath == "" {
		return
	}

	changes := buildAgentConfigChanges(input)
	if len(changes) > 0 {
		if err := config.UpdateAgentInConfig(s.deps.ConfigPath, name, changes); err != nil {
			s.logger.Warn("failed to persist agent config", "agent", name, "error", err)
		}
	}
}

// buildAgentConfigChanges converts non-nil input fields to a TOML change map.
func buildAgentConfigChanges(input *agentConfigUpdateInput) map[string]any {
	changes := make(map[string]any)
	if input.SessionTier != nil {
		changes["session_tier"] = *input.SessionTier
	}
	if input.LLMProvider != nil {
		changes["llm_provider"] = *input.LLMProvider
	}
	if input.LLMModel != nil {
		changes["llm_model"] = *input.LLMModel
	}
	if input.Description != nil {
		changes["description"] = *input.Description
	}
	if input.MaxToolRounds != nil {
		changes["max_tool_rounds"] = *input.MaxToolRounds
	}
	if input.BrowserURLAllowlist != nil {
		allowlist := make([]any, len(*input.BrowserURLAllowlist))
		for i, d := range *input.BrowserURLAllowlist {
			allowlist[i] = d
		}
		changes["browser_url_allowlist"] = allowlist
	}
	if input.Fallbacks != nil {
		changes["fallback"] = serializeFallbacks(*input.Fallbacks)
	}
	if input.CostLimitSoft != nil {
		changes["cost_limit_soft"] = *input.CostLimitSoft
	}
	if input.CostLimitHard != nil {
		changes["cost_limit_hard"] = *input.CostLimitHard
	}
	addSupervisorConfigChanges(changes, input)
	addReviewerConfigChanges(changes, input)
	return changes
}

func addSupervisorConfigChanges(changes map[string]any, input *agentConfigUpdateInput) {
	if input.Supervisor != nil {
		changes["supervisor"] = *input.Supervisor
	}
	if input.SupervisorTimeout != nil {
		changes["supervisor_timeout"] = *input.SupervisorTimeout
	}
	if input.SupervisorContextMessages != nil {
		changes["supervisor_context_messages"] = *input.SupervisorContextMessages
	}
	if input.SupervisorBodyExcerptLen != nil {
		changes["supervisor_body_excerpt_len"] = *input.SupervisorBodyExcerptLen
	}
	if input.SupervisorToolDescLen != nil {
		changes["supervisor_tool_desc_len"] = *input.SupervisorToolDescLen
	}
}

func addReviewerConfigChanges(changes map[string]any, input *agentConfigUpdateInput) {
	if input.ReviewerModel != nil {
		changes["reviewer_model"] = *input.ReviewerModel
	}
	if input.ReviewerProvider != nil {
		changes["reviewer_provider"] = *input.ReviewerProvider
	}
	if input.ReviewMaxIterations != nil {
		changes["review_max_iterations"] = *input.ReviewMaxIterations
	}
	if input.ReviewTimeout != nil {
		changes["review_timeout"] = *input.ReviewTimeout
	}
	if input.NudgeMemoryInterval != nil {
		changes["nudge_memory_interval"] = *input.NudgeMemoryInterval
	}
	if input.NudgeSkillInterval != nil {
		changes["nudge_skill_interval"] = *input.NudgeSkillInterval
	}
}

// updateInMemoryAgentConfig applies input fields to the in-memory config.
func (s *Server) updateInMemoryAgentConfig(name string, input *agentConfigUpdateInput) {
	if s.deps.Config == nil {
		return
	}
	for i := range s.deps.Config.Agents {
		if s.deps.Config.Agents[i].Name != name {
			continue
		}
		applyAgentFields(&s.deps.Config.Agents[i], input)
		return
	}
}

// renameInMemoryAgent updates the agent name in the in-memory config slice.
func (s *Server) renameInMemoryAgent(oldName, newName string) {
	if s.deps.Config == nil {
		return
	}
	for i := range s.deps.Config.Agents {
		if s.deps.Config.Agents[i].Name == oldName {
			s.deps.Config.Agents[i].Name = newName
			return
		}
	}
}

// applySupervisorChanges validates and applies all supervisor-related changes
// (reference, timeout, context messages). Returns an error message or "".
func (s *Server) applySupervisorChanges(name string, e *agent.Engine, input *agentConfigUpdateInput) string {
	if input.Supervisor != nil {
		if errMsg := s.applySupervisor(name, e, *input.Supervisor); errMsg != "" {
			return errMsg
		}
	}
	if input.SupervisorTimeout != nil || input.SupervisorContextMessages != nil {
		var timeout time.Duration
		var ctxMsgs int
		if input.SupervisorTimeout != nil {
			timeout, _ = time.ParseDuration(*input.SupervisorTimeout) // validated earlier
		}
		if input.SupervisorContextMessages != nil {
			ctxMsgs = *input.SupervisorContextMessages
		}
		e.SetSupervisorConfig(timeout, ctxMsgs)
	}
	if input.SupervisorBodyExcerptLen != nil || input.SupervisorToolDescLen != nil {
		var bodyLen, descLen int
		if input.SupervisorBodyExcerptLen != nil {
			bodyLen = *input.SupervisorBodyExcerptLen
		}
		if input.SupervisorToolDescLen != nil {
			descLen = *input.SupervisorToolDescLen
		}
		e.SetSupervisorExcerptConfig(bodyLen, descLen)
	}
	return ""
}

// applySupervisor validates and wires (or clears) the supervisor reference.
// Returns an error message or empty string on success.
func (s *Server) applySupervisor(name string, e *agent.Engine, supervisorName string) string {
	if supervisorName == "" {
		// Clear supervisor.
		e.SetSupervisor(nil)
		return ""
	}
	if supervisorName == name {
		return "agent cannot supervise itself"
	}
	sup := s.deps.Dispatcher.Agent(supervisorName)
	if sup == nil {
		return "supervisor agent not found: " + supervisorName
	}
	// Validate supervisor agent's tier is not supervised (would deadlock).
	if sup.PermissionTier() == "supervised" {
		return "supervisor agent must not use supervised tier"
	}
	// Validate no chaining — supervisor must not have its own supervisor.
	if sup.Supervisor() != nil {
		return "supervisor chaining is not supported: " + supervisorName + " already has a supervisor"
	}
	e.SetSupervisor(sup)
	return ""
}

func applyAgentFields(ac *config.AgentInstanceConfig, input *agentConfigUpdateInput) {
	if input.SessionTier != nil {
		ac.SessionTier = *input.SessionTier
	}
	if input.LLMProvider != nil {
		ac.LLMProvider = *input.LLMProvider
	}
	if input.LLMModel != nil {
		ac.LLMModel = *input.LLMModel
	}
	if input.Description != nil {
		ac.Description = *input.Description
	}
	if input.MaxToolRounds != nil {
		ac.MaxToolRounds = *input.MaxToolRounds
	}
	if input.BrowserURLAllowlist != nil {
		ac.BrowserURLAllowlist = *input.BrowserURLAllowlist
	}
	if input.Fallbacks != nil {
		ac.Fallbacks = *input.Fallbacks
	}
	if input.CostLimitSoft != nil {
		ac.CostLimitSoft = input.CostLimitSoft
	}
	if input.CostLimitHard != nil {
		ac.CostLimitHard = input.CostLimitHard
	}
	applySupervisorFields(ac, input)
	applyReviewerFields(ac, input)
}

func applySupervisorFields(ac *config.AgentInstanceConfig, input *agentConfigUpdateInput) {
	if input.Supervisor != nil {
		ac.Supervisor = *input.Supervisor
	}
	if input.SupervisorTimeout != nil {
		ac.SupervisorTimeout = *input.SupervisorTimeout
	}
	if input.SupervisorContextMessages != nil {
		ac.SupervisorContextMessages = *input.SupervisorContextMessages
	}
	if input.SupervisorBodyExcerptLen != nil {
		ac.SupervisorBodyExcerptLen = *input.SupervisorBodyExcerptLen
	}
	if input.SupervisorToolDescLen != nil {
		ac.SupervisorToolDescLen = *input.SupervisorToolDescLen
	}
}

func applyReviewerFields(ac *config.AgentInstanceConfig, input *agentConfigUpdateInput) {
	if input.ReviewerModel != nil {
		ac.ReviewerModel = *input.ReviewerModel
	}
	if input.ReviewerProvider != nil {
		ac.ReviewerProvider = *input.ReviewerProvider
	}
	if input.ReviewMaxIterations != nil {
		ac.ReviewMaxIterations = *input.ReviewMaxIterations
	}
	if input.ReviewTimeout != nil {
		ac.ReviewTimeout = *input.ReviewTimeout
	}
	if input.NudgeMemoryInterval != nil {
		ac.NudgeMemoryInterval = *input.NudgeMemoryInterval
	}
	if input.NudgeSkillInterval != nil {
		ac.NudgeSkillInterval = *input.NudgeSkillInterval
	}
}

// ---------------------------------------------------------------------------
// Agent create & delete
// ---------------------------------------------------------------------------

// companionSupervisorInput holds optional companion-supervisor config for
// atomic agent + supervisor creation (used by the setup wizard).
type companionSupervisorInput struct {
	Name            string `json:"name"`
	LLMModel        string `json:"llm_model,omitempty"`
	Timeout         string `json:"timeout,omitempty"`
	ContextMessages int    `json:"context_messages,omitempty"`
}

// agentCreateInput holds the fields for POST /api/v1/agents.
type agentCreateInput struct {
	Name             string                    `json:"name"`
	LLMProvider      string                    `json:"llm_provider,omitempty"`
	LLMModel         string                    `json:"llm_model,omitempty"`
	SessionTier      string                    `json:"session_tier,omitempty"`
	Description      string                    `json:"description,omitempty"`
	CreateSupervisor *companionSupervisorInput `json:"create_supervisor,omitempty"`
}

// handleCreateAgent godoc
// @Summary Create agent
// @Description Creates a new agent at runtime with persona directory and TOML persistence
// @Tags agents
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body agentCreateInput true "Agent configuration"
// @Success 201 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Router /agents [post]
func (s *Server) validateAgentCreateInput(input *agentCreateInput) (int, string) {
	if !config.ValidResourceName(input.Name) {
		return http.StatusBadRequest, "invalid agent name: must be lowercase alphanumeric with hyphens, 1-64 chars"
	}
	if s.deps.Dispatcher.Agent(input.Name) != nil {
		return http.StatusConflict, "agent already exists"
	}
	if input.SessionTier != "" && !security.ValidTier(input.SessionTier) {
		return http.StatusBadRequest, "invalid session_tier: must be autonomous, supervised, or restricted"
	}
	return 0, ""
}

func (s *Server) handleCreateAgent(w http.ResponseWriter, r *http.Request) {
	if s.deps.AgentFactory == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent creation not available"})
		return
	}
	if s.deps.ConfigPath == "" {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "config persistence not available"})
		return
	}

	var input agentCreateInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
		return
	}

	if code, msg := s.validateAgentCreateInput(&input); msg != "" {
		writeJSON(w, code, map[string]string{"error": msg})
		return
	}

	personaDir := filepath.Join(s.deps.Config.DataDir, "agents", input.Name)
	if err := os.MkdirAll(personaDir, 0o750); err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "creating persona directory: " + err.Error(),
		})
		return
	}

	ac := config.AgentInstanceConfig{
		Name:        input.Name,
		LLMProvider: input.LLMProvider,
		LLMModel:    input.LLMModel,
		SessionTier: input.SessionTier,
		Description: input.Description,
		PersonaDir:  personaDir,
	}

	e, _, err := s.deps.AgentFactory(ac)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "building agent engine: " + err.Error(),
		})
		return
	}

	// Persist to TOML first — this is the source of truth. If it fails,
	// don't register the agent in memory to avoid config drift on restart.
	if err := config.AddAgentToConfig(s.deps.ConfigPath, input.Name, input.LLMProvider, input.LLMModel, input.SessionTier, input.Description, personaDir); err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "persisting agent to config: " + err.Error(),
		})
		return
	}

	if err := s.deps.Dispatcher.AddAgent(input.Name, e); err != nil {
		// TOML was written but runtime registration failed — remove TOML entry
		// to stay consistent.
		if rmErr := config.RemoveAgentFromConfig(s.deps.ConfigPath, input.Name); rmErr != nil {
			s.logger.Warn("failed to roll back agent config after AddAgent error", "agent", input.Name, "error", rmErr)
		}
		writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
		return
	}

	if s.deps.Config != nil {
		s.deps.Config.Agents = append(s.deps.Config.Agents, ac)
	}

	resp := map[string]string{"name": input.Name, "status": "created"}
	if input.CreateSupervisor != nil && input.SessionTier == "supervised" {
		supName, supCode, supErr := s.createCompanionSupervisor(input.Name, e, input.LLMProvider, input.CreateSupervisor)
		if supErr != "" {
			s.rollbackAgent(input.Name)
			writeJSON(w, supCode, map[string]string{"error": supErr})
			return
		}
		resp["supervisor"] = supName
	}
	writeJSON(w, http.StatusCreated, resp)
}

func (s *Server) rollbackAgent(name string) {
	if err := s.deps.Dispatcher.RemoveAgent(name); err != nil {
		s.logger.Warn("rollback: failed to remove agent from dispatcher", "agent", name, "error", err)
	}
	if rmErr := config.RemoveAgentFromConfig(s.deps.ConfigPath, name); rmErr != nil {
		s.logger.Warn("rollback: failed to remove agent from config", "agent", name, "error", rmErr)
	}
	if s.deps.Config != nil {
		for i, a := range s.deps.Config.Agents {
			if a.Name == name {
				personaDir := a.PersonaDir
				s.deps.Config.Agents = append(s.deps.Config.Agents[:i], s.deps.Config.Agents[i+1:]...)
				if personaDir != "" {
					_ = os.Remove(personaDir)
				}
				break
			}
		}
	}
}

func resolveCompanionName(sup *companionSupervisorInput) string {
	if sup.Name != "" {
		return sup.Name
	}
	return "supervisor"
}

func (s *Server) validateCompanionName(supName, agentName string) (int, string) {
	if !config.ValidResourceName(supName) {
		return http.StatusBadRequest, "invalid supervisor name: must be lowercase alphanumeric with hyphens, 1-64 chars"
	}
	if s.deps.Dispatcher.Agent(supName) != nil {
		return http.StatusConflict, "supervisor agent name already exists: " + supName
	}
	if supName == agentName {
		return http.StatusBadRequest, "supervisor name must differ from agent name"
	}
	return 0, ""
}

// createCompanionSupervisor creates an autonomous supervisor agent and wires it
// to the given supervised agent. Returns (supervisorName, httpStatus, errorMessage).
func (s *Server) createCompanionSupervisor(agentName string, mainEngine *agent.Engine, llmProvider string, sup *companionSupervisorInput) (string, int, string) {
	supName := resolveCompanionName(sup)
	if code, errMsg := s.validateCompanionName(supName, agentName); errMsg != "" {
		return "", code, errMsg
	}

	supPersonaDir := filepath.Join(s.deps.Config.DataDir, "agents", supName)
	if err := os.MkdirAll(supPersonaDir, 0o750); err != nil {
		return "", http.StatusInternalServerError, "creating supervisor persona directory: " + err.Error()
	}

	supAC := config.AgentInstanceConfig{
		Name:        supName,
		LLMProvider: llmProvider,
		LLMModel:    sup.LLMModel,
		SessionTier: "autonomous",
		PersonaDir:  supPersonaDir,
	}

	supEngine, _, err := s.deps.AgentFactory(supAC)
	if err != nil {
		return "", http.StatusInternalServerError, "building supervisor engine: " + err.Error()
	}

	if err := config.AddAgentToConfig(s.deps.ConfigPath, supName, llmProvider, sup.LLMModel, "autonomous", "", supPersonaDir); err != nil {
		return "", http.StatusInternalServerError, "persisting supervisor to config: " + err.Error()
	}

	if err := s.deps.Dispatcher.AddAgent(supName, supEngine); err != nil {
		_ = config.RemoveAgentFromConfig(s.deps.ConfigPath, supName)
		return "", http.StatusInternalServerError, "registering supervisor agent: " + err.Error()
	}

	if s.deps.Config != nil {
		s.deps.Config.Agents = append(s.deps.Config.Agents, supAC)
	}

	s.wireCompanionSupervisor(agentName, mainEngine, supEngine, sup)
	return supName, 0, ""
}

func (s *Server) wireCompanionSupervisor(agentName string, mainEngine, supEngine *agent.Engine, sup *companionSupervisorInput) {
	mainEngine.SetSupervisor(supEngine)

	timeout, err := time.ParseDuration(sup.Timeout)
	if err != nil {
		timeout = 30 * time.Second
	}
	ctxMsgs := sup.ContextMessages
	if ctxMsgs == 0 {
		ctxMsgs = 5
	}
	mainEngine.SetSupervisorConfig(timeout, ctxMsgs)

	supName := resolveCompanionName(sup)
	supChanges := map[string]any{"supervisor": supName}
	if sup.Timeout != "" {
		supChanges["supervisor_timeout"] = sup.Timeout
	}
	if sup.ContextMessages > 0 {
		supChanges["supervisor_context_messages"] = int64(sup.ContextMessages)
	}
	if err := config.UpdateAgentInConfig(s.deps.ConfigPath, agentName, supChanges); err != nil {
		s.logger.Warn("failed to persist supervisor reference", "agent", agentName, "error", err)
	}

	if s.deps.Config != nil {
		for i := range s.deps.Config.Agents {
			if s.deps.Config.Agents[i].Name == agentName {
				s.deps.Config.Agents[i].Supervisor = supName
				if sup.Timeout != "" {
					s.deps.Config.Agents[i].SupervisorTimeout = sup.Timeout
				}
				if sup.ContextMessages > 0 {
					s.deps.Config.Agents[i].SupervisorContextMessages = sup.ContextMessages
				}
				break
			}
		}
	}
}

// agentDependencyError checks whether the named agent is referenced by any
// channels or schedules and returns a descriptive error if so.
func (s *Server) agentDependencyError(name string) string {
	var blockingChannels []string
	for chName, ch := range s.deps.Dispatcher.Channels() {
		if ch.AgentName == name {
			blockingChannels = append(blockingChannels, chName)
		}
	}
	if len(blockingChannels) > 0 {
		return "agent is referenced by channels: " + strings.Join(blockingChannels, ", ")
	}

	if s.deps.Config != nil {
		var blockingSchedules []string
		for _, sched := range s.deps.Config.Schedules {
			if sched.Agent == name {
				blockingSchedules = append(blockingSchedules, sched.Name)
			}
		}
		if len(blockingSchedules) > 0 {
			return "agent is referenced by schedules: " + strings.Join(blockingSchedules, ", ")
		}

		var blockingSupervisors []string
		for _, ag := range s.deps.Config.Agents {
			if ag.Supervisor == name {
				blockingSupervisors = append(blockingSupervisors, ag.Name)
			}
		}
		if len(blockingSupervisors) > 0 {
			return "agent is used as supervisor by: " + strings.Join(blockingSupervisors, ", ")
		}
	}
	return ""
}

// handleDeleteAgent godoc
// @Summary Delete agent
// @Description Removes an agent. Rejects if referenced by channels or schedules.
// @Tags agents
// @Produce json
// @Security BearerAuth
// @Param name path string true "Agent name"
// @Success 204 "No Content"
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Router /agents/{name} [delete]
func (s *Server) handleDeleteAgent(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	if name == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing agent name"})
		return
	}
	if s.deps.ConfigPath == "" {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "config persistence not available"})
		return
	}
	if s.deps.Dispatcher.Agent(name) == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
		return
	}
	if len(s.deps.Dispatcher.Agents()) == 1 {
		writeJSON(w, http.StatusConflict, map[string]string{"error": "cannot remove the last agent"})
		return
	}

	if depErr := s.agentDependencyError(name); depErr != "" {
		writeJSON(w, http.StatusConflict, map[string]string{"error": depErr})
		return
	}

	// Persist removal to TOML first — this is the source of truth. If it
	// fails, don't remove the agent from memory to avoid config drift.
	if err := config.RemoveAgentFromConfig(s.deps.ConfigPath, name); err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "removing agent from config: " + err.Error(),
		})
		return
	}

	if err := s.deps.Dispatcher.RemoveAgent(name); err != nil {
		// TOML was updated but runtime removal failed — re-add TOML entry
		// to stay consistent. This is unlikely but we handle it defensively.
		s.logger.Error("failed to remove agent from dispatcher after config update", "agent", name, "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}

	// Remove from in-memory config.
	if s.deps.Config != nil {
		agents := s.deps.Config.Agents
		for i := range agents {
			if agents[i].Name == name {
				s.deps.Config.Agents = append(agents[:i], agents[i+1:]...)
				break
			}
		}
	}

	w.WriteHeader(http.StatusNoContent)
}

// serializeFallbacks converts fallback configs to a slice of maps for TOML persistence.
func serializeFallbacks(fbs []config.FallbackConfig) []any {
	result := make([]any, len(fbs))
	for i, f := range fbs {
		m := map[string]any{
			"trigger": f.Trigger,
			"action":  f.Action,
		}
		if f.Provider != "" {
			m["provider"] = f.Provider
		}
		if f.Model != "" {
			m["model"] = f.Model
		}
		if f.Scope != "" {
			m["scope"] = f.Scope
		}
		if f.MaxRetries > 0 {
			m["max_retries"] = f.MaxRetries
		}
		if f.Backoff != "" {
			m["backoff"] = f.Backoff
		}
		result[i] = m
	}
	return result
}
