package chat

import (
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/spinner"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/sst/opencode/internal/app"
	"github.com/sst/opencode/internal/components/dialog"
	"github.com/sst/opencode/internal/layout"
	"github.com/sst/opencode/internal/state"
	"github.com/sst/opencode/internal/styles"
	"github.com/sst/opencode/internal/theme"
	"github.com/sst/opencode/pkg/client"
)

type messagesComponent struct {
	app             *app.App
	width, height   int
	viewport        viewport.Model
	spinner         spinner.Model
	rendering       bool
	attachments     viewport.Model
	showToolResults bool
	cache           *MessageCache
	tail            bool
}
type renderFinishedMsg struct{}
type ToggleToolMessagesMsg struct{}

type MessageKeys struct {
	PageDown     key.Binding
	PageUp       key.Binding
	HalfPageUp   key.Binding
	HalfPageDown key.Binding
}

var messageKeys = MessageKeys{
	PageDown: key.NewBinding(
		key.WithKeys("pgdown"),
		key.WithHelp("f/pgdn", "page down"),
	),
	PageUp: key.NewBinding(
		key.WithKeys("pgup"),
		key.WithHelp("b/pgup", "page up"),
	),
	HalfPageUp: key.NewBinding(
		key.WithKeys("ctrl+u"),
		key.WithHelp("ctrl+u", "½ page up"),
	),
	HalfPageDown: key.NewBinding(
		key.WithKeys("ctrl+d", "ctrl+d"),
		key.WithHelp("ctrl+d", "½ page down"),
	),
}

func (m *messagesComponent) Init() tea.Cmd {
	return tea.Batch(m.viewport.Init(), m.spinner.Tick)
}

func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd
	switch msg := msg.(type) {
	case SendMsg:
		m.viewport.GotoBottom()
		m.tail = true
		return m, nil
	case dialog.ThemeChangedMsg:
		m.cache.Clear()
		m.renderView()
		return m, nil
	case ToggleToolMessagesMsg:
		m.showToolResults = !m.showToolResults
		m.renderView()
		return m, nil
	case state.SessionSelectedMsg:
		m.cache.Clear()
		cmd := m.Reload()
		m.viewport.GotoBottom()
		return m, cmd
	case state.SessionClearedMsg:
		m.cache.Clear()
		cmd := m.Reload()
		return m, cmd
	case tea.KeyMsg:
		if key.Matches(msg, messageKeys.PageUp) ||
			key.Matches(msg, messageKeys.PageDown) ||
			key.Matches(msg, messageKeys.HalfPageUp) ||
			key.Matches(msg, messageKeys.HalfPageDown) {
			u, cmd := m.viewport.Update(msg)
			m.viewport = u
			m.tail = m.viewport.AtBottom()
			cmds = append(cmds, cmd)
		}
	case renderFinishedMsg:
		m.rendering = false
		if m.tail {
			m.viewport.GotoBottom()
		}
	case state.StateUpdatedMsg:
		m.renderView()
		if m.tail {
			m.viewport.GotoBottom()
		}
	}

	spinner, cmd := m.spinner.Update(msg)
	m.spinner = spinner
	cmds = append(cmds, cmd)
	return m, tea.Batch(cmds...)
}

type blockType int

const (
	none blockType = iota
	systemTextBlock
	userTextBlock
	assistantTextBlock
	toolInvocationBlock
)

