package model

import (
	"fmt"
	"path/filepath"
	"strings"
	"time"

	"charm.land/lipgloss/v2"
	uv "github.com/charmbracelet/ultraviolet"
	"github.com/charmbracelet/x/ansi"

	"github.com/charmbracelet/crush/internal/config"
	"github.com/charmbracelet/crush/internal/message"
	"github.com/charmbracelet/crush/internal/ui/common"
	"github.com/charmbracelet/crush/internal/ui/lazylist"
	"github.com/charmbracelet/crush/internal/ui/list"
	"github.com/charmbracelet/crush/internal/ui/styles"
	"github.com/charmbracelet/crush/internal/ui/toolrender"
)

// Identifiable is an interface for items that can provide a unique identifier.
type Identifiable interface {
	ID() string
}

// MessageItem represents a [message.Message] item that can be displayed in the
// UI and be part of a [list.List] identifiable by a unique ID.
type MessageItem interface {
	list.Item
	list.Focusable
	list.Highlightable
	lazylist.Item
	Identifiable
}

// MessageContentItem represents rendered message content (text, markdown, errors, etc).
type MessageContentItem struct {
	list.BaseFocusable
	list.BaseHighlightable
	id         string
	content    string
	isMarkdown bool
	maxWidth   int
	cache      map[int]string // Cache for rendered content at different widths
	sty        *styles.Styles
}

// NewMessageContentItem creates a new message content item.
func NewMessageContentItem(id, content string, isMarkdown bool, sty *styles.Styles) *MessageContentItem {
	m := &MessageContentItem{
		id:         id,
		content:    content,
		isMarkdown: isMarkdown,
		maxWidth:   120,
		cache:      make(map[int]string),
		sty:        sty,
	}
	m.InitHighlight()
	return m
}

// ID implements Identifiable.
func (m *MessageContentItem) ID() string {
	return m.id
}

// Height implements list.Item.
func (m *MessageContentItem) Height(width int) int {
	// Calculate content width accounting for frame size
	contentWidth := width
	if style := m.CurrentStyle(); style != nil {
		contentWidth -= style.GetHorizontalFrameSize()
	}

	rendered := m.render(contentWidth)

	// Apply focus/blur styling if configured to get accurate height
	if style := m.CurrentStyle(); style != nil {
		rendered = style.Render(rendered)
	}

	return strings.Count(rendered, "\n") + 1
}

// Draw implements list.Item.
func (m *MessageContentItem) Draw(scr uv.Screen, area uv.Rectangle) {
	width := area.Dx()
	height := area.Dy()

	// Calculate content width accounting for frame size
	contentWidth := width
	style := m.CurrentStyle()
	if style != nil {
		contentWidth -= style.GetHorizontalFrameSize()
	}

	rendered := m.render(contentWidth)

	// Apply focus/blur styling if configured
	if style != nil {
		rendered = style.Render(rendered)
	}

	// Create temp buffer to draw content with highlighting
	tempBuf := uv.NewScreenBuffer(width, height)

	// Draw the rendered content to temp buffer
	styled := uv.NewStyledString(rendered)
	styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))

	// Apply highlighting if active
	m.ApplyHighlight(&tempBuf, width, height, style)

	// Copy temp buffer to actual screen at the target area
	tempBuf.Draw(scr, area)
}

// Render implements lazylist.Item.
func (m *MessageContentItem) Render(width int) string {
	return m.render(width)
}

// render renders the content at the given width, using cache if available.
func (m *MessageContentItem) render(width int) string {
	// Cap width to maxWidth for markdown
	cappedWidth := width
	if m.isMarkdown {
		cappedWidth = min(width, m.maxWidth)
	}

	// Check cache first
	if cached, ok := m.cache[cappedWidth]; ok {
		return cached
	}

	// Not cached - render now
	var rendered string
	if m.isMarkdown {
		renderer := common.MarkdownRenderer(m.sty, cappedWidth)
		result, err := renderer.Render(m.content)
		if err != nil {
			rendered = m.content
		} else {
			rendered = strings.TrimSuffix(result, "\n")
		}
	} else {
		rendered = m.content
	}

	// Cache the result
	m.cache[cappedWidth] = rendered
	return rendered
}

// SetHighlight implements list.Highlightable and extends BaseHighlightable.
func (m *MessageContentItem) SetHighlight(startLine, startCol, endLine, endCol int) {
	m.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
	// Clear cache when highlight changes
	m.cache = make(map[int]string)
}

