package dialog

import (
	"context"
	"fmt"
	"sort"
	"time"

	"github.com/charmbracelet/bubbles/v2/key"
	tea "github.com/charmbracelet/bubbletea/v2"
	"github.com/lithammer/fuzzysearch/fuzzy"
	"github.com/sst/opencode-sdk-go"
	"github.com/sst/opencode/internal/app"
	"github.com/sst/opencode/internal/components/list"
	"github.com/sst/opencode/internal/components/modal"
	"github.com/sst/opencode/internal/layout"
	"github.com/sst/opencode/internal/styles"
	"github.com/sst/opencode/internal/theme"
	"github.com/sst/opencode/internal/util"
)

const (
	numVisibleModels = 10
	minDialogWidth   = 40
	maxDialogWidth   = 80
	maxRecentModels  = 5
)

// ModelDialog interface for the model selection dialog
type ModelDialog interface {
	layout.Modal
}

type modelDialog struct {
	app          *app.App
	allModels    []ModelWithProvider
	width        int
	height       int
	modal        *modal.Modal
	searchDialog *SearchDialog
	dialogWidth  int
}

type ModelWithProvider struct {
	Model    opencode.Model
	Provider opencode.Provider
}

// modelItem is a custom list item for model selections
type modelItem struct {
	model ModelWithProvider
}

func (m modelItem) Render(
	selected bool,
	width int,
	baseStyle styles.Style,
) string {
	t := theme.CurrentTheme()

	itemStyle := baseStyle.
		Background(t.BackgroundPanel()).
		Foreground(t.Text())

	if selected {
		itemStyle = itemStyle.Foreground(t.Primary())
	}

	providerStyle := baseStyle.
		Foreground(t.TextMuted()).
		Background(t.BackgroundPanel())

	modelPart := itemStyle.Render(m.model.Model.Name)
	providerPart := providerStyle.Render(fmt.Sprintf(" %s", m.model.Provider.Name))

	combinedText := modelPart + providerPart
	return baseStyle.
		Background(t.BackgroundPanel()).
		PaddingLeft(1).
		Render(combinedText)
}

func (m modelItem) Selectable() bool {
	return true
}

type modelKeyMap struct {
	Enter  key.Binding
	Escape key.Binding
}

var modelKeys = modelKeyMap{
	Enter: key.NewBinding(
		key.WithKeys("enter"),
		key.WithHelp("enter", "select model"),
	),
	Escape: key.NewBinding(
		key.WithKeys("esc"),
		key.WithHelp("esc", "close"),
	),
}

func (m *modelDialog) Init() tea.Cmd {
	m.setupAllModels()
	return m.searchDialog.Init()
}

func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case SearchSelectionMsg:
		// Handle selection from search dialog
		if item, ok := msg.Item.(modelItem); ok {
			return m, tea.Sequence(
				util.CmdHandler(modal.CloseModalMsg{}),
				util.CmdHandler(
					app.ModelSelectedMsg{
						Provider: item.model.Provider,
						Model:    item.model.Model,
					}),
			)
		}
		return m, util.CmdHandler(modal.CloseModalMsg{})
	case SearchCancelledMsg:
		return m, util.CmdHandler(modal.CloseModalMsg{})

	case SearchRemoveItemMsg:
		if item, ok := msg.Item.(modelItem); ok {
			if m.isModelInRecentSection(item.model, msg.Index) {
				m.app.State.RemoveModelFromRecentlyUsed(item.model.Provider.ID, item.model.Model.ID)
				m.app.SaveState()
				items := m.buildDisplayList(m.searchDialog.GetQuery())
				m.searchDialog.SetItems(items)
			}
		}
		return m, nil

	case SearchQueryChangedMsg:
		// Update the list based on search query
		items := m.buildDisplayList(msg.Query)
		m.searchDialog.SetItems(items)
		return m, nil

	case tea.WindowSizeMsg:
		m.width = msg.Width
		m.height = msg.Height
		m.searchDialog.SetWidth(m.dialogWidth)
		m.searchDialog.SetHeight(msg.Height)
	}

	updatedDialog, cmd := m.searchDialog.Update(msg)
	m.searchDialog = updatedDialog.(*SearchDialog)
	return m, cmd
}

func (m *modelDialog) View() string {
	return m.searchDialog.View()
}

