package splash

import (
	"fmt"
	"slices"

	"github.com/charmbracelet/bubbles/v2/key"
	tea "github.com/charmbracelet/bubbletea/v2"
	"github.com/charmbracelet/crush/internal/config"
	"github.com/charmbracelet/crush/internal/fur/provider"
	"github.com/charmbracelet/crush/internal/tui/components/chat"
	"github.com/charmbracelet/crush/internal/tui/components/completions"
	"github.com/charmbracelet/crush/internal/tui/components/core"
	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
	"github.com/charmbracelet/crush/internal/tui/components/core/list"
	"github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
	"github.com/charmbracelet/crush/internal/tui/components/logo"
	"github.com/charmbracelet/crush/internal/tui/styles"
	"github.com/charmbracelet/crush/internal/tui/util"
	"github.com/charmbracelet/crush/internal/version"
	"github.com/charmbracelet/lipgloss/v2"
)

type Splash interface {
	util.Model
	layout.Sizeable
	layout.Help
	Cursor() *tea.Cursor
	// SetOnboarding controls whether the splash shows model selection UI
	SetOnboarding(bool)
	// SetProjectInit controls whether the splash shows project initialization prompt
	SetProjectInit(bool)
}

const (
	SplashScreenPaddingX = 2 // Padding X for the splash screen
	SplashScreenPaddingY = 1 // Padding Y for the splash screen
)

// OnboardingCompleteMsg is sent when onboarding is complete
type OnboardingCompleteMsg struct{}

type splashCmp struct {
	width, height int
	keyMap        KeyMap
	logoRendered  string

	// State
	isOnboarding     bool
	needsProjectInit bool
	selectedNo       bool

	modelList            *models.ModelListComponent
	cursorRow, cursorCol int
}

func New() Splash {
	keyMap := DefaultKeyMap()
	listKeyMap := list.DefaultKeyMap()
	listKeyMap.Down.SetEnabled(false)
	listKeyMap.Up.SetEnabled(false)
	listKeyMap.HalfPageDown.SetEnabled(false)
	listKeyMap.HalfPageUp.SetEnabled(false)
	listKeyMap.Home.SetEnabled(false)
	listKeyMap.End.SetEnabled(false)
	listKeyMap.DownOneItem = keyMap.Next
	listKeyMap.UpOneItem = keyMap.Previous

	t := styles.CurrentTheme()
	inputStyle := t.S().Base.Padding(0, 1, 0, 1)
	modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
	return &splashCmp{
		width:        0,
		height:       0,
		keyMap:       keyMap,
		logoRendered: "",
		modelList:    modelList,
		selectedNo:   false,
	}
}

func (s *splashCmp) SetOnboarding(onboarding bool) {
	s.isOnboarding = onboarding
	if onboarding {
		providers, err := config.Providers()
		if err != nil {
			return
		}
		filteredProviders := []provider.Provider{}
		simpleProviders := []string{
			"anthropic",
			"openai",
			"gemini",
			"xai",
			"openrouter",
		}
		for _, p := range providers {
			if slices.Contains(simpleProviders, string(p.ID)) {
				filteredProviders = append(filteredProviders, p)
			}
		}
		s.modelList.SetProviders(filteredProviders)
	}
}

func (s *splashCmp) SetProjectInit(needsInit bool) {
	s.needsProjectInit = needsInit
}

// GetSize implements SplashPage.
func (s *splashCmp) GetSize() (int, int) {
	return s.width, s.height
}

// Init implements SplashPage.
func (s *splashCmp) Init() tea.Cmd {
	return s.modelList.Init()
}

// SetSize implements SplashPage.
func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
	s.width = width
	s.height = height
	s.logoRendered = s.logoBlock()
	listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
	listWidth := min(60, width-(SplashScreenPaddingX*2))

	// Calculate the cursor position based on the height and logo size
	s.cursorRow = height - listHeigh
	return s.modelList.SetSize(listWidth, listHeigh)
}

