package standard

import (
	"fmt"
	"time"

	"github.com/yaoapp/gou/text"
	kunlog "github.com/yaoapp/kun/log"
	"github.com/yaoapp/yao/agent/assistant"
	agentcontext "github.com/yaoapp/yao/agent/context"
	"github.com/yaoapp/yao/agent/output/message"
	robottypes "github.com/yaoapp/yao/agent/robot/types"
	oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
)

// StreamCallback receives text chunks during streaming agent calls.
// Return 0 to continue, non-zero to stop.
type StreamCallback func(chunk *StreamChunk) int

// StreamChunk represents a single chunk in a streaming response.
type StreamChunk struct {
	Type    string // "text", "thinking", "event"
	Content string
	Delta   bool
}

// AgentCaller provides unified interface for calling AI assistants
// It wraps the Yao Assistant framework and handles:
// - Getting assistant by ID
// - Single call with messages (streaming)
// - Multi-turn conversation with session state
// - Parsing responses (text, JSON, Next hook data)
type AgentCaller struct {
	// SkipOutput skips sending output to client (for internal calls)
	SkipOutput bool

	// SkipHistory skips saving to chat history (default: true for robot)
	// Set to false to enable multi-turn conversation with history
	SkipHistory bool

	// SkipSearch skips auto search
	SkipSearch bool

	// ChatID is used for multi-turn conversations to maintain session state
	// If empty, each call is independent (no history)
	ChatID string

	// Connector overrides the assistant's default LLM connector (from Robot.LanguageModel).
	// When non-empty, passed as opts.Connector to ast.Stream so the agent uses the Robot's model.
	Connector string

	// Workspace is the workspace ID bound to the Robot.
	// When non-empty, injected into agentCtx.Metadata["workspace_id"] for sandbox node resolution.
	Workspace string

	// log is an optional structured logger; when set, Call emits agent-call logs.
	log *execLogger
}

// NewAgentCaller creates a new AgentCaller with default settings (single-call mode)
func NewAgentCaller() *AgentCaller {
	return &AgentCaller{
		SkipOutput:  true, // Robot executions don't send to UI
		SkipHistory: true, // Robot executions don't save to chat history
		SkipSearch:  true, // Robot executions don't trigger auto search
	}
}

// NewConversationCaller creates an AgentCaller for multi-turn conversations
// chatID is used to maintain session state across calls
// This is useful for:
// - P2 (Tasks): Iterative task refinement with user feedback
// - P3 (Run): Multi-step task execution with intermediate results
func NewConversationCaller(chatID string) *AgentCaller {
	return &AgentCaller{
		SkipOutput:  true,
		SkipHistory: false, // Enable history for multi-turn
		SkipSearch:  true,
		ChatID:      chatID,
	}
}

// CallResult holds the result of an agent call
type CallResult struct {
	// Content is the raw text content from LLM completion
	Content string

	// Next is the data returned from Next hook (if any)
	// This is typically a structured response from the assistant
	Next interface{}

	// Response is the full response object (for advanced use)
	Response *agentcontext.Response
}

// IsEmpty returns true if the result has no content
func (r *CallResult) IsEmpty() bool {
	return r.Content == "" && r.Next == nil
}

// GetText returns the text content, preferring Content over Next
func (r *CallResult) GetText() string {
	if r.Content != "" {
		return r.Content
	}
	if s, ok := r.Next.(string); ok {
		return s
	}
	if m, ok := r.Next.(map[string]interface{}); ok {
		if content, ok := m["content"].(string); ok {
			return content
		}
		if data, ok := m["data"].(map[string]interface{}); ok {
			if content, ok := data["content"].(string); ok {
				return content
			}
		}
	}
	return ""
}

// GetJSON attempts to parse the result as JSON
// It tries in order:
// 1. Next hook data (already structured)
// 2. Content parsed using gou/text.ExtractJSON (fault-tolerant)
// Returns the parsed data and any error
func (r *CallResult) GetJSON() (map[string]interface{}, error) {
	if r.Next != nil {
		if m, ok := r.Next.(map[string]interface{}); ok {
			if data, ok := m["data"].(map[string]interface{}); ok {
				return data, nil
			}
			return m, nil
		}
	}

	if r.Content != "" {
		data := text.ExtractJSON(r.Content)
		if data != nil {
			if m, ok := data.(map[string]interface{}); ok {
				return m, nil
			}
		}
		return nil, fmt.Errorf("content is not a JSON object")
	}

	return nil, fmt.Errorf("no content to parse")
}