func (m *modelDialog) calculateOptimalWidth(models []ModelWithProvider) int {
	maxWidth := minDialogWidth

	for _, model := range models {
		// Calculate the width needed for this item: "ModelName (ProviderName)"
		// Add 4 for the parentheses, space, and some padding
		itemWidth := len(model.Model.Name) + len(model.Provider.Name) + 4
		if itemWidth > maxWidth {
			maxWidth = itemWidth
		}
	}

	if maxWidth > maxDialogWidth {
		maxWidth = maxDialogWidth
	}

	return maxWidth
}

func (m *modelDialog) setupAllModels() {
	providers, _ := m.app.ListProviders(context.Background())

	m.allModels = make([]ModelWithProvider, 0)
	for _, provider := range providers {
		for _, model := range provider.Models {
			m.allModels = append(m.allModels, ModelWithProvider{
				Model:    model,
				Provider: provider,
			})
		}
	}

	m.sortModels()

	// Calculate optimal width based on all models
	m.dialogWidth = m.calculateOptimalWidth(m.allModels)

	// Initialize search dialog
	m.searchDialog = NewSearchDialog("Search models...", numVisibleModels)
	m.searchDialog.SetWidth(m.dialogWidth)

	// Build initial display list (empty query shows grouped view)
	items := m.buildDisplayList("")
	m.searchDialog.SetItems(items)
}

func (m *modelDialog) sortModels() {
	sort.Slice(m.allModels, func(i, j int) bool {
		modelA := m.allModels[i]
		modelB := m.allModels[j]

		usageA := m.getModelUsageTime(modelA.Provider.ID, modelA.Model.ID)
		usageB := m.getModelUsageTime(modelB.Provider.ID, modelB.Model.ID)

		// If both have usage times, sort by most recent first
		if !usageA.IsZero() && !usageB.IsZero() {
			return usageA.After(usageB)
		}

		// If only one has usage time, it goes first
		if !usageA.IsZero() && usageB.IsZero() {
			return true
		}
		if usageA.IsZero() && !usageB.IsZero() {
			return false
		}

		// If neither has usage time, sort by release date desc if available
		if modelA.Model.ReleaseDate != "" && modelB.Model.ReleaseDate != "" {
			dateA := m.parseReleaseDate(modelA.Model.ReleaseDate)
			dateB := m.parseReleaseDate(modelB.Model.ReleaseDate)
			if !dateA.IsZero() && !dateB.IsZero() {
				return dateA.After(dateB)
			}
		}

		// If only one has release date, it goes first
		if modelA.Model.ReleaseDate != "" && modelB.Model.ReleaseDate == "" {
			return true
		}
		if modelA.Model.ReleaseDate == "" && modelB.Model.ReleaseDate != "" {
			return false
		}

		// If neither has usage time nor release date, fall back to alphabetical sorting
		return modelA.Model.Name < modelB.Model.Name
	})
}

func (m *modelDialog) parseReleaseDate(dateStr string) time.Time {
	if parsed, err := time.Parse("2006-01-02", dateStr); err == nil {
		return parsed
	}

	return time.Time{}
}

func (m *modelDialog) getModelUsageTime(providerID, modelID string) time.Time {
	for _, usage := range m.app.State.RecentlyUsedModels {
		if usage.ProviderID == providerID && usage.ModelID == modelID {
			return usage.LastUsed
		}
	}
	return time.Time{}
}

// buildDisplayList creates the list items based on search query
func (m *modelDialog) buildDisplayList(query string) []list.Item {
	if query != "" {
		// Search mode: use fuzzy matching
		return m.buildSearchResults(query)
	} else {
		// Grouped mode: show Recent section and provider groups
		return m.buildGroupedResults()
	}
}

// buildSearchResults creates a flat list of search results using fuzzy matching
func (m *modelDialog) buildSearchResults(query string) []list.Item {
	type modelMatch struct {
		model ModelWithProvider
		score int
	}

	modelNames := []string{}
	modelMap := make(map[string]ModelWithProvider)

	// Create search strings and perform fuzzy matching
	for _, model := range m.allModels {
		searchStr := fmt.Sprintf("%s %s", model.Model.Name, model.Provider.Name)
		modelNames = append(modelNames, searchStr)
		modelMap[searchStr] = model

		searchStr = fmt.Sprintf("%s %s", model.Provider.Name, model.Model.Name)
		modelNames = append(modelNames, searchStr)
		modelMap[searchStr] = model
	}

	matches := fuzzy.RankFindFold(query, modelNames)
	sort.Sort(matches)

	items := []list.Item{}
	seenModels := make(map[string]bool)

	for _, match := range matches {
		model := modelMap[match.Target]
		// Create a unique key to avoid duplicates
		key := fmt.Sprintf("%s:%s", model.Provider.ID, model.Model.ID)
		if seenModels[key] {
			continue
		}
		seenModels[key] = true
		items = append(items, modelItem{model: model})
	}

	return items
}

