package tui

import (
	"context"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/jerryfane/gitmoot/internal/db"
)

type tiState int

const (
	tiField tiState = iota
	tiCustomPath
	tiRepoCheck
	tiRepoMissing
	tiConfirm
	tiDone
)

// defaultTrainInitPoll is how often the form checks whether the current field's
// prompt was answered externally (mirrors the line wizard's 200ms ticker).
const defaultTrainInitPoll = 200 * time.Millisecond

// TrainInitModel is the bubbletea form for `skillopt train init`. It walks the
// fields one at a time, publishing a prompt record per field so an agent can
// answer it with `gitmoot interactive answer` while a human uses the keyboard.
type TrainInitModel struct {
	store     PromptStore
	fields    []Field
	summary   func(answers map[string]string) [][]string
	interpret Interpret
	poll      time.Duration

	state     tiState
	idx       int
	gen       int // bumped on every field transition; stale poll msgs are dropped
	answers   map[string]string
	external  bool
	flash     string // "answered externally by <source>"
	inlineErr string

	input     textinput.Model
	choiceIdx int

	pendingPromptID string
	aborted         bool

	// pendingValue holds a validated free-text answer awaiting a repo
	// existence check / creation decision (tiRepoCheck / tiRepoMissing).
	pendingValue string

	// Done, when set, replaces tea.Quit on completion/abort so the form can
	// run embedded under the Root router (the cmd typically pops the form and
	// delivers the Result to the model below).
	Done func(Result) tea.Cmd
}

// NewTrainInit builds the form. summary renders the confirm-screen rows from the
// collected answers (the caller folds in defaulted fields like task_kind/mode).
func NewTrainInit(store PromptStore, fields []Field, summary func(map[string]string) [][]string, interpret Interpret, poll time.Duration) TrainInitModel {
	if poll <= 0 {
		poll = defaultTrainInitPoll
	}
	return TrainInitModel{
		store:     store,
		fields:    fields,
		summary:   summary,
		interpret: interpret,
		poll:      poll,
		answers:   map[string]string{},
	}
}

// Result is read by the caller after the program exits.
func (m TrainInitModel) Result() Result {
	return Result{Values: m.answers, Aborted: m.aborted, ExternallyAnswered: m.external}
}

// Init kicks off field setup. The actual enterField mutation happens in Update
// (on initMsg) because Init's value receiver cannot persist state changes.
func (m TrainInitModel) Init() tea.Cmd {
	return func() tea.Msg { return initMsg{} }
}

func (m TrainInitModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case initMsg:
		if len(m.fields) == 0 {
			m.state = tiConfirm
			return m, nil
		}
		return m, m.enterField(m.nextFieldIdx(0))
	case tea.KeyMsg:
		return m.updateKey(msg)
	case promptReadyMsg:
		if msg.gen == m.gen && msg.err != nil {
			m.inlineErr = "prompt publish failed: " + msg.err.Error()
		}
		return m, nil
	case pollTickMsg:
		if msg.gen != m.gen {
			return m, nil // stale tick from a field we already left
		}
		return m, checkPromptCmd(m.store, m.pendingPromptID, m.gen)
	case pollResultMsg:
		if msg.gen != m.gen {
			return m, nil
		}
		if msg.err == nil && msg.prompt.State == db.InteractivePromptStateResolved {
			m.external = true
			m.flash = "answered externally by " + emptyOr(msg.prompt.AnswerSource, "agent")
			return m.commit(msg.prompt.AnswerValue)
		}
		// Not answered yet (or the record is not visible yet) — keep polling.
		return m, pollTick(m.poll, m.gen)
	case repoCheckMsg:
		if msg.gen != m.gen || m.state != tiRepoCheck {
			return m, nil
		}
		if msg.err != nil {
			m.inlineErr = "repo check failed: " + msg.err.Error()
			if m.fields[m.idx].Kind == FieldText {
				m.state = tiField
			} else {
				m.state = tiCustomPath // keep the typed value editable
			}
			return m, nil
		}
		if !msg.missing {
			return m.commit(msg.value)
		}
		m.state = tiRepoMissing
		return m, nil
	case repoCreatedMsg:
		if msg.gen != m.gen || m.state != tiRepoMissing {
			return m, nil // the user re-entered the field before the create returned
		}
		if msg.err != nil {
			m.state = tiRepoMissing
			m.inlineErr = "create failed: " + msg.err.Error()
			return m, nil
		}
		// Make the just-created repo a selectable, pre-selected choice on every
		// repo field so it is auto-selected here and reusable for the other one.
		m.registerCreatedRepo(msg.value)
		return m.commit(msg.value)
	}
	return m, nil
}

