package ui

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"termdoku/internal/config"
	"termdoku/internal/game"
	"termdoku/internal/generator"
	"termdoku/internal/savegame"
	"termdoku/internal/solver"
	"termdoku/internal/theme"

	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
)

type timerTickMsg struct{}

type flashDoneMsg struct{ Row, Col int }

type Model struct {
	keymap KeyMap
	styles UIStyles
	theme  theme.Theme

	board        game.Board
	solution     game.Grid
	cursorRow    int
	cursorCol    int
	autoCheck    bool
	timerEnabled bool
	startTime    time.Time
	elapsed      time.Duration
	completed    bool
	paused       bool
	difficulty   string

	undoStack    []game.Move
	redoStack    []game.Move
	flashes      map[[2]int]time.Time
	showHelp     bool
	hintsUsed    int
	noteMode     bool
	notes        map[[2]int][]uint8 // cell -> candidate numbers
	showWinAnim  bool
	winAnimStart time.Time
}

func New(p generator.Grid, th theme.Theme, cfg config.Config) Model {
	b := game.NewBoardFromPuzzle(game.Grid(p))
	// Solve once for auto-check
	sg := b.Values
	if s := solveCopy(b.Values); s != nil {
		sg = *s
	}
	km := DefaultKeyMap()
	km.ApplyBindings(cfg.Bindings)
	m := Model{
		keymap:       km,
		styles:       BuildStyles(th),
		theme:        th,
		board:        b,
		solution:     sg,
		cursorRow:    0,
		cursorCol:    0,
		autoCheck:    cfg.AutoCheck,
		timerEnabled: cfg.TimerEnabled,
		startTime:    time.Now(),
		flashes:      map[[2]int]time.Time{},
		notes:        make(map[[2]int][]uint8),
	}
	return m
}

func solveCopy(g game.Grid) *game.Grid {
	var sg solver.Grid
	for r := 0; r < 9; r++ {
		for c := 0; c < 9; c++ {
			sg[r][c] = g[r][c]
		}
	}
	if solver.Solve(&sg, 2*time.Second) {
		var out game.Grid
		for r := 0; r < 9; r++ {
			for c := 0; c < 9; c++ {
				out[r][c] = sg[r][c]
			}
		}
		return &out
	}
	return nil
}

func (m Model) Init() tea.Cmd {
	var cmds []tea.Cmd
	if m.timerEnabled {
		cmds = append(cmds, tea.Tick(time.Second, func(time.Time) tea.Msg { return timerTickMsg{} }))
	}
	return tea.Batch(cmds...)
}

func (m Model) View() string { return Render(m) }

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		return m.handleKey(msg)
	case timerTickMsg:
		if m.timerEnabled && !m.completed {
			m.elapsed = time.Since(m.startTime)
			return m, tea.Tick(time.Second, func(time.Time) tea.Msg { return timerTickMsg{} })
		}
		return m, nil
	case flashDoneMsg:
		delete(m.flashes, [2]int{msg.Row, msg.Col})
		return m, nil
	}
	return m, nil
}

func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	k := msg

	// Help overlay toggle
	if key.Matches(k, m.keymap.Help) {
		m.showHelp = !m.showHelp
		return m, nil
	}

	// When help is shown, only allow closing it
	if m.showHelp {
		return m, nil
	}

	// Pause toggle
	if key.Matches(k, m.keymap.Pause) {
		m.paused = !m.paused
		return m, nil
	}

	// When paused, only allow unpause or quit
	if m.paused {
		if k.String() == "q" || k.String() == "esc" || k.String() == "ctrl+c" {
			return m, tea.Quit
		}
		return m, nil
	}

	// Toggle features
	if key.Matches(k, m.keymap.ToggleAuto) {
		m.autoCheck = !m.autoCheck
		return m, nil
	}
	if key.Matches(k, m.keymap.ToggleTimer) {
		m.timerEnabled = !m.timerEnabled
		if m.timerEnabled && !m.completed {
			m.startTime = time.Now().Add(-m.elapsed)
			return m, tea.Tick(time.Second, func(time.Time) tea.Msg { return timerTickMsg{} })
		}
		return m, nil
	}
	if key.Matches(k, m.keymap.ToggleNote) {
		m.noteMode = !m.noteMode
		return m, nil
	}

	// Hint
	if key.Matches(k, m.keymap.Hint) && !m.completed {
		m = m.applyHint()
		return m, nil
	}

	// Undo/Redo
	if key.Matches(k, m.keymap.Undo) {
		m = m.applyUndo()
		return m, nil
	}
	if key.Matches(k, m.keymap.Redo) {
		m = m.applyRedo()
		return m, nil
	}

	// Save/Load
	if key.Matches(k, m.keymap.Save) && !m.completed {
		_ = m.saveGame() // Ignore errors for now
		return m, nil
	}
	if key.Matches(k, m.keymap.Load) {
		if loadedModel, err := m.loadGame(); err == nil {
			m = loadedModel
		}
		return m, nil
	}

	s := k.String()
	switch s {
	case "up", "k":
		m.cursorRow = clamp(m.cursorRow-1, 0, 8)
	case "down", "j":
		m.cursorRow = clamp(m.cursorRow+1, 0, 8)
	case "left", "h":
		m.cursorCol = clamp(m.cursorCol-1, 0, 8)
	case "right", "l":
		m.cursorCol = clamp(m.cursorCol+1, 0, 8)
	case " ", "0":
		return m.applyInput(0)
	case "1", "2", "3", "4", "5", "6", "7", "8", "9":
		v := uint8(s[0] - '0')
		return m.applyInput(v)
	case "q", "esc", "ctrl+c":
		return m, tea.Quit
	}
	return m, nil
}