// ToolCallItem represents a rendered tool call with its header and content.
type ToolCallItem struct {
	list.BaseFocusable
	list.BaseHighlightable
	id         string
	toolCall   message.ToolCall
	toolResult message.ToolResult
	cancelled  bool
	isNested   bool
	maxWidth   int
	cache      map[int]cachedToolRender // Cache for rendered content at different widths
	cacheKey   string                   // Key to invalidate cache when content changes
	sty        *styles.Styles
}

// cachedToolRender stores both the rendered string and its height.
type cachedToolRender struct {
	content string
	height  int
}

// NewToolCallItem creates a new tool call item.
func NewToolCallItem(id string, toolCall message.ToolCall, toolResult message.ToolResult, cancelled bool, isNested bool, sty *styles.Styles) *ToolCallItem {
	t := &ToolCallItem{
		id:         id,
		toolCall:   toolCall,
		toolResult: toolResult,
		cancelled:  cancelled,
		isNested:   isNested,
		maxWidth:   120,
		cache:      make(map[int]cachedToolRender),
		cacheKey:   generateCacheKey(toolCall, toolResult, cancelled),
		sty:        sty,
	}
	t.InitHighlight()
	return t
}

// generateCacheKey creates a key that changes when tool call content changes.
func generateCacheKey(toolCall message.ToolCall, toolResult message.ToolResult, cancelled bool) string {
	// Simple key based on result state - when result arrives or changes, key changes
	return fmt.Sprintf("%s:%s:%v", toolCall.ID, toolResult.ToolCallID, cancelled)
}

// ID implements Identifiable.
func (t *ToolCallItem) ID() string {
	return t.id
}

// Height implements list.Item.
func (t *ToolCallItem) Height(width int) int {
	// Calculate content width accounting for frame size
	contentWidth := width
	frameSize := 0
	if style := t.CurrentStyle(); style != nil {
		frameSize = style.GetHorizontalFrameSize()
		contentWidth -= frameSize
	}

	cached := t.renderCached(contentWidth)

	// Add frame size to height if needed
	height := cached.height
	if frameSize > 0 {
		// Frame can add to height (borders, padding)
		if style := t.CurrentStyle(); style != nil {
			// Quick render to get accurate height with frame
			rendered := style.Render(cached.content)
			height = strings.Count(rendered, "\n") + 1
		}
	}

	return height
}

// Render implements lazylist.Item.
func (t *ToolCallItem) Render(width int) string {
	cached := t.renderCached(width)
	return cached.content
}

// Draw implements list.Item.
func (t *ToolCallItem) Draw(scr uv.Screen, area uv.Rectangle) {
	width := area.Dx()
	height := area.Dy()

	// Calculate content width accounting for frame size
	contentWidth := width
	style := t.CurrentStyle()
	if style != nil {
		contentWidth -= style.GetHorizontalFrameSize()
	}

	cached := t.renderCached(contentWidth)
	rendered := cached.content

	if style != nil {
		rendered = style.Render(rendered)
	}

	tempBuf := uv.NewScreenBuffer(width, height)
	styled := uv.NewStyledString(rendered)
	styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))

	t.ApplyHighlight(&tempBuf, width, height, style)
	tempBuf.Draw(scr, area)
}

// renderCached renders the tool call at the given width with caching.
func (t *ToolCallItem) renderCached(width int) cachedToolRender {
	cappedWidth := min(width, t.maxWidth)

	// Check if we have a valid cache entry
	if cached, ok := t.cache[cappedWidth]; ok {
		return cached
	}

	// Render the tool call
	ctx := &toolrender.RenderContext{
		Call:      t.toolCall,
		Result:    t.toolResult,
		Cancelled: t.cancelled,
		IsNested:  t.isNested,
		Width:     cappedWidth,
		Styles:    t.sty,
	}

	rendered := toolrender.Render(ctx)
	height := strings.Count(rendered, "\n") + 1

	cached := cachedToolRender{
		content: rendered,
		height:  height,
	}
	t.cache[cappedWidth] = cached
	return cached
}

// SetHighlight implements list.Highlightable.
func (t *ToolCallItem) SetHighlight(startLine, startCol, endLine, endCol int) {
	t.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
	// Clear cache when highlight changes
	t.cache = make(map[int]cachedToolRender)
}

// UpdateResult updates the tool result and invalidates the cache if needed.
func (t *ToolCallItem) UpdateResult(result message.ToolResult) {
	newKey := generateCacheKey(t.toolCall, result, t.cancelled)
	if newKey != t.cacheKey {
		t.toolResult = result
		t.cacheKey = newKey
		t.cache = make(map[int]cachedToolRender)
	}
}