func checkRepoCmd(field Field, value string, gen int) tea.Cmd {
	return func() tea.Msg {
		if field.CheckRepo == nil {
			return repoCheckMsg{gen: gen, value: value}
		}
		missing, err := field.CheckRepo(value)
		return repoCheckMsg{gen: gen, value: value, missing: missing, err: err}
	}
}

func createRepoCmd(field Field, value string, gen int) tea.Cmd {
	return func() tea.Msg {
		var err error
		if field.CreateRepo != nil {
			err = field.CreateRepo(value)
		}
		return repoCreatedMsg{gen: gen, value: value, err: err}
	}
}

func (m TrainInitModel) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	if msg.String() == "ctrl+c" {
		return m.abort()
	}
	// A keypress dismisses a lingering "answered externally" flash.
	m.flash = ""
	switch m.state {
	case tiField:
		return m.updateFieldKey(msg)
	case tiCustomPath:
		return m.updateCustomPathKey(msg)
	case tiRepoCheck:
		if msg.String() == "esc" {
			// Cancel just the check; collected answers stay. The gen guard on
			// repoCheckMsg drops the in-flight result.
			m.gen++
			m.inlineErr = ""
			return m.reenterFieldEntry()
		}
		return m, nil // checking; ignore keys until the result arrives
	case tiRepoMissing:
		switch msg.String() {
		case "c", "C":
			m.inlineErr = ""
			return m, createRepoCmd(m.fields[m.idx], m.pendingValue, m.gen)
		case "e", "E", "esc":
			// Re-enter with the value prefilled; picker fields edit in their
			// text sub-state (the field view renders the list, not the input).
			m.inlineErr = ""
			ti := textinput.New()
			ti.SetValue(m.pendingValue)
			ti.CursorEnd()
			m.input = ti
			if m.fields[m.idx].Kind == FieldText {
				m.state = tiField
			} else {
				m.state = tiCustomPath
			}
			return m, m.input.Focus()
		}
		return m, nil
	case tiConfirm:
		switch msg.String() {
		case "y", "Y", "enter":
			m.state = tiDone
			if m.Done != nil {
				return m, m.Done(m.Result())
			}
			return m, tea.Quit
		case "n", "N", "esc":
			return m.abort()
		}
		return m, nil
	}
	return m, nil
}

func (m TrainInitModel) updateFieldKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	field := m.fields[m.idx]
	if msg.String() == "esc" {
		return m.abort()
	}
	if field.Kind == FieldText {
		if msg.String() == "enter" {
			value, status := m.validate(field.Name, m.input.Value())
			if status != "ok" {
				if strings.TrimSpace(m.input.Value()) != "" {
					m.inlineErr = "invalid value — check the expected format"
				} else {
					m.inlineErr = "value required"
				}
				return m, nil
			}
			if field.CheckRepo != nil {
				m.pendingValue = value
				m.state = tiRepoCheck
				m.inlineErr = ""
				return m, checkRepoCmd(field, value, m.gen)
			}
			return m.commit(value)
		}
		var cmd tea.Cmd
		m.input, cmd = m.input.Update(msg)
		return m, cmd
	}
	// FieldChoice / FieldTemplate: a cursor list.
	switch msg.String() {
	case "up", "k":
		if m.choiceIdx > 0 {
			m.choiceIdx--
		}
	case "down", "j":
		if m.choiceIdx < len(field.Choices)-1 {
			m.choiceIdx++
		}
	case "enter":
		choice := field.Choices[m.choiceIdx]
		if choice.Custom {
			m.state = tiCustomPath
			m.inlineErr = ""
			m.input = newPathInput()
			if choice.Placeholder != "" {
				m.input.Placeholder = choice.Placeholder
			}
			return m, m.input.Focus()
		}
		return m.commit(choice.Value)
	}
	return m, nil
}