func (m Model) applyInput(v uint8) (tea.Model, tea.Cmd) {
	if m.board.IsGiven(m.cursorRow, m.cursorCol) {
		return m, nil
	}

	// Note mode: toggle candidate number
	if m.noteMode && v != 0 {
		key := [2]int{m.cursorRow, m.cursorCol}
		notes := m.notes[key]

		// Toggle the note
		found := false
		for i, n := range notes {
			if n == v {
				// Remove note
				notes = append(notes[:i], notes[i+1:]...)
				found = true
				break
			}
		}
		if !found {
			// Add note
			notes = append(notes, v)
		}

		if len(notes) > 0 {
			m.notes[key] = notes
		} else {
			delete(m.notes, key)
		}
		return m, nil
	}

	// Normal mode: set value
	prev, ok := m.board.SetValue(m.cursorRow, m.cursorCol, v)
	if !ok {
		return m, nil
	}

	// Clear notes for this cell when setting a value
	if v != 0 {
		delete(m.notes, [2]int{m.cursorRow, m.cursorCol})
	}

	mv := game.Move{Row: m.cursorRow, Col: m.cursorCol, Prev: prev, Next: v, At: time.Now()}
	m.undoStack = append(m.undoStack, mv)
	m.redoStack = nil
	m.flashes[[2]int{m.cursorRow, m.cursorCol}] = time.Now().Add(120 * time.Millisecond)

	if isSolved(m.board.Values, m.solution) {
		m.completed = true
		m.showWinAnim = true
		m.winAnimStart = time.Now()
	}
	return m, tea.Tick(130*time.Millisecond, func(time.Time) tea.Msg { return flashDoneMsg{Row: mv.Row, Col: mv.Col} })
}

func (m Model) applyUndo() Model {
	if len(m.undoStack) == 0 {
		return m
	}
	last := m.undoStack[len(m.undoStack)-1]
	m.undoStack = m.undoStack[:len(m.undoStack)-1]
	m.board.Values[last.Row][last.Col] = last.Prev
	m.redoStack = append(m.redoStack, last)
	m.cursorRow, m.cursorCol = last.Row, last.Col
	m.completed = isSolved(m.board.Values, m.solution)
	return m
}

func (m Model) applyRedo() Model {
	if len(m.redoStack) == 0 {
		return m
	}
	last := m.redoStack[len(m.redoStack)-1]
	m.redoStack = m.redoStack[:len(m.redoStack)-1]
	m.board.Values[last.Row][last.Col] = last.Next
	m.undoStack = append(m.undoStack, last)
	m.cursorRow, m.cursorCol = last.Row, last.Col
	m.completed = isSolved(m.board.Values, m.solution)
	return m
}

func (m Model) applyHint() Model {
	// Find first empty cell and fill it with solution value
	for r := 0; r < 9; r++ {
		for c := 0; c < 9; c++ {
			if !m.board.Given[r][c] && m.board.Values[r][c] == 0 {
				solutionVal := m.solution[r][c]
				if solutionVal != 0 {
					m.board.Values[r][c] = solutionVal
					m.cursorRow, m.cursorCol = r, c
					m.hintsUsed++
					m.flashes[[2]int{r, c}] = time.Now().Add(500 * time.Millisecond)

					// Clear notes for hinted cell
					delete(m.notes, [2]int{r, c})

					if isSolved(m.board.Values, m.solution) {
						m.completed = true
						m.showWinAnim = true
						m.winAnimStart = time.Now()
					}
					return m
				}
			}
		}
	}
	return m
}