// AttachmentItem represents a file attachment in a user message.
type AttachmentItem struct {
	list.BaseFocusable
	list.BaseHighlightable
	id       string
	filename string
	path     string
	sty      *styles.Styles
}

// NewAttachmentItem creates a new attachment item.
func NewAttachmentItem(id, filename, path string, sty *styles.Styles) *AttachmentItem {
	a := &AttachmentItem{
		id:       id,
		filename: filename,
		path:     path,
		sty:      sty,
	}
	a.InitHighlight()
	return a
}

// ID implements Identifiable.
func (a *AttachmentItem) ID() string {
	return a.id
}

// Height implements list.Item.
func (a *AttachmentItem) Height(width int) int {
	return 1
}

// Render implements lazylist.Item.
func (a *AttachmentItem) Render(width int) string {
	const maxFilenameWidth = 10
	return a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
		" %s %s ",
		styles.DocumentIcon,
		ansi.Truncate(a.filename, maxFilenameWidth, "..."),
	))
}

// Draw implements list.Item.
func (a *AttachmentItem) Draw(scr uv.Screen, area uv.Rectangle) {
	width := area.Dx()
	height := area.Dy()

	// Calculate content width accounting for frame size
	contentWidth := width
	style := a.CurrentStyle()
	if style != nil {
		contentWidth -= style.GetHorizontalFrameSize()
	}

	const maxFilenameWidth = 10
	content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
		" %s %s ",
		styles.DocumentIcon,
		ansi.Truncate(a.filename, maxFilenameWidth, "..."),
	))

	if style != nil {
		content = style.Render(content)
	}

	tempBuf := uv.NewScreenBuffer(width, height)
	styled := uv.NewStyledString(content)
	styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))

	a.ApplyHighlight(&tempBuf, width, height, style)
	tempBuf.Draw(scr, area)
}

// ThinkingItem represents thinking/reasoning content in assistant messages.
type ThinkingItem struct {
	list.BaseFocusable
	list.BaseHighlightable
	id       string
	thinking string
	duration time.Duration
	finished bool
	maxWidth int
	cache    map[int]string
	sty      *styles.Styles
}

// NewThinkingItem creates a new thinking item.
func NewThinkingItem(id, thinking string, duration time.Duration, finished bool, sty *styles.Styles) *ThinkingItem {
	t := &ThinkingItem{
		id:       id,
		thinking: thinking,
		duration: duration,
		finished: finished,
		maxWidth: 120,
		cache:    make(map[int]string),
		sty:      sty,
	}
	t.InitHighlight()
	return t
}

// ID implements Identifiable.
func (t *ThinkingItem) ID() string {
	return t.id
}

// Height implements list.Item.
func (t *ThinkingItem) Height(width int) int {
	// Calculate content width accounting for frame size
	contentWidth := width
	if style := t.CurrentStyle(); style != nil {
		contentWidth -= style.GetHorizontalFrameSize()
	}

	rendered := t.render(contentWidth)
	return strings.Count(rendered, "\n") + 1
}

// Render implements lazylist.Item.
func (t *ThinkingItem) Render(width int) string {
	return t.render(width)
}

// Draw implements list.Item.
func (t *ThinkingItem) Draw(scr uv.Screen, area uv.Rectangle) {
	width := area.Dx()
	height := area.Dy()

	// Calculate content width accounting for frame size
	contentWidth := width
	style := t.CurrentStyle()
	if style != nil {
		contentWidth -= style.GetHorizontalFrameSize()
	}

	rendered := t.render(contentWidth)

	if style != nil {
		rendered = style.Render(rendered)
	}

	tempBuf := uv.NewScreenBuffer(width, height)
	styled := uv.NewStyledString(rendered)
	styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))

	t.ApplyHighlight(&tempBuf, width, height, style)
	tempBuf.Draw(scr, area)
}

// render renders the thinking content.
func (t *ThinkingItem) render(width int) string {
	cappedWidth := min(width, t.maxWidth)

	if cached, ok := t.cache[cappedWidth]; ok {
		return cached
	}

	renderer := common.PlainMarkdownRenderer(cappedWidth - 1)
	rendered, err := renderer.Render(t.thinking)
	if err != nil {
		// Fallback to line-by-line rendering
		lines := strings.Split(t.thinking, "\n")
		var content strings.Builder
		lineStyle := t.sty.PanelMuted
		for i, line := range lines {
			if line == "" {
				continue
			}
			content.WriteString(lineStyle.Width(cappedWidth).Render(line))
			if i < len(lines)-1 {
				content.WriteString("\n")
			}
		}
		rendered = content.String()
	}

	fullContent := strings.TrimSpace(rendered)

	// Add footer if finished
	if t.finished && t.duration > 0 {
		footer := t.sty.Chat.Message.ThinkingFooter.Render(fmt.Sprintf("Thought for %s", t.duration.String()))
		fullContent = lipgloss.JoinVertical(lipgloss.Left, fullContent, "", footer)
	}

	result := t.sty.PanelMuted.Width(cappedWidth).Padding(0, 1).Render(fullContent)

	t.cache[cappedWidth] = result
	return result
}