func (m *messagesComponent) renderView() {
	if m.width == 0 {
		return
	}

	blocks := make([]string, 0)
	previousBlockType := none
	for _, message := range m.app.Messages {
		if message.Role == client.System {
			continue // ignoring system messages for now
		}

		var content string
		var cached bool

		author := ""
		switch message.Role {
		case client.User:
			author = app.Info.User
		case client.Assistant:
			author = message.Metadata.Assistant.ModelID
		}

		for _, p := range message.Parts {
			part, err := p.ValueByDiscriminator()
			if err != nil {
				continue //TODO: handle error?
			}

			switch part.(type) {
			// case client.MessagePartStepStart:
			// 	messages = append(messages, "")
			case client.MessagePartText:
				text := part.(client.MessagePartText)
				key := m.cache.GenerateKey(message.Id, text.Text, layout.Current.Viewport.Width)
				content, cached = m.cache.Get(key)
				if !cached {
					content = renderText(message, text.Text, author)
					m.cache.Set(key, content)
				}
				if previousBlockType != none {
					blocks = append(blocks, "")
				}
				blocks = append(blocks, content)
				if message.Role == client.User {
					previousBlockType = userTextBlock
				} else if message.Role == client.Assistant {
					previousBlockType = assistantTextBlock
				} else if message.Role == client.System {
					previousBlockType = systemTextBlock
				}
			case client.MessagePartToolInvocation:
				toolInvocationPart := part.(client.MessagePartToolInvocation)
				toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
				metadata := map[string]any{}
				if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
					metadata = message.Metadata.Tool[toolCall.ToolCallId].(map[string]any)
				}
				var result *string
				resultPart, resultError := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolResult()
				if resultError == nil {
					result = &resultPart.Result
				}

				if toolCall.State == "result" {
					key := m.cache.GenerateKey(message.Id,
						toolCall.ToolCallId,
						m.showToolResults,
						layout.Current.Viewport.Width,
					)
					content, cached = m.cache.Get(key)
					if !cached {
						content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
						m.cache.Set(key, content)
					}
				} else {
					// if the tool call isn't finished, never cache
					content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
				}

				if previousBlockType != toolInvocationBlock {
					blocks = append(blocks, "")
				}
				blocks = append(blocks, content)
				previousBlockType = toolInvocationBlock
			}
		}
	}

	t := theme.CurrentTheme()
	centered := []string{}
	for _, block := range blocks {
		centered = append(centered, lipgloss.PlaceHorizontal(
			m.width,
			lipgloss.Center,
			block,
			lipgloss.WithWhitespaceBackground(t.Background()),
		))
	}

	m.viewport.Height = m.height - lipgloss.Height(m.header())
	m.viewport.SetContent(strings.Join(centered, "\n"))
}

func (m *messagesComponent) header() string {
	if m.app.Session.Id == "" {
		return ""
	}

	t := theme.CurrentTheme()
	width := layout.Current.Container.Width
	base := styles.BaseStyle().Render
	muted := styles.Muted().Render
	headerLines := []string{}
	headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width))
	if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
		headerLines = append(headerLines, muted(m.app.Session.Share.Url))
	} else {
		headerLines = append(headerLines, base("/share")+muted(" to create a shareable link"))
	}
	header := strings.Join(headerLines, "\n")

	header = styles.BaseStyle().
		Width(width).
		PaddingTop(1).
		BorderBottom(true).
		BorderForeground(t.BorderSubtle()).
		BorderStyle(lipgloss.NormalBorder()).
		Background(t.Background()).
		Render(header)

	return styles.ForceReplaceBackgroundWithLipgloss(header, t.Background())
}

func (m *messagesComponent) View() string {
	if len(m.app.Messages) == 0 || m.rendering {
		return m.home()
	}
	return lipgloss.JoinVertical(
		lipgloss.Left,
		lipgloss.PlaceHorizontal(m.width, lipgloss.Center, m.header()),
		m.viewport.View(),
	)
}

// func hasToolsWithoutResponse(messages []message.Message) bool {
// 	toolCalls := make([]message.ToolCall, 0)
// 	toolResults := make([]message.ToolResult, 0)
// 	for _, m := range messages {
// 		toolCalls = append(toolCalls, m.ToolCalls()...)
// 		toolResults = append(toolResults, m.ToolResults()...)
// 	}
//
// 	for _, v := range toolCalls {
// 		found := false
// 		for _, r := range toolResults {
// 			if v.ID == r.ToolCallID {
// 				found = true
// 				break
// 			}
// 		}
// 		if !found && v.Finished {
// 			return true
// 		}
// 	}
// 	return false
// }

// func hasUnfinishedToolCalls(messages []message.Message) bool {
// 	toolCalls := make([]message.ToolCall, 0)
// 	for _, m := range messages {
// 		toolCalls = append(toolCalls, m.ToolCalls()...)
// 	}
// 	for _, v := range toolCalls {
// 		if !v.Finished {
// 			return true
// 		}
// 	}
// 	return false
// }

