package commands

import (
	"fmt"
	"os"
	"slices"
	"strings"

	"charm.land/bubbles/v2/help"
	"charm.land/bubbles/v2/key"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/catwalk/pkg/catwalk"

	"github.com/charmbracelet/crush/internal/agent"
	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
	"github.com/charmbracelet/crush/internal/config"
	"github.com/charmbracelet/crush/internal/csync"
	"github.com/charmbracelet/crush/internal/pubsub"
	"github.com/charmbracelet/crush/internal/tui/components/chat"
	"github.com/charmbracelet/crush/internal/tui/components/core"
	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
	"github.com/charmbracelet/crush/internal/tui/exp/list"
	"github.com/charmbracelet/crush/internal/tui/styles"
	"github.com/charmbracelet/crush/internal/tui/util"
)

const (
	CommandsDialogID dialogs.DialogID = "commands"

	defaultWidth int = 70
)

type commandType uint

func (c commandType) String() string { return []string{"System", "User", "MCP"}[c] }

const (
	SystemCommands commandType = iota
	UserCommands
	MCPPrompts
)

type listModel = list.FilterableList[list.CompletionItem[Command]]

// Command represents a command that can be executed
type Command struct {
	ID          string
	Title       string
	Description string
	Shortcut    string // Optional shortcut for the command
	Handler     func(cmd Command) tea.Cmd
}

// CommandsDialog represents the commands dialog.
type CommandsDialog interface {
	dialogs.DialogModel
}

type commandDialogCmp struct {
	width   int
	wWidth  int // Width of the terminal window
	wHeight int // Height of the terminal window

	commandList  listModel
	keyMap       CommandsDialogKeyMap
	help         help.Model
	selected     commandType           // Selected SystemCommands, UserCommands, or MCPPrompts
	userCommands []Command             // User-defined commands
	mcpPrompts   *csync.Slice[Command] // MCP prompts
	sessionID    string                // Current session ID
}

type (
	SwitchSessionsMsg      struct{}
	NewSessionsMsg         struct{}
	SwitchModelMsg         struct{}
	QuitMsg                struct{}
	OpenFilePickerMsg      struct{}
	ToggleHelpMsg          struct{}
	ToggleCompactModeMsg   struct{}
	ToggleThinkingMsg      struct{}
	OpenReasoningDialogMsg struct{}
	OpenExternalEditorMsg  struct{}
	ToggleYoloModeMsg      struct{}
	CompactMsg             struct {
		SessionID string
	}
)

func NewCommandDialog(sessionID string) CommandsDialog {
	keyMap := DefaultCommandsDialogKeyMap()
	listKeyMap := list.DefaultKeyMap()
	listKeyMap.Down.SetEnabled(false)
	listKeyMap.Up.SetEnabled(false)
	listKeyMap.DownOneItem = keyMap.Next
	listKeyMap.UpOneItem = keyMap.Previous

	t := styles.CurrentTheme()
	inputStyle := t.S().Base.PaddingLeft(1).PaddingBottom(1)
	commandList := list.NewFilterableList(
		[]list.CompletionItem[Command]{},
		list.WithFilterInputStyle(inputStyle),
		list.WithFilterListOptions(
			list.WithKeyMap(listKeyMap),
			list.WithWrapNavigation(),
			list.WithResizeByList(),
		),
	)
	help := help.New()
	help.Styles = t.S().Help
	return &commandDialogCmp{
		commandList: commandList,
		width:       defaultWidth,
		keyMap:      DefaultCommandsDialogKeyMap(),
		help:        help,
		selected:    SystemCommands,
		sessionID:   sessionID,
		mcpPrompts:  csync.NewSlice[Command](),
	}
}

func (c *commandDialogCmp) Init() tea.Cmd {
	commands, err := LoadCustomCommands()
	if err != nil {
		return util.ReportError(err)
	}
	c.userCommands = commands
	c.mcpPrompts.SetSlice(loadMCPPrompts())
	return c.setCommandType(c.selected)
}