// SetHighlight implements list.Highlightable.
func (t *ThinkingItem) SetHighlight(startLine, startCol, endLine, endCol int) {
	t.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
	t.cache = make(map[int]string)
}

// SectionHeaderItem represents a section header (e.g., assistant info).
type SectionHeaderItem struct {
	list.BaseFocusable
	list.BaseHighlightable
	id              string
	modelName       string
	duration        time.Duration
	isSectionHeader bool
	sty             *styles.Styles
}

// NewSectionHeaderItem creates a new section header item.
func NewSectionHeaderItem(id, modelName string, duration time.Duration, sty *styles.Styles) *SectionHeaderItem {
	s := &SectionHeaderItem{
		id:              id,
		modelName:       modelName,
		duration:        duration,
		isSectionHeader: true,
		sty:             sty,
	}
	s.InitHighlight()
	return s
}

// ID implements Identifiable.
func (s *SectionHeaderItem) ID() string {
	return s.id
}

// IsSectionHeader returns true if this is a section header.
func (s *SectionHeaderItem) IsSectionHeader() bool {
	return s.isSectionHeader
}

// Height implements list.Item.
func (s *SectionHeaderItem) Height(width int) int {
	return 1
}

// Render implements lazylist.Item.
func (s *SectionHeaderItem) Render(width int) string {
	return s.sty.Chat.Message.SectionHeader.Render(fmt.Sprintf("%s %s %s",
		s.sty.Subtle.Render(styles.ModelIcon),
		s.sty.Muted.Render(s.modelName),
		s.sty.Subtle.Render(s.duration.String()),
	))
}

// Draw implements list.Item.
func (s *SectionHeaderItem) Draw(scr uv.Screen, area uv.Rectangle) {
	width := area.Dx()
	height := area.Dy()

	// Calculate content width accounting for frame size
	contentWidth := width
	style := s.CurrentStyle()
	if style != nil {
		contentWidth -= style.GetHorizontalFrameSize()
	}

	infoMsg := s.sty.Subtle.Render(s.duration.String())
	icon := s.sty.Subtle.Render(styles.ModelIcon)
	modelFormatted := s.sty.Muted.Render(s.modelName)
	content := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)

	content = s.sty.Chat.Message.SectionHeader.Render(content)

	if style != nil {
		content = style.Render(content)
	}

	tempBuf := uv.NewScreenBuffer(width, height)
	styled := uv.NewStyledString(content)
	styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))

	s.ApplyHighlight(&tempBuf, width, height, style)
	tempBuf.Draw(scr, area)
}