// Update implements SplashPage.
func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		return s, s.SetSize(msg.Width, msg.Height)
	case tea.KeyPressMsg:
		switch {
		case key.Matches(msg, s.keyMap.Select):
			if s.isOnboarding {
				modelInx := s.modelList.SelectedIndex()
				items := s.modelList.Items()
				selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
				if s.isProviderConfigured(string(selectedItem.Provider.ID)) {
					cmd := s.setPreferredModel(selectedItem)
					s.isOnboarding = false
					return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
				}
			} else if s.needsProjectInit {
				return s, s.initializeProject()
			}
		case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
			if s.needsProjectInit {
				s.selectedNo = !s.selectedNo
				return s, nil
			}
		case key.Matches(msg, s.keyMap.Yes):
			if s.needsProjectInit {
				return s, s.initializeProject()
			}
		case key.Matches(msg, s.keyMap.No):
			if s.needsProjectInit {
				s.needsProjectInit = false
				return s, util.CmdHandler(OnboardingCompleteMsg{})
			}
		default:
			if s.isOnboarding {
				u, cmd := s.modelList.Update(msg)
				s.modelList = u
				return s, cmd
			}
		}
	}
	return s, nil
}

func (s *splashCmp) initializeProject() tea.Cmd {
	s.needsProjectInit = false
	prompt := `Please analyze this codebase and create a CRUSH.md file containing:
1. Build/lint/test commands - especially for running a single test
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.

The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
If there's already a CRUSH.md, improve it.
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
Add the .crush directory to the .gitignore file if it's not already there.`

	if err := config.MarkProjectInitialized(); err != nil {
		return util.ReportError(err)
	}
	var cmds []tea.Cmd

	cmds = append(cmds, util.CmdHandler(OnboardingCompleteMsg{}))
	if !s.selectedNo {
		cmds = append(cmds,
			util.CmdHandler(chat.SessionClearedMsg{}),
			util.CmdHandler(chat.SendMsg{
				Text: prompt,
			}),
		)
	}
	return tea.Sequence(cmds...)
}

func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
	cfg := config.Get()
	model := cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID)
	if model == nil {
		return util.ReportError(fmt.Errorf("model %s not found for provider %s", selectedItem.Model.ID, selectedItem.Provider.ID))
	}

	selectedModel := config.SelectedModel{
		Model:           selectedItem.Model.ID,
		Provider:        string(selectedItem.Provider.ID),
		ReasoningEffort: model.DefaultReasoningEffort,
		MaxTokens:       model.DefaultMaxTokens,
	}

	err := cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel)
	if err != nil {
		return util.ReportError(err)
	}

	// Now lets automatically setup the small model
	knownProvider, err := s.getProvider(selectedItem.Provider.ID)
	if err != nil {
		return util.ReportError(err)
	}
	if knownProvider == nil {
		// for local provider we just use the same model
		err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
		if err != nil {
			return util.ReportError(err)
		}
	} else {
		smallModel := knownProvider.DefaultSmallModelID
		model := cfg.GetModel(string(selectedItem.Provider.ID), smallModel)
		// should never happen
		if model == nil {
			err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
			if err != nil {
				return util.ReportError(err)
			}
			return nil
		}
		smallSelectedModel := config.SelectedModel{
			Model:           smallModel,
			Provider:        string(selectedItem.Provider.ID),
			ReasoningEffort: model.DefaultReasoningEffort,
			MaxTokens:       model.DefaultMaxTokens,
		}
		err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel)
		if err != nil {
			return util.ReportError(err)
		}
	}
	return nil
}

func (s *splashCmp) getProvider(providerID provider.InferenceProvider) (*provider.Provider, error) {
	providers, err := config.Providers()
	if err != nil {
		return nil, err
	}
	for _, p := range providers {
		if p.ID == providerID {
			return &p, nil
		}
	}
	return nil, nil
}

