package mcp

import (
	"context"
	"fmt"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
)

// Workflow enforcement modes.
const (
	workflowModeBlock = "block" // out-of-phase calls are refused
	workflowModeWarn  = "warn"  // out-of-phase edits auto-advance the phase
)

// workflowPhase is one ordered stage of a workflow.
type workflowPhase struct {
	name       string
	allowsEdit bool
}

// workflowState is the per-session phase-enforcement state machine. It is
// nil on sessionState until a workflow is started; guarded by sessionState.mu.
type workflowState struct {
	phases  []workflowPhase
	current int
	mode    string
}

// defaultWorkflowPhases is the canonical explore → implement → verify
// ordering: editing tools are gated until the implement phase.
func defaultWorkflowPhases() []workflowPhase {
	return []workflowPhase{
		{name: "explore", allowsEdit: false},
		{name: "implement", allowsEdit: true},
		{name: "verify", allowsEdit: true},
	}
}

func (w *workflowState) phase() workflowPhase {
	if w.current < 0 || w.current >= len(w.phases) {
		return workflowPhase{name: "unknown"}
	}
	return w.phases[w.current]
}

func (w *workflowState) phaseNames() []string {
	out := make([]string, len(w.phases))
	for i, p := range w.phases {
		out[i] = p.name
	}
	return out
}

// advance moves to the next phase, returning false when already at the
// final phase.
func (w *workflowState) advance() bool {
	if w.current >= len(w.phases)-1 {
		return false
	}
	w.current++
	return true
}

// firstEditPhase returns the index of the earliest edit-capable phase, or
// the current index when no phase permits edits.
func (w *workflowState) firstEditPhase() int {
	for i, p := range w.phases {
		if p.allowsEdit {
			return i
		}
	}
	return w.current
}

// workflowHidesEdits reports whether a block-mode workflow currently in a
// non-editing phase should hide editing tools from this session.
func (s *Server) workflowHidesEdits(ctx context.Context) bool {
	sess := s.sessionFor(ctx)
	if sess == nil {
		return false
	}
	sess.mu.Lock()
	defer sess.mu.Unlock()
	wf := sess.workflow
	return wf != nil && wf.mode == workflowModeBlock && !wf.phase().allowsEdit
}

// checkWorkflowGate enforces phase ordering for editing tools. In block
// mode an out-of-phase edit is refused with a structured error; in warn
// mode the workflow auto-advances to the first editing phase and the call
// proceeds. Non-editing tools are never phase-gated.
func (s *Server) checkWorkflowGate(ctx context.Context, toolName string) *mcp.CallToolResult {
	if !editingToolNames[toolName] {
		return nil
	}
	sess := s.sessionFor(ctx)
	if sess == nil {
		return nil
	}
	sess.mu.Lock()
	defer sess.mu.Unlock()
	wf := sess.workflow
	if wf == nil || wf.phase().allowsEdit {
		return nil
	}
	if wf.mode == workflowModeWarn {
		wf.current = wf.firstEditPhase() // auto-advance
		return nil
	}
	return NewStructuredErrorResult(StructuredError{
		ErrorCode: ErrCodeToolOutOfPhase,
		Message: fmt.Sprintf("%q is an editing tool but the active workflow is in the %q phase, which "+
			"does not permit edits. Advance the workflow (workflow action=\"advance\") until it reaches "+
			"an editing phase.", toolName, wf.phase().name),
		Retriable: true,
		Data: map[string]any{
			"tool":          toolName,
			"current_phase": wf.phase().name,
			"phases":        wf.phaseNames(),
			"recovery":      "call the workflow tool with action=advance",
		},
	})
}

// registerWorkflowTool registers the workflow phase-enforcement tool.
func (s *Server) registerWorkflowTool() {
	s.addTool(mcp.NewTool("workflow",
		mcp.WithDescription("Drive a phase-enforcement workflow for this session. Phases run "+
			"explore → implement → verify, and editing tools are gated until the implement phase. "+
			"In \"block\" mode an out-of-phase edit is refused with a structured error and the phase "+
			"must be advanced explicitly; in \"warn\" mode an out-of-phase edit auto-advances the "+
			"phase. Actions: start, advance, status, stop."),
		mcp.WithString("action", mcp.Required(),
			mcp.Description("start | advance | status | stop")),
		mcp.WithString("mode",
			mcp.Description("for action=start: \"block\" (default) or \"warn\"")),
	), s.handleWorkflow)
}

func (s *Server) handleWorkflow(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	action, err := req.RequireString("action")
	if err != nil {
		return mcp.NewToolResultError("action is required (start | advance | status | stop)"), nil
	}
	act := strings.ToLower(strings.TrimSpace(action))
	sess := s.sessionFor(ctx)

	var (
		invalidAction bool
		noWorkflow    bool
	)
	sess.mu.Lock()
	switch act {
	case "start":
		mode := workflowModeBlock
		if strings.ToLower(strings.TrimSpace(req.GetString("mode", ""))) == workflowModeWarn {
			mode = workflowModeWarn
		}
		sess.workflow = &workflowState{phases: defaultWorkflowPhases(), mode: mode}
	case "advance":
		if sess.workflow == nil {
			noWorkflow = true
		} else {
			sess.workflow.advance()
		}
	case "stop":
		sess.workflow = nil
	case "status":
		// reported below
	default:
		invalidAction = true
	}
	// Snapshot under the lock; build the response after releasing it so
	// respondJSONOrTOON's session lookups never re-enter sess.mu.
	payload := map[string]any{"active": false}
	if wf := sess.workflow; wf != nil && !invalidAction {
		payload = map[string]any{
			"active":        true,
			"mode":          wf.mode,
			"current_phase": wf.phase().name,
			"phases":        wf.phaseNames(),
			"edits_allowed": wf.phase().allowsEdit,
		}
	}
	sess.mu.Unlock()

	if invalidAction {
		return NewStructuredErrorResult(StructuredError{
			ErrorCode: ErrCodeInvalidArgument,
			Message:   fmt.Sprintf("unknown action %q — want start, advance, status, or stop", action),
		}), nil
	}
	if noWorkflow {
		return mcp.NewToolResultError("no workflow is active — call workflow with action=start first"), nil
	}
	return s.respondJSONOrTOON(ctx, req, payload)
}