// GetMessageItems extracts [MessageItem]s from a [message.Message]. It returns
// all parts of the message as [MessageItem]s.
//
// For assistant messages with tool calls, pass a toolResults map to link results.
// Use BuildToolResultMap to create this map from all messages in a session.
func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolResult) []MessageItem {
	sty := styles.DefaultStyles()
	var items []MessageItem

	// Skip tool result messages - they're displayed inline with tool calls
	if msg.Role == message.Tool {
		return items
	}

	// Create base styles for the message
	var focusStyle, blurStyle lipgloss.Style
	if msg.Role == message.User {
		focusStyle = sty.Chat.Message.UserFocused
		blurStyle = sty.Chat.Message.UserBlurred
	} else {
		focusStyle = sty.Chat.Message.AssistantFocused
		blurStyle = sty.Chat.Message.AssistantBlurred
	}

	// Process user messages
	if msg.Role == message.User {
		// Add main text content
		content := msg.Content().String()
		if content != "" {
			item := NewMessageContentItem(
				fmt.Sprintf("%s-content", msg.ID),
				content,
				true, // User messages are markdown
				&sty,
			)
			item.SetFocusStyles(&focusStyle, &blurStyle)
			items = append(items, item)
		}

		// Add attachments
		for i, attachment := range msg.BinaryContent() {
			filename := filepath.Base(attachment.Path)
			item := NewAttachmentItem(
				fmt.Sprintf("%s-attachment-%d", msg.ID, i),
				filename,
				attachment.Path,
				&sty,
			)
			item.SetFocusStyles(&focusStyle, &blurStyle)
			items = append(items, item)
		}

		return items
	}

	// Process assistant messages
	if msg.Role == message.Assistant {
		// Check if we need to add a section header
		finishData := msg.FinishPart()
		if finishData != nil && msg.Model != "" {
			model := config.Get().GetModel(msg.Provider, msg.Model)
			modelName := "Unknown Model"
			if model != nil {
				modelName = model.Name
			}

			// Calculate duration (this would need the last user message time)
			duration := time.Duration(0)
			if finishData.Time > 0 {
				duration = time.Duration(finishData.Time-msg.CreatedAt) * time.Second
			}

			header := NewSectionHeaderItem(
				fmt.Sprintf("%s-header", msg.ID),
				modelName,
				duration,
				&sty,
			)
			items = append(items, header)
		}

		// Add thinking content if present
		reasoning := msg.ReasoningContent()
		if strings.TrimSpace(reasoning.Thinking) != "" {
			duration := time.Duration(0)
			if reasoning.StartedAt > 0 && reasoning.FinishedAt > 0 {
				duration = time.Duration(reasoning.FinishedAt-reasoning.StartedAt) * time.Second
			}

			item := NewThinkingItem(
				fmt.Sprintf("%s-thinking", msg.ID),
				reasoning.Thinking,
				duration,
				reasoning.FinishedAt > 0,
				&sty,
			)
			item.SetFocusStyles(&focusStyle, &blurStyle)
			items = append(items, item)
		}

		// Add main text content
		content := msg.Content().String()
		finished := msg.IsFinished()

		// Handle special finish states
		if finished && content == "" && finishData != nil {
			switch finishData.Reason {
			case message.FinishReasonEndTurn:
				// No content to show
			case message.FinishReasonCanceled:
				item := NewMessageContentItem(
					fmt.Sprintf("%s-content", msg.ID),
					"*Canceled*",
					true,
					&sty,
				)
				item.SetFocusStyles(&focusStyle, &blurStyle)
				items = append(items, item)
			case message.FinishReasonError:
				// Render error
				errTag := sty.Chat.Message.ErrorTag.Render("ERROR")
				truncated := ansi.Truncate(finishData.Message, 100, "...")
				title := fmt.Sprintf("%s %s", errTag, sty.Chat.Message.ErrorTitle.Render(truncated))
				details := sty.Chat.Message.ErrorDetails.Render(finishData.Details)
				errorContent := fmt.Sprintf("%s\n\n%s", title, details)

				item := NewMessageContentItem(
					fmt.Sprintf("%s-error", msg.ID),
					errorContent,
					false,
					&sty,
				)
				item.SetFocusStyles(&focusStyle, &blurStyle)
				items = append(items, item)
			}
		} else if content != "" {
			item := NewMessageContentItem(
				fmt.Sprintf("%s-content", msg.ID),
				content,
				true, // Assistant messages are markdown
				&sty,
			)
			item.SetFocusStyles(&focusStyle, &blurStyle)
			items = append(items, item)
		}

		// Add tool calls
		toolCalls := msg.ToolCalls()

		// Use passed-in tool results map (if nil, use empty map)
		resultMap := toolResults
		if resultMap == nil {
			resultMap = make(map[string]message.ToolResult)
		}

		for _, tc := range toolCalls {
			result, hasResult := resultMap[tc.ID]
			if !hasResult {
				result = message.ToolResult{}
			}

			item := NewToolCallItem(
				fmt.Sprintf("%s-tool-%s", msg.ID, tc.ID),
				tc,
				result,
				false, // cancelled state would need to be tracked separately
				false, // nested state would be detected from tool results
				&sty,
			)

			// Tool calls use muted style with optional focus border
			item.SetFocusStyles(&sty.Chat.Message.ToolCallFocused, &sty.Chat.Message.ToolCallBlurred)

			items = append(items, item)
		}

		return items
	}

	return items
}

// BuildToolResultMap creates a map of tool call IDs to their results from a list of messages.
// Tool result messages (role == message.Tool) contain the results that should be linked
// to tool calls in assistant messages.
func BuildToolResultMap(messages []*message.Message) map[string]message.ToolResult {
	resultMap := make(map[string]message.ToolResult)
	for _, msg := range messages {
		if msg.Role == message.Tool {
			for _, result := range msg.ToolResults() {
				if result.ToolCallID != "" {
					resultMap[result.ToolCallID] = result
				}
			}
		}
	}
	return resultMap
}