func (m TrainInitModel) updateCustomPathKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "esc":
		// Back to the template list; the prompt record stays live (same gen).
		m.state = tiField
		m.inlineErr = ""
		return m, nil
	case "enter":
		raw := strings.TrimSpace(m.input.Value())
		if raw == "" {
			m.inlineErr = "a value is required"
			return m, nil
		}
		// The custom sub-state runs the same pipeline as a text field:
		// validation, then the repo existence check when the field has one.
		// The template field keeps its historical raw commit — its custom
		// values (ids, versions, file paths, incl. digit-only ones the
		// interpreter would mistake for menu numbers) resolve later in the cli.
		field := m.fields[m.idx]
		value := raw
		if field.Kind != FieldTemplate {
			var status string
			value, status = m.validate(field.Name, raw)
			if status != "ok" {
				m.inlineErr = "invalid value — check the expected format"
				return m, nil
			}
		}
		if field.CheckRepo != nil {
			m.pendingValue = value
			m.state = tiRepoCheck
			m.inlineErr = ""
			return m, checkRepoCmd(field, value, m.gen)
		}
		return m.commit(value)
	}
	var cmd tea.Cmd
	m.input, cmd = m.input.Update(msg)
	return m, cmd
}

// reenterFieldEntry returns to where the pending value was being entered: the
// text field itself, or the picker's text sub-state with the value preserved.
func (m TrainInitModel) reenterFieldEntry() (tea.Model, tea.Cmd) {
	ti := textinput.New()
	ti.SetValue(m.pendingValue)
	ti.CursorEnd()
	m.input = ti
	if m.fields[m.idx].Kind == FieldText {
		m.state = tiField
	} else {
		m.state = tiCustomPath
	}
	return m, m.input.Focus()
}

func (m TrainInitModel) validate(field, text string) (string, string) {
	if m.interpret != nil {
		return m.interpret(field, text)
	}
	if strings.TrimSpace(text) == "" {
		return "", "reask"
	}
	return strings.TrimSpace(text), "ok"
}

// commit records the answer for the current field, deletes its prompt record,
// and advances to the next field or the confirm screen.
func (m TrainInitModel) commit(value string) (tea.Model, tea.Cmd) {
	field := m.fields[m.idx]
	m.answers[field.Name] = value
	cmds := []tea.Cmd{deletePromptCmd(m.store, field.Prompt.ID)}
	if next := m.nextFieldIdx(m.idx + 1); next < len(m.fields) {
		cmds = append(cmds, m.enterField(next))
		return m, tea.Batch(cmds...)
	}
	// All fields answered → confirm (auto-accept when an agent drove the form).
	m.gen++
	m.pendingPromptID = ""
	if m.external {
		m.state = tiDone
		exit := tea.Quit
		if m.Done != nil {
			exit = m.Done(m.Result())
		}
		cmds = append(cmds, exit)
		return m, tea.Batch(cmds...)
	}
	m.state = tiConfirm
	return m, tea.Batch(cmds...)
}

// nextFieldIdx returns the index of the next field at or after `from` whose
// Skip predicate (if any) does not skip it given the answers so far, or
// len(fields) when none remain.
func (m TrainInitModel) nextFieldIdx(from int) int {
	for i := from; i < len(m.fields); i++ {
		if m.fields[i].Skip == nil || !m.fields[i].Skip(m.answers) {
			return i
		}
	}
	return len(m.fields)
}

// enterField sets up the widget for field i, publishes its prompt record, and
// starts the external-answer poll. It mutates m through the pointer receiver and
// returns the batched commands.
func (m *TrainInitModel) enterField(i int) tea.Cmd {
	if i < 0 || i >= len(m.fields) {
		return nil // defensive: all fields skipped (unreachable with a real form)
	}
	m.state = tiField
	m.idx = i
	m.gen++
	// Note: m.flash is intentionally NOT reset here, so an "answered externally"
	// flash set just before advancing remains visible on the next field until the
	// user presses a key (cleared in updateKey).
	m.inlineErr = ""
	field := m.fields[i]
	m.pendingPromptID = field.Prompt.ID
	cmds := []tea.Cmd{
		upsertPromptCmd(m.store, field.Prompt, m.gen),
		pollTick(m.poll, m.gen),
	}
	if field.Kind == FieldText {
		ti := textinput.New()
		ti.SetValue(field.Default)
		ti.CursorEnd()
		m.input = ti
		cmds = append(cmds, m.input.Focus())
	} else {
		m.choiceIdx = choiceIndex(field.Choices, field.Default)
	}
	return tea.Batch(cmds...)
}