func (m Model) saveGame() error {
	// Convert notes map to string keys for JSON
	notesJSON := make(map[string][]uint8)
	for key, notes := range m.notes {
		keyStr := strconv.Itoa(key[0]) + "," + strconv.Itoa(key[1])
		notesJSON[keyStr] = notes
	}

	sg := savegame.SavedGame{
		Board:        m.board.Values,
		Solution:     m.solution,
		Given:        m.board.Given,
		Difficulty:   m.difficulty,
		Elapsed:      int64(m.elapsed.Seconds()),
		StartTime:    m.startTime,
		HintsUsed:    m.hintsUsed,
		Notes:        notesJSON,
		SavedAt:      time.Now(),
		AutoCheck:    m.autoCheck,
		TimerEnabled: m.timerEnabled,
	}
	return savegame.Save(sg)
}

func (m Model) loadGame() (Model, error) {
	sg, err := savegame.Load()
	if err != nil {
		return m, err
	}

	// Restore board
	m.board.Values = sg.Board
	m.board.Given = sg.Given
	m.solution = sg.Solution
	m.difficulty = sg.Difficulty
	m.hintsUsed = sg.HintsUsed
	m.autoCheck = sg.AutoCheck
	m.timerEnabled = sg.TimerEnabled

	// Restore notes (convert string keys back to [2]int)
	m.notes = make(map[[2]int][]uint8)
	for keyStr, notes := range sg.Notes {
		// Parse "r,c" format
		var r, c int
		fmt.Sscanf(keyStr, "%d,%d", &r, &c)
		m.notes[[2]int{r, c}] = notes
	}

	// Restore timer state
	if m.timerEnabled {
		m.elapsed = time.Duration(sg.Elapsed) * time.Second
		m.startTime = time.Now().Add(-m.elapsed)
	}

	m.completed = isSolved(m.board.Values, m.solution)
	return m, nil
}

func clamp(v, lo, hi int) int {
	if v < lo {
		return lo
	}
	if v > hi {
		return hi
	}
	return v
}

func (m Model) StatusLine() string {
	// Completed UI
	if m.completed {
		adaptiveColors := theme.NewAdaptiveColors(m.theme)
		gradientColors := adaptiveColors.GetGradientColors()
		completeGrad := gradientColors["complete"]
		var completeText string
		if m.timerEnabled {
			secs := int(m.elapsed.Truncate(time.Second).Seconds())
			mins := (secs / 60) % 100
			s := secs % 60
			timeStr := fmt.Sprintf("%02d:%02d", mins, s)
			if m.hintsUsed > 0 {
				completeText = fmt.Sprintf("✭ Clear %s (%d hints) ! Press 'm' for menu ✭", timeStr, m.hintsUsed)
			} else {
				completeText = fmt.Sprintf("✭ Clear %s ! Press 'm' for menu ✭", timeStr)
			}
		} else {
			completeText = "✭ Clear! Press 'm' for menu ✭"
		}
		return gradientText(completeText, completeGrad[0], completeGrad[1])
	}
	// All filled but not solved → Try again
	if allFilled(m.board.Values) && !isSolved(m.board.Values, m.solution) {
		return m.styles.StatusError.Render("✭ Try again... ✭")
	}
	// Normal status (fixed width segments)
	var parts []string

	// Timer
	if m.timerEnabled {
		secs := int(m.elapsed.Truncate(time.Second).Seconds())
		mins := (secs / 60) % 100
		s := secs % 60
		timeValue := fmt.Sprintf("%02d:%02d", mins, s)
		parts = append(parts, m.styles.Status.Render("Timer: ")+m.styles.BoolTrue.Render(timeValue))
	}

	// Hints used
	if m.hintsUsed > 0 {
		parts = append(parts, m.styles.Status.Render(fmt.Sprintf("Hints: %d", m.hintsUsed)))
	}

	// Note mode indicator
	if m.noteMode {
		parts = append(parts, m.styles.BoolTrue.Render("NOTE MODE"))
	}

	// Help hint
	parts = append(parts, m.styles.Status.Render("Help: ?"))

	separator := m.styles.Status.Render(" | ")
	return strings.Join(parts, separator)
}

func allFilled(g game.Grid) bool {
	for r := 0; r < 9; r++ {
		for c := 0; c < 9; c++ {
			if g[r][c] == 0 {
				return false
			}
		}
	}
	return true
}

func isSolved(cur game.Grid, sol game.Grid) bool {
	for r := 0; r < 9; r++ {
		for c := 0; c < 9; c++ {
			if cur[r][c] != sol[r][c] {
				return false
			}
		}
	}
	return true
}