func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		c.wWidth = msg.Width
		c.wHeight = msg.Height
		return c, tea.Batch(
			c.setCommandType(c.selected),
			c.commandList.SetSize(c.listWidth(), c.listHeight()),
		)
	case pubsub.Event[mcp.Event]:
		// Reload MCP prompts when MCP state changes
		if msg.Type == pubsub.UpdatedEvent {
			c.mcpPrompts.SetSlice(loadMCPPrompts())
			// If we're currently viewing MCP prompts, refresh the list
			if c.selected == MCPPrompts {
				return c, c.setCommandType(MCPPrompts)
			}
			return c, nil
		}
	case tea.KeyPressMsg:
		switch {
		case key.Matches(msg, c.keyMap.Select):
			selectedItem := c.commandList.SelectedItem()
			if selectedItem == nil {
				return c, nil // No item selected, do nothing
			}
			command := (*selectedItem).Value()
			return c, tea.Sequence(
				util.CmdHandler(dialogs.CloseDialogMsg{}),
				command.Handler(command),
			)
		case key.Matches(msg, c.keyMap.Tab):
			if len(c.userCommands) == 0 && c.mcpPrompts.Len() == 0 {
				return c, nil
			}
			return c, c.setCommandType(c.next())
		case key.Matches(msg, c.keyMap.Close):
			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
		default:
			u, cmd := c.commandList.Update(msg)
			c.commandList = u.(listModel)
			return c, cmd
		}
	}
	return c, nil
}

func (c *commandDialogCmp) next() commandType {
	switch c.selected {
	case SystemCommands:
		if len(c.userCommands) > 0 {
			return UserCommands
		}
		if c.mcpPrompts.Len() > 0 {
			return MCPPrompts
		}
		fallthrough
	case UserCommands:
		if c.mcpPrompts.Len() > 0 {
			return MCPPrompts
		}
		fallthrough
	case MCPPrompts:
		return SystemCommands
	default:
		return SystemCommands
	}
}

func (c *commandDialogCmp) View() string {
	t := styles.CurrentTheme()
	listView := c.commandList
	radio := c.commandTypeRadio()

	header := t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5) + " " + radio)
	if len(c.userCommands) == 0 && c.mcpPrompts.Len() == 0 {
		header = t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-4))
	}
	content := lipgloss.JoinVertical(
		lipgloss.Left,
		header,
		listView.View(),
		"",
		t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
	)
	return c.style().Render(content)
}

func (c *commandDialogCmp) Cursor() *tea.Cursor {
	if cursor, ok := c.commandList.(util.Cursor); ok {
		cursor := cursor.Cursor()
		if cursor != nil {
			cursor = c.moveCursor(cursor)
		}
		return cursor
	}
	return nil
}

func (c *commandDialogCmp) commandTypeRadio() string {
	t := styles.CurrentTheme()

	fn := func(i commandType) string {
		if i == c.selected {
			return "◉ " + i.String()
		}
		return "○ " + i.String()
	}

	parts := []string{
		fn(SystemCommands),
	}
	if len(c.userCommands) > 0 {
		parts = append(parts, fn(UserCommands))
	}
	if c.mcpPrompts.Len() > 0 {
		parts = append(parts, fn(MCPPrompts))
	}
	return t.S().Base.Foreground(t.FgHalfMuted).Render(strings.Join(parts, " "))
}

func (c *commandDialogCmp) listWidth() int {
	return defaultWidth - 2 // 4 for padding
}

func (c *commandDialogCmp) setCommandType(commandType commandType) tea.Cmd {
	c.selected = commandType

	var commands []Command
	switch c.selected {
	case SystemCommands:
		commands = c.defaultCommands()
	case UserCommands:
		commands = c.userCommands
	case MCPPrompts:
		commands = slices.Collect(c.mcpPrompts.Seq())
	}

	commandItems := []list.CompletionItem[Command]{}
	for _, cmd := range commands {
		opts := []list.CompletionItemOption{
			list.WithCompletionID(cmd.ID),
		}
		if cmd.Shortcut != "" {
			opts = append(
				opts,
				list.WithCompletionShortcut(cmd.Shortcut),
			)
		}
		commandItems = append(commandItems, list.NewCompletionItem(cmd.Title, cmd, opts...))
	}
	return c.commandList.SetItems(commandItems)
}

func (c *commandDialogCmp) listHeight() int {
	listHeigh := len(c.commandList.Items()) + 2 + 4 // height based on items + 2 for the input + 4 for the sections
	return min(listHeigh, c.wHeight/2)
}

func (c *commandDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
	row, col := c.Position()
	offset := row + 3
	cursor.Y += offset
	cursor.X = cursor.X + col + 2
	return cursor
}

func (c *commandDialogCmp) style() lipgloss.Style {
	t := styles.CurrentTheme()
	return t.S().Base.
		Width(c.width).
		Border(lipgloss.RoundedBorder()).
		BorderForeground(t.BorderFocus)
}

func (c *commandDialogCmp) Position() (int, int) {
	row := c.wHeight/4 - 2 // just a bit above the center
	col := c.wWidth / 2
	col -= c.width / 2
	return row, col
}