// buildGroupedResults creates a grouped list with Recent section and provider groups
func (m *modelDialog) buildGroupedResults() []list.Item {
	var items []list.Item

	// Add Recent section
	recentModels := m.getRecentModels(maxRecentModels)
	if len(recentModels) > 0 {
		items = append(items, list.HeaderItem("Recent"))
		for _, model := range recentModels {
			items = append(items, modelItem{model: model})
		}
	}

	// Group models by provider
	providerGroups := make(map[string][]ModelWithProvider)
	for _, model := range m.allModels {
		providerName := model.Provider.Name
		providerGroups[providerName] = append(providerGroups[providerName], model)
	}

	// Get sorted provider names for consistent order
	var providerNames []string
	for name := range providerGroups {
		providerNames = append(providerNames, name)
	}
	sort.Strings(providerNames)

	// Add provider groups
	for _, providerName := range providerNames {
		models := providerGroups[providerName]

		// Sort models within provider group
		sort.Slice(models, func(i, j int) bool {
			modelA := models[i]
			modelB := models[j]

			usageA := m.getModelUsageTime(modelA.Provider.ID, modelA.Model.ID)
			usageB := m.getModelUsageTime(modelB.Provider.ID, modelB.Model.ID)

			// Sort by usage time first, then by release date, then alphabetically
			if !usageA.IsZero() && !usageB.IsZero() {
				return usageA.After(usageB)
			}
			if !usageA.IsZero() && usageB.IsZero() {
				return true
			}
			if usageA.IsZero() && !usageB.IsZero() {
				return false
			}

			// Sort by release date if available
			if modelA.Model.ReleaseDate != "" && modelB.Model.ReleaseDate != "" {
				dateA := m.parseReleaseDate(modelA.Model.ReleaseDate)
				dateB := m.parseReleaseDate(modelB.Model.ReleaseDate)
				if !dateA.IsZero() && !dateB.IsZero() {
					return dateA.After(dateB)
				}
			}

			return modelA.Model.Name < modelB.Model.Name
		})

		// Add provider header
		items = append(items, list.HeaderItem(providerName))

		// Add models in this provider group
		for _, model := range models {
			items = append(items, modelItem{model: model})
		}
	}

	return items
}

// getRecentModels returns the most recently used models
func (m *modelDialog) getRecentModels(limit int) []ModelWithProvider {
	var recentModels []ModelWithProvider

	// Get recent models from app state
	for _, usage := range m.app.State.RecentlyUsedModels {
		if len(recentModels) >= limit {
			break
		}

		// Find the corresponding model
		for _, model := range m.allModels {
			if model.Provider.ID == usage.ProviderID && model.Model.ID == usage.ModelID {
				recentModels = append(recentModels, model)
				break
			}
		}
	}

	return recentModels
}

func (m *modelDialog) isModelInRecentSection(model ModelWithProvider, index int) bool {
	// Only check if we're in grouped mode (no search query)
	if m.searchDialog.GetQuery() != "" {
		return false
	}

	recentModels := m.getRecentModels(maxRecentModels)
	if len(recentModels) == 0 {
		return false
	}

	// Index 0 is the "Recent" header, so recent models are at indices 1 to len(recentModels)
	if index >= 1 && index <= len(recentModels) {
		if index-1 < len(recentModels) {
			recentModel := recentModels[index-1]
			return recentModel.Provider.ID == model.Provider.ID && recentModel.Model.ID == model.Model.ID
		}
	}

	return false
}

func (m *modelDialog) Render(background string) string {
	return m.modal.Render(m.View(), background)
}

func (s *modelDialog) Close() tea.Cmd {
	return nil
}

func NewModelDialog(app *app.App) ModelDialog {
	dialog := &modelDialog{
		app: app,
	}

	dialog.setupAllModels()

	dialog.modal = modal.New(
		modal.WithTitle("Select Model"),
		modal.WithMaxWidth(dialog.dialogWidth+4),
	)

	return dialog
}