// GetJSONArray attempts to parse the result as JSON array
// Similar to GetJSON but for array responses
func (r *CallResult) GetJSONArray() ([]interface{}, error) {
	// Try Next hook data first
	if r.Next != nil {
		if arr, ok := r.Next.([]interface{}); ok {
			return arr, nil
		}
		if m, ok := r.Next.(map[string]interface{}); ok {
			// Check for "data" wrapper
			if data, ok := m["data"].([]interface{}); ok {
				return data, nil
			}
		}
	}

	// Try parsing Content using gou/text (handles markdown blocks, JSON, YAML)
	if r.Content != "" {
		data := text.ExtractJSON(r.Content)
		if data != nil {
			if arr, ok := data.([]interface{}); ok {
				return arr, nil
			}
		}
		return nil, fmt.Errorf("content is not a JSON array")
	}

	return nil, fmt.Errorf("no content to parse")
}

// Call calls an assistant with messages and returns the result
// This is the main entry point for agent calls
func (c *AgentCaller) Call(ctx *robottypes.Context, assistantID string, messages []agentcontext.Message) (*CallResult, error) {
	// Get assistant
	ast, err := assistant.Get(assistantID)
	if err != nil {
		return nil, fmt.Errorf("assistant not found: %s: %w", assistantID, err)
	}

	// Build options
	opts := &agentcontext.Options{
		Skip: &agentcontext.Skip{
			Output:  c.SkipOutput,
			History: c.SkipHistory,
			Search:  c.SkipSearch,
		},
		Connector: c.Connector,
	}

	agentCtx := c.buildAgentContext(ctx, assistantID)
	defer func() {
		kunlog.Trace("[robot-agent] releasing context: assistantID=%s chatID=%s", assistantID, c.ChatID)
		agentCtx.Release()
	}()

	callStart := time.Now()
	kunlog.Trace("[robot-agent] Call started: assistantID=%s chatID=%s", assistantID, c.ChatID)

	response, err := ast.Stream(agentCtx, messages, opts)
	if err != nil {
		kunlog.Trace("[robot-agent] Call failed: assistantID=%s elapsed=%v err=%v", assistantID, time.Since(callStart).Round(time.Second), err)
		return nil, fmt.Errorf("assistant call failed: %w", err)
	}

	kunlog.Trace("[robot-agent] Call completed: assistantID=%s elapsed=%v", assistantID, time.Since(callStart).Round(time.Second))

	result := &CallResult{
		Response: response,
	}

	if response.Next != nil {
		result.Next = response.Next
	}

	if response.Completion != nil {
		if content, ok := response.Completion.Content.(string); ok {
			result.Content = content
		}
	}

	if c.log != nil {
		c.log.logAgentCall(assistantID, result)
	}

	return result, nil
}

// CallWithMessages is a convenience method that builds messages from a single user input
func (c *AgentCaller) CallWithMessages(ctx *robottypes.Context, assistantID string, userContent string) (*CallResult, error) {
	messages := []agentcontext.Message{
		{
			Role:    agentcontext.RoleUser,
			Content: userContent,
		},
	}
	return c.Call(ctx, assistantID, messages)
}

// CallWithSystemAndUser calls with both system and user messages
func (c *AgentCaller) CallWithSystemAndUser(ctx *robottypes.Context, assistantID string, systemContent, userContent string) (*CallResult, error) {
	messages := []agentcontext.Message{
		{
			Role:    agentcontext.RoleSystem,
			Content: systemContent,
		},
		{
			Role:    agentcontext.RoleUser,
			Content: userContent,
		},
	}
	return c.Call(ctx, assistantID, messages)
}