func (c *commandDialogCmp) defaultCommands() []Command {
	commands := []Command{
		{
			ID:          "new_session",
			Title:       "New Session",
			Description: "start a new session",
			Shortcut:    "ctrl+n",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(NewSessionsMsg{})
			},
		},
		{
			ID:          "switch_session",
			Title:       "Switch Session",
			Description: "Switch to a different session",
			Shortcut:    "ctrl+s",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(SwitchSessionsMsg{})
			},
		},
		{
			ID:          "switch_model",
			Title:       "Switch Model",
			Description: "Switch to a different model",
			Shortcut:    "ctrl+l",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(SwitchModelMsg{})
			},
		},
	}

	// Only show compact command if there's an active session
	if c.sessionID != "" {
		commands = append(commands, Command{
			ID:          "Summarize",
			Title:       "Summarize Session",
			Description: "Summarize the current session and create a new one with the summary",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(CompactMsg{
					SessionID: c.sessionID,
				})
			},
		})
	}

	// Add reasoning toggle for models that support it
	cfg := config.Get()
	if agentCfg, ok := cfg.Agents[config.AgentCoder]; ok {
		providerCfg := cfg.GetProviderForModel(agentCfg.Model)
		model := cfg.GetModelByType(agentCfg.Model)
		if providerCfg != nil && model != nil && model.CanReason {
			selectedModel := cfg.Models[agentCfg.Model]

			// Anthropic models: thinking toggle
			if providerCfg.Type == catwalk.TypeAnthropic {
				status := "Enable"
				if selectedModel.Think {
					status = "Disable"
				}
				commands = append(commands, Command{
					ID:          "toggle_thinking",
					Title:       status + " Thinking Mode",
					Description: "Toggle model thinking for reasoning-capable models",
					Handler: func(cmd Command) tea.Cmd {
						return util.CmdHandler(ToggleThinkingMsg{})
					},
				})
			}

			// OpenAI models: reasoning effort dialog
			if len(model.ReasoningLevels) > 0 {
				commands = append(commands, Command{
					ID:          "select_reasoning_effort",
					Title:       "Select Reasoning Effort",
					Description: "Choose reasoning effort level (low/medium/high)",
					Handler: func(cmd Command) tea.Cmd {
						return util.CmdHandler(OpenReasoningDialogMsg{})
					},
				})
			}
		}
	}
	// Only show toggle compact mode command if window width is larger than compact breakpoint (90)
	if c.wWidth > 120 && c.sessionID != "" {
		commands = append(commands, Command{
			ID:          "toggle_sidebar",
			Title:       "Toggle Sidebar",
			Description: "Toggle between compact and normal layout",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(ToggleCompactModeMsg{})
			},
		})
	}
	if c.sessionID != "" {
		agentCfg := config.Get().Agents[config.AgentCoder]
		model := config.Get().GetModelByType(agentCfg.Model)
		if model.SupportsImages {
			commands = append(commands, Command{
				ID:          "file_picker",
				Title:       "Open File Picker",
				Shortcut:    "ctrl+f",
				Description: "Open file picker",
				Handler: func(cmd Command) tea.Cmd {
					return util.CmdHandler(OpenFilePickerMsg{})
				},
			})
		}
	}

	// Add external editor command if $EDITOR is available
	if os.Getenv("EDITOR") != "" {
		commands = append(commands, Command{
			ID:          "open_external_editor",
			Title:       "Open External Editor",
			Shortcut:    "ctrl+o",
			Description: "Open external editor to compose message",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(OpenExternalEditorMsg{})
			},
		})
	}

	return append(commands, []Command{
		{
			ID:          "toggle_yolo",
			Title:       "Toggle Yolo Mode",
			Description: "Toggle yolo mode",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(ToggleYoloModeMsg{})
			},
		},
		{
			ID:          "toggle_help",
			Title:       "Toggle Help",
			Shortcut:    "ctrl+g",
			Description: "Toggle help",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(ToggleHelpMsg{})
			},
		},
		{
			ID:          "init",
			Title:       "Initialize Project",
			Description: fmt.Sprintf("Create/Update the %s memory file", config.Get().Options.InitializeAs),
			Handler: func(cmd Command) tea.Cmd {
				initPrompt, err := agent.InitializePrompt(*config.Get())
				if err != nil {
					return util.ReportError(err)
				}
				return util.CmdHandler(chat.SendMsg{
					Text: initPrompt,
				})
			},
		},
		{
			ID:          "quit",
			Title:       "Quit",
			Description: "Quit",
			Shortcut:    "ctrl+c",
			Handler: func(cmd Command) tea.Cmd {
				return util.CmdHandler(QuitMsg{})
			},
		},
	}...)
}

func (c *commandDialogCmp) ID() dialogs.DialogID {
	return CommandsDialogID
}