func (s *splashCmp) isProviderConfigured(providerID string) bool {
	cfg := config.Get()
	if _, ok := cfg.Providers[providerID]; ok {
		return true
	}
	return false
}

func (s *splashCmp) View() string {
	t := styles.CurrentTheme()

	var content string
	if s.isOnboarding {
		remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
		modelListView := s.modelList.View()
		modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
			lipgloss.JoinVertical(
				lipgloss.Left,
				t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Choose a Model"),
				"",
				modelListView,
			),
		)
		content = lipgloss.JoinVertical(
			lipgloss.Left,
			s.logoRendered,
			modelSelector,
		)
	} else if s.needsProjectInit {
		t := styles.CurrentTheme()

		titleStyle := t.S().Base.Foreground(t.FgBase)
		bodyStyle := t.S().Base.Foreground(t.FgMuted)
		shortcutStyle := t.S().Base.Foreground(t.Success)

		initText := lipgloss.JoinVertical(
			lipgloss.Left,
			titleStyle.Render("Would you like to initialize this project?"),
			"",
			bodyStyle.Render("When I initialize your codebase I examine the project and put the"),
			bodyStyle.Render("result into a CRUSH.md file which serves as general context."),
			"",
			bodyStyle.Render("You can also initialize anytime via ")+shortcutStyle.Render("ctrl+p")+bodyStyle.Render("."),
			"",
			bodyStyle.Render("Would you like to initialize now?"),
		)

		yesButton := core.SelectableButton(core.ButtonOpts{
			Text:           "Yep!",
			UnderlineIndex: 0,
			Selected:       !s.selectedNo,
		})

		noButton := core.SelectableButton(core.ButtonOpts{
			Text:           "Nope",
			UnderlineIndex: 0,
			Selected:       s.selectedNo,
		})

		buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, "  ", noButton)

		remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)

		initContent := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
			lipgloss.JoinVertical(
				lipgloss.Left,
				initText,
				"",
				buttons,
			),
		)

		content = lipgloss.JoinVertical(
			lipgloss.Left,
			s.logoRendered,
			initContent,
		)
	} else {
		content = s.logoRendered
	}

	return t.S().Base.
		Width(s.width).
		Height(s.height).
		PaddingTop(SplashScreenPaddingY).
		PaddingLeft(SplashScreenPaddingX).
		PaddingRight(SplashScreenPaddingX).
		PaddingBottom(SplashScreenPaddingY).
		Render(content)
}

func (s *splashCmp) Cursor() *tea.Cursor {
	if s.isOnboarding {
		cursor := s.modelList.Cursor()
		if cursor != nil {
			return s.moveCursor(cursor)
		}
	} else {
		return nil
	}
	return nil
}

func (s *splashCmp) logoBlock() string {
	t := styles.CurrentTheme()
	const padding = 2
	return logo.Render(version.Version, false, logo.Opts{
		FieldColor:   t.Primary,
		TitleColorA:  t.Secondary,
		TitleColorB:  t.Primary,
		CharmColor:   t.Secondary,
		VersionColor: t.Primary,
		Width:        s.width - (SplashScreenPaddingX * 2),
	})
}

func (m *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
	if cursor == nil {
		return nil
	}
	offset := m.cursorRow
	cursor.Y += offset
	cursor.X = cursor.X + 3 // 3 for padding
	return cursor
}

// Bindings implements SplashPage.
func (s *splashCmp) Bindings() []key.Binding {
	if s.isOnboarding {
		return []key.Binding{
			s.keyMap.Select,
			s.keyMap.Next,
			s.keyMap.Previous,
		}
	} else if s.needsProjectInit {
		return []key.Binding{
			s.keyMap.Select,
			s.keyMap.Yes,
			s.keyMap.No,
			s.keyMap.Tab,
			s.keyMap.LeftRight,
		}
	}
	return []key.Binding{}
}