// CallStream calls an assistant with messages and streams text chunks via callback.
// The callback receives each text delta in real-time while the response is being generated.
// After streaming completes, the full CallResult is returned.
func (c *AgentCaller) CallStream(ctx *robottypes.Context, assistantID string, messages []agentcontext.Message, streamFn StreamCallback) (*CallResult, error) {
	ast, err := assistant.Get(assistantID)
	if err != nil {
		return nil, fmt.Errorf("assistant not found: %s: %w", assistantID, err)
	}

	opts := &agentcontext.Options{
		Skip: &agentcontext.Skip{
			Output:  c.SkipOutput,
			History: c.SkipHistory,
			Search:  c.SkipSearch,
		},
		Connector: c.Connector,
	}

	// Hook OnMessage to intercept streaming chunks and forward to callback
	if streamFn != nil {
		opts.OnMessage = func(msg *message.Message) int {
			if msg == nil {
				return 0
			}
			switch msg.Type {
			case message.TypeText:
				if msg.Delta {
					content, _ := msg.Props["content"].(string)
					if content != "" {
						return streamFn(&StreamChunk{Type: "text", Content: content, Delta: true})
					}
				}
			case message.TypeThinking:
				if msg.Delta {
					content, _ := msg.Props["content"].(string)
					if content != "" {
						return streamFn(&StreamChunk{Type: "thinking", Content: content, Delta: true})
					}
				}
			}
			return 0
		}
	}

	agentCtx := c.buildAgentContext(ctx, assistantID)
	defer func() {
		kunlog.Trace("[robot-agent] releasing context (CallStream): assistantID=%s chatID=%s", assistantID, c.ChatID)
		agentCtx.Release()
	}()

	callStart := time.Now()
	kunlog.Trace("[robot-agent] CallStream started: assistantID=%s chatID=%s", assistantID, c.ChatID)

	response, err := ast.Stream(agentCtx, messages, opts)
	if err != nil {
		kunlog.Trace("[robot-agent] CallStream failed: assistantID=%s elapsed=%v err=%v", assistantID, time.Since(callStart).Round(time.Second), err)
		return nil, fmt.Errorf("assistant call failed: %w", err)
	}

	kunlog.Trace("[robot-agent] CallStream completed: assistantID=%s elapsed=%v", assistantID, time.Since(callStart).Round(time.Second))

	result := &CallResult{Response: response}
	if response.Next != nil {
		result.Next = response.Next
	}
	if response.Completion != nil {
		if content, ok := response.Completion.Content.(string); ok {
			result.Content = content
		}
	}

	if c.log != nil {
		c.log.logAgentCall(assistantID, result)
	}

	return result, nil
}

// CallWithMessagesStream is a convenience method that streams a single user input.
func (c *AgentCaller) CallWithMessagesStream(ctx *robottypes.Context, assistantID string, userContent string, streamFn StreamCallback) (*CallResult, error) {
	messages := []agentcontext.Message{
		{
			Role:    agentcontext.RoleUser,
			Content: userContent,
		},
	}
	return c.CallStream(ctx, assistantID, messages, streamFn)
}

// CallStreamRaw calls an assistant with streaming, passing raw message.Message objects
// to the callback without any type filtering or degradation. This preserves all CUI
// message protocol fields (chunk_id, message_id, block_id, delta_path, etc.)
// for direct SSE passthrough to the frontend.
func (c *AgentCaller) CallStreamRaw(ctx *robottypes.Context, assistantID string, messages []agentcontext.Message, onMessage agentcontext.OnMessageFunc) (*CallResult, error) {
	ast, err := assistant.Get(assistantID)
	if err != nil {
		return nil, fmt.Errorf("assistant not found: %s: %w", assistantID, err)
	}

	opts := &agentcontext.Options{
		Skip: &agentcontext.Skip{
			Output:  c.SkipOutput,
			History: c.SkipHistory,
			Search:  c.SkipSearch,
		},
		Connector: c.Connector,
	}

	if onMessage != nil {
		opts.OnMessage = onMessage
	}

	agentCtx := c.buildAgentContext(ctx, assistantID)
	defer func() {
		kunlog.Trace("[robot-agent] releasing context (CallStreamRaw): assistantID=%s chatID=%s", assistantID, c.ChatID)
		agentCtx.Release()
	}()

	callStart := time.Now()
	kunlog.Trace("[robot-agent] CallStreamRaw started: assistantID=%s chatID=%s", assistantID, c.ChatID)

	response, err := ast.Stream(agentCtx, messages, opts)
	if err != nil {
		kunlog.Trace("[robot-agent] CallStreamRaw failed: assistantID=%s elapsed=%v err=%v", assistantID, time.Since(callStart).Round(time.Second), err)
		return nil, fmt.Errorf("assistant call failed: %w", err)
	}

	kunlog.Trace("[robot-agent] CallStreamRaw completed: assistantID=%s elapsed=%v", assistantID, time.Since(callStart).Round(time.Second))

	result := &CallResult{Response: response}
	if response.Next != nil {
		result.Next = response.Next
	}
	if response.Completion != nil {
		if content, ok := response.Completion.Content.(string); ok {
			result.Content = content
		}
	}

	if c.log != nil {
		c.log.logAgentCall(assistantID, result)
	}

	return result, nil
}