func (m TrainInitModel) abort() (tea.Model, tea.Cmd) {
	m.aborted = true
	m.state = tiDone
	exit := tea.Quit
	if m.Done != nil {
		exit = m.Done(m.Result())
	}
	cmds := []tea.Cmd{exit}
	if m.pendingPromptID != "" {
		cmds = append(cmds, deletePromptCmd(m.store, m.pendingPromptID))
	}
	m.gen++
	return m, tea.Batch(cmds...)
}

func newPathInput() textinput.Model {
	ti := textinput.New()
	ti.Placeholder = "template id, version, or file path"
	return ti
}

// registerCreatedRepo records a newly-created repo on every repo field (those
// with a CreateRepo callback): it inserts the repo as a selectable choice and
// makes it the field's default, so the field it was created on shows it selected
// on re-entry and the other repo field pre-selects it too.
func (m *TrainInitModel) registerCreatedRepo(repo string) {
	for i := range m.fields {
		if m.fields[i].CreateRepo == nil {
			continue
		}
		if m.fields[i].Kind == FieldChoice || m.fields[i].Kind == FieldTemplate {
			m.fields[i].Choices = ensureRepoChoice(m.fields[i].Choices, repo)
		}
		m.fields[i].Default = repo
	}
}

// ensureRepoChoice returns choices with repo present as a normal entry, inserted
// before the first Custom ("another repo…") entry so it stays last. A repo
// already in the list is left untouched (no duplicate).
func ensureRepoChoice(choices []Choice, repo string) []Choice {
	for _, c := range choices {
		if c.Value == repo {
			return choices
		}
	}
	insertAt := len(choices)
	for i, c := range choices {
		if c.Custom {
			insertAt = i
			break
		}
	}
	out := make([]Choice, 0, len(choices)+1)
	out = append(out, choices[:insertAt]...)
	out = append(out, Choice{Value: repo, Label: repo})
	out = append(out, choices[insertAt:]...)
	return out
}

func choiceIndex(choices []Choice, value string) int {
	if value == "" {
		return 0 // no default → first item (avoids matching a Custom entry's empty Value)
	}
	for i, c := range choices {
		if c.Value == value {
			return i
		}
	}
	return 0
}

func emptyOr(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

// --- messages and commands ---

// initMsg triggers first-field setup inside Update (where mutations persist).
type initMsg struct{}

type promptReadyMsg struct {
	gen int
	err error
}

type pollTickMsg struct{ gen int }

type pollResultMsg struct {
	gen    int
	prompt db.InteractivePrompt
	err    error
}

type promptGoneMsg struct{ err error }

type repoCheckMsg struct {
	gen     int
	value   string
	missing bool
	err     error
}

type repoCreatedMsg struct {
	gen   int
	value string
	err   error
}

func upsertPromptCmd(store PromptStore, prompt db.InteractivePrompt, gen int) tea.Cmd {
	return func() tea.Msg {
		return promptReadyMsg{gen: gen, err: store.UpsertInteractivePrompt(context.Background(), prompt)}
	}
}

func pollTick(d time.Duration, gen int) tea.Cmd {
	return tea.Tick(d, func(time.Time) tea.Msg { return pollTickMsg{gen: gen} })
}

func checkPromptCmd(store PromptStore, id string, gen int) tea.Cmd {
	return func() tea.Msg {
		prompt, err := store.GetInteractivePrompt(context.Background(), id)
		return pollResultMsg{gen: gen, prompt: prompt, err: err}
	}
}

func deletePromptCmd(store PromptStore, id string) tea.Cmd {
	return func() tea.Msg {
		return promptGoneMsg{err: store.DeleteInteractivePrompt(context.Background(), id)}
	}
}