func (m *messagesComponent) help() string {
	t := theme.CurrentTheme()
	baseStyle := styles.BaseStyle()

	text := ""

	if m.app.IsBusy() {
		text += lipgloss.JoinHorizontal(
			lipgloss.Left,
			baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
			baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to interrupt"),
		)
	} else {
		text += lipgloss.JoinHorizontal(
			lipgloss.Left,
			baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"),
			baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
			baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"),
			baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline"),
		)
	}
	return baseStyle.
		Width(m.width).
		Render(text)
}

func (m *messagesComponent) home() string {
	t := theme.CurrentTheme()
	baseStyle := styles.BaseStyle()
	base := baseStyle.Render
	muted := styles.Muted().Render

	// 	mark := `
	// ███▀▀█
	// ███  █
	// ▀▀▀▀▀▀  `
	open := `
█▀▀█ █▀▀█ █▀▀ █▀▀▄ 
█░░█ █░░█ █▀▀ █░░█ 
▀▀▀▀ █▀▀▀ ▀▀▀ ▀  ▀ `
	code := `
█▀▀ █▀▀█ █▀▀▄ █▀▀
█░░ █░░█ █░░█ █▀▀
▀▀▀ ▀▀▀▀ ▀▀▀  ▀▀▀`

	logo := lipgloss.JoinHorizontal(
		lipgloss.Top,
		// styles.BaseStyle().Foreground(t.Primary()).Render(mark),
		styles.Muted().Render(open),
		styles.BaseStyle().Render(code),
	)
	cwd := app.Info.Path.Cwd
	config := app.Info.Path.Config

	commands := [][]string{
		{"/help", "show help"},
		{"/sessions", "list sessions"},
		{"/new", "start a new session"},
		{"/model", "switch model"},
		{"/share", "share the current session"},
		{"/exit", "exit the app"},
	}

	commandLines := []string{}
	for _, command := range commands {
		commandLines = append(commandLines, (base(command[0]) + " " + muted(command[1])))
	}

	logoAndVersion := lipgloss.JoinVertical(
		lipgloss.Right,
		logo,
		muted(app.Info.Version),
	)

	lines := []string{}
	lines = append(lines, "")
	lines = append(lines, "")
	lines = append(lines, logoAndVersion)
	lines = append(lines, "")
	lines = append(lines, base("cwd ")+muted(cwd))
	lines = append(lines, base("config ")+muted(config))
	lines = append(lines, "")
	lines = append(lines, commandLines...)
	lines = append(lines, "")
	if m.rendering {
		lines = append(lines, styles.Muted().Render("Loading session..."))
	} else {
		lines = append(lines, "")
	}

	return styles.ForceReplaceBackgroundWithLipgloss(
		lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
			baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
				lipgloss.JoinVertical(
					lipgloss.Top,
					lines...,
				),
			)),
		t.Background(),
	)
}

func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
	if m.width == width && m.height == height {
		return nil
	}
	// Clear cache on resize since width affects rendering
	if m.width != width {
		m.cache.Clear()
	}
	m.width = width
	m.height = height
	m.viewport.Width = width
	m.viewport.Height = height - lipgloss.Height(m.header())
	m.attachments.Width = width + 40
	m.attachments.Height = 3
	m.renderView()
	return nil
}

func (m *messagesComponent) GetSize() (int, int) {
	return m.width, m.height
}

func (m *messagesComponent) Reload() tea.Cmd {
	m.rendering = true
	return func() tea.Msg {
		m.renderView()
		return renderFinishedMsg{}
	}
}

func (m *messagesComponent) BindingKeys() []key.Binding {
	return []key.Binding{
		m.viewport.KeyMap.PageDown,
		m.viewport.KeyMap.PageUp,
		m.viewport.KeyMap.HalfPageUp,
		m.viewport.KeyMap.HalfPageDown,
	}
}

func NewMessagesComponent(app *app.App) tea.Model {
	customSpinner := spinner.Spinner{
		Frames: []string{" ", "┃", "┃"},
		FPS:    time.Second / 3,
	}
	s := spinner.New(spinner.WithSpinner(customSpinner))

	vp := viewport.New(0, 0)
	attachments := viewport.New(0, 0)
	vp.KeyMap.PageUp = messageKeys.PageUp
	vp.KeyMap.PageDown = messageKeys.PageDown
	vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
	vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown

	return &messagesComponent{
		app:             app,
		viewport:        vp,
		spinner:         s,
		attachments:     attachments,
		showToolResults: true,
		cache:           NewMessageCache(),
		tail:            true,
	}
}
