package agents

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"

	"github.com/enmanuel/agents/internal/config"
	"github.com/enmanuel/agents/pkg/command"
	"github.com/enmanuel/agents/pkg/decision"
	coretypes "github.com/enmanuel/agents/pkg/llm"
	"github.com/enmanuel/agents/pkg/personality"
	shelllm "github.com/enmanuel/agents/shell/llm"
)

// runLLM executes the LLM completion loop, including iterative tool-use.
func (a *Agent) runLLM(ctx context.Context, msgCtx decision.MessageContext, memKey string) (string, error) {
	a.logger.Debug("calling LLM",
		"model", a.cfg.LLM.Primary.Model,
		"provider", a.cfg.LLM.Primary.Provider,
	)

	// Load system prompt from file if configured, else use description
	systemPrompt := a.cfg.Agent.Description
	if spFile := a.cfg.LLM.Reasoning.SystemPromptFile; spFile != "" {
		// Resolve path relative to agent directory
		spPath := filepath.Join("agents", a.cfg.Agent.ID, spFile)
		if data, err := os.ReadFile(spPath); err == nil {
			systemPrompt = string(data)
		} else {
			a.logger.Warn("failed to load system_prompt_file, using description", "path", spPath, "err", err)
		}
	}

	// Concatenate personality prompt block
	personalityBlock := personality.BuildPersonalityPrompt(a.personality)
	if personalityBlock != "" {
		systemPrompt = systemPrompt + "\n\n" + personalityBlock
	}

	// Build messages: conversation history from window (includes current user msg)
	messages := a.getWindowMessages(memKey)
	if len(messages) == 0 {
		// Fallback if memory is disabled: just the current message
		messages = []coretypes.Message{
			{Role: coretypes.RoleUser, Content: msgCtx.Content},
		}
	}

	// Build tool specs for the LLM if tool_use is enabled
	var llmTools []coretypes.ToolSpec
	if a.cfg.LLM.ToolUse.Enabled && a.toolReg.Len() > 0 {
		llmTools = a.toolReg.ToLLMSpecs()
		a.logger.Debug("tools available for LLM", "count", len(llmTools))
	}

	maxIter := a.cfg.LLM.ToolUse.MaxIterations
	if maxIter <= 0 {
		maxIter = defaultMaxToolIterations
	}

	// Tool-use loop: call LLM → execute tools → feed results back → repeat
	for i := 0; i < maxIter; i++ {
		req := coretypes.CompletionRequest{
			Model:        a.cfg.LLM.Primary.Model,
			MaxTokens:    a.cfg.LLM.Primary.MaxTokens,
			Temperature:  a.cfg.LLM.Primary.Temperature,
			SystemPrompt: systemPrompt,
			Messages:     messages,
			Tools:        llmTools,
		}

		resp, err := a.llm(ctx, req)
		if err != nil {
			a.logger.Error("LLM call failed", "model", req.Model, "err", err)
			return "", err
		}

		a.logger.Debug("LLM responded",
			"content_len", len(resp.Content),
			"tool_calls", len(resp.ToolCalls),
			"finish_reason", resp.FinishReason,
		)

		// No tool calls — return the text response
		if len(resp.ToolCalls) == 0 {
			return resp.Content, nil
		}

		// Append assistant message with tool calls to conversation
		messages = append(messages, coretypes.Message{
			Role:      coretypes.RoleAssistant,
			Content:   resp.Content,
			ToolCalls: resp.ToolCalls,
		})

		// Execute each tool and append results
		for _, tc := range resp.ToolCalls {
			a.logger.Info("executing tool",
				"tool", tc.Name,
				"call_id", tc.ID,
			)

			// RBAC check for tool execution
			if !a.acl.CanDo(msgCtx.SenderID, "tool:"+tc.Name) {
				a.logger.Info("tool_denied", "tool", tc.Name, "sender", msgCtx.SenderID)
				messages = append(messages, coretypes.Message{
					Role:       coretypes.RoleTool,
					Content:    "error: permission denied for tool " + tc.Name,
					ToolCallID: tc.ID,
				})
				continue
			}

			// Notify the room that a tool is being called (respect thread context)
			toolNotice := fmt.Sprintf("\U0001f528 <em>%s</em>", tc.Name)
			if err := a.sendReply(ctx, msgCtx.RoomID, msgCtx.EventID, msgCtx.ThreadID, toolNotice); err != nil {
				a.logger.Warn("failed to send tool call notice", "tool", tc.Name, "err", err)
			}

			result := a.toolReg.ExecuteForRoom(ctx, tc.Name, tc.Arguments, msgCtx.RoomID)

			output := result.Output
			if result.Err != nil {
				output = fmt.Sprintf("error: %s", result.Err)
				a.logger.Warn("tool execution error",
					"tool", tc.Name,
					"err", result.Err,
				)
			} else {
				a.logger.Debug("tool executed",
					"tool", tc.Name,
					"output_len", len(output),
				)
			}

			messages = append(messages, coretypes.Message{
				Role:       coretypes.RoleTool,
				Content:    output,
				ToolCallID: tc.ID,
			})
		}
	}

	// Max iterations reached — return whatever we have
	a.logger.Warn("tool-use loop reached max iterations", "max", maxIter)
	return "I've reached the maximum number of tool iterations. Here's what I found so far.", nil
}

// initLLM creates the LLM client function with optional fallback.
// Returns nil when no provider is configured (command-only bot).
func initLLM(cfg *config.AgentConfig, logger *slog.Logger) (coretypes.CompleteFunc, error) {
	if cfg.LLM.Primary.Provider == "" {
		logger.Info("no LLM configured, running as command-only bot")
		return nil, nil
	}

	llmLog := logger.With("component", "llm")
	primaryLLM, err := shelllm.FromConfig(cfg.LLM.Primary, llmLog)
	if err != nil {
		return nil, fmt.Errorf("primary LLM: %w", err)
	}

	llmFunc := primaryLLM
	if cfg.LLM.Fallback.Provider != "" {
		fallbackLLM, err := shelllm.FromConfig(cfg.LLM.Fallback, llmLog)
		if err != nil {
			logger.Warn("fallback LLM config error", "err", err)
		} else {
			llmFunc = shelllm.WithFallback(primaryLLM, fallbackLLM, cfg.LLM.Fallback, llmLog)
		}
	}

	return llmFunc, nil
}

// loadPromptCommands scans the project-root prompts/ directory and loads all .md files.
func (a *Agent) loadPromptCommands() {
	prompts, err := command.LoadPromptCommands("prompts")
	if err != nil {
		a.logger.Warn("failed to load prompt-commands", "err", err)
		return
	}
	a.promptCmds = make(map[string]string, len(prompts))
	for _, p := range prompts {
		a.promptCmds[p.Name] = p.Content
	}
	if len(a.promptCmds) > 0 {
		names := make([]string, 0, len(a.promptCmds))
		for n := range a.promptCmds {
			names = append(names, n)
		}
		a.logger.Info("prompt-commands loaded", "count", len(a.promptCmds), "names", names)
	}
}