// CallWithMessagesStreamRaw is a convenience method that streams raw messages for a single user input.
func (c *AgentCaller) CallWithMessagesStreamRaw(ctx *robottypes.Context, assistantID string, userContent string, onMessage agentcontext.OnMessageFunc) (*CallResult, error) {
	messages := []agentcontext.Message{
		{
			Role:    agentcontext.RoleUser,
			Content: userContent,
		},
	}
	return c.CallStreamRaw(ctx, assistantID, messages, onMessage)
}

// buildAgentContext converts robot context to agent context
func (c *AgentCaller) buildAgentContext(ctx *robottypes.Context, assistantID string) *agentcontext.Context {
	// Build authorized info for agent context
	var authorized *oauthtypes.AuthorizedInfo
	if ctx.Auth != nil {
		authorized = &oauthtypes.AuthorizedInfo{
			UserID: ctx.Auth.UserID,
			TeamID: ctx.Auth.TeamID,
		}
	}

	// Create a new agent context
	// Use ChatID for multi-turn conversations, empty for single calls
	agentCtx := agentcontext.New(ctx.Context, authorized, c.ChatID)
	agentCtx.AssistantID = assistantID

	// Propagate locale to agent context; fall back to "en" so that
	// i18n.Tr / buildBoxDisplayName always resolve {{name}} templates.
	if ctx.Locale != "" {
		agentCtx.Locale = ctx.Locale
	} else {
		agentCtx.Locale = "en"
	}

	// Use noop logger to suppress LLM debug output for robot executions
	// Robot executions run in background and don't need console output
	if agentCtx.Logger != nil {
		agentCtx.Logger.Close()
	}
	agentCtx.Logger = agentcontext.Noop()

	if c.Workspace != "" {
		if agentCtx.Metadata == nil {
			agentCtx.Metadata = map[string]interface{}{}
		}
		agentCtx.Metadata["workspace_id"] = c.Workspace
	}

	kunlog.Trace("[robot-agent] context built: assistantID=%s chatID=%s contextID=%s", assistantID, c.ChatID, agentCtx.ID)
	return agentCtx
}

// ExtractCodeBlock extracts the first code block from content using gou/text
// Returns the CodeBlock with type, content, and parsed data (for JSON/YAML)
func ExtractCodeBlock(content string) *text.CodeBlock {
	return text.ExtractFirst(content)
}

// ExtractAllCodeBlocks extracts all code blocks from content using gou/text
func ExtractAllCodeBlocks(content string) []text.CodeBlock {
	return text.Extract(content)
}

// ============================================================================
// Conversation - Multi-turn dialogue support
// ============================================================================

// Conversation manages a multi-turn dialogue with an assistant
// Useful for:
// - P2 (Tasks): Iterative task planning with clarification
// - P3 (Run): Multi-step execution with intermediate validation
// - Complex reasoning that requires back-and-forth
type Conversation struct {
	caller      *AgentCaller
	assistantID string
	messages    []agentcontext.Message
	maxTurns    int
}

// TurnResult holds the result of a single conversation turn
type TurnResult struct {
	Turn     int                    // Turn number (1-based)
	Input    string                 // User input for this turn
	Result   *CallResult            // Agent response
	Messages []agentcontext.Message // Full message history after this turn
}

// NewConversation creates a new multi-turn conversation
// assistantID: the assistant to converse with
// chatID: session ID for maintaining state (use exec.ID for robot executions)
// maxTurns: maximum number of turns (0 = unlimited)
func NewConversation(assistantID, chatID string, maxTurns int) *Conversation {
	return &Conversation{
		caller:      NewConversationCaller(chatID),
		assistantID: assistantID,
		messages:    make([]agentcontext.Message, 0),
		maxTurns:    maxTurns,
	}
}

// WithCaller sets a custom AgentCaller for the conversation
// Useful for customizing SkipSearch or other options
func (c *Conversation) WithCaller(caller *AgentCaller) *Conversation {
	c.caller = caller
	return c
}

// WithSystemPrompt adds a system prompt at the beginning of the conversation
func (c *Conversation) WithSystemPrompt(systemPrompt string) *Conversation {
	if systemPrompt != "" {
		c.messages = append([]agentcontext.Message{{
			Role:    agentcontext.RoleSystem,
			Content: systemPrompt,
		}}, c.messages...)
	}
	return c
}

// WithHistory initializes the conversation with existing message history
// Note: Message structs are copied, but Content (interface{}) is a shallow copy
func (c *Conversation) WithHistory(messages []agentcontext.Message) *Conversation {
	c.messages = append(c.messages, messages...)
	return c
}

// Turn executes a single turn in the conversation
// userInput: the user's message for this turn
// Returns the turn result with agent response
func (c *Conversation) Turn(ctx *robottypes.Context, userInput string) (*TurnResult, error) {
	// Check max turns
	turnNum := c.TurnCount() + 1
	if c.maxTurns > 0 && turnNum > c.maxTurns {
		return nil, fmt.Errorf("max turns (%d) exceeded", c.maxTurns)
	}

	// Build messages with user input (don't modify history yet)
	userMsg := agentcontext.Message{
		Role:    agentcontext.RoleUser,
		Content: userInput,
	}
	// Create a new slice to avoid modifying c.messages if capacity allows append in-place
	messagesWithInput := make([]agentcontext.Message, len(c.messages)+1)
	copy(messagesWithInput, c.messages)
	messagesWithInput[len(c.messages)] = userMsg

	// Call assistant with full history
	result, err := c.caller.Call(ctx, c.assistantID, messagesWithInput)
	if err != nil {
		return nil, fmt.Errorf("turn %d failed: %w", turnNum, err)
	}

	// Only update history after successful call
	c.messages = append(c.messages, userMsg)

	// Add assistant response to history
	if result.Content != "" {
		c.messages = append(c.messages, agentcontext.Message{
			Role:    agentcontext.RoleAssistant,
			Content: result.Content,
		})
	}

	// Return a copy of messages to prevent external modification
	messagesCopy := make([]agentcontext.Message, len(c.messages))
	copy(messagesCopy, c.messages)

	return &TurnResult{
		Turn:     turnNum,
		Input:    userInput,
		Result:   result,
		Messages: messagesCopy,
	}, nil
}

// TurnCount returns the number of user turns so far
func (c *Conversation) TurnCount() int {
	count := 0
	for _, msg := range c.messages {
		if msg.Role == agentcontext.RoleUser {
			count++
		}
	}
	return count
}

// Messages returns a copy of the current message history
func (c *Conversation) Messages() []agentcontext.Message {
	messagesCopy := make([]agentcontext.Message, len(c.messages))
	copy(messagesCopy, c.messages)
	return messagesCopy
}

// LastResponse returns a copy of the last assistant response, or nil if none
func (c *Conversation) LastResponse() *agentcontext.Message {
	for i := len(c.messages) - 1; i >= 0; i-- {
		if c.messages[i].Role == agentcontext.RoleAssistant {
			// Return a copy to prevent external modification
			msg := c.messages[i]
			return &msg
		}
	}
	return nil
}

// Reset clears the conversation history (keeps system prompt if any)
func (c *Conversation) Reset() {
	// Keep system prompt if present
	var systemPrompt *agentcontext.Message
	if len(c.messages) > 0 && c.messages[0].Role == agentcontext.RoleSystem {
		systemPrompt = &c.messages[0]
	}

	c.messages = make([]agentcontext.Message, 0)
	if systemPrompt != nil {
		c.messages = append(c.messages, *systemPrompt)
	}
}

// RunUntil runs the conversation until a condition is met
// checkFn: called after each turn, returns (done, error)
// Returns all turn results
func (c *Conversation) RunUntil(
	ctx *robottypes.Context,
	inputFn func(turn int, lastResult *CallResult) (string, error),
	checkFn func(turn int, result *CallResult) (done bool, err error),
) ([]*TurnResult, error) {
	var results []*TurnResult

	for {
		turnNum := c.TurnCount() + 1

		// Check max turns
		if c.maxTurns > 0 && turnNum > c.maxTurns {
			return results, fmt.Errorf("max turns (%d) exceeded without completion", c.maxTurns)
		}

		// Get input for this turn
		var lastResult *CallResult
		if len(results) > 0 {
			lastResult = results[len(results)-1].Result
		}

		input, err := inputFn(turnNum, lastResult)
		if err != nil {
			return results, fmt.Errorf("input generation failed at turn %d: %w", turnNum, err)
		}

		// Execute turn
		turnResult, err := c.Turn(ctx, input)
		if err != nil {
			return results, err
		}
		results = append(results, turnResult)

		// Check completion condition
		done, err := checkFn(turnNum, turnResult.Result)
		if err != nil {
			return results, fmt.Errorf("check failed at turn %d: %w", turnNum, err)
		}
		if done {
			return results, nil
		}
	}
}
