package tui

import (
	"fmt"
	"image"
	"os"
	"strings"
	"time"

	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"

	"skraak/tools"
	"skraak/utils"
)

// playbackFinishedMsg is sent when audio playback completes
type playbackFinishedMsg struct{}

// Styles
var (
	titleStyle = lipgloss.NewStyle().
			Bold(true).
			Foreground(lipgloss.Color("15")).
			Background(lipgloss.Color("62")).
			Padding(0, 1)

	labelStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("86"))

	errorStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("196"))

	helpStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("241"))

	commentBoxStyle = lipgloss.NewStyle().
				Border(lipgloss.RoundedBorder()).
				BorderForeground(lipgloss.Color("62")).
				Padding(0, 1)

	commentInputStyle = lipgloss.NewStyle().
				Foreground(lipgloss.Color("15"))

	commentHelpStyle = lipgloss.NewStyle().
				Foreground(lipgloss.Color("241"))
)

// Model holds TUI state
type Model struct {
	state        *tools.ClassifyState
	err          string
	quitting     bool
	bindingsHelp string // pre-computed bindings text

	// Comment dialog state
	commentMode bool   // true when comment dialog is open
	commentText string // current input text
}

// New creates a new TUI model
func New(state *tools.ClassifyState) Model {
	// Pre-compute bindings help text
	var bindings []string
	for _, b := range state.Config.Bindings {
		if b.CallType != "" {
			bindings = append(bindings, fmt.Sprintf("%s=%s/%s", b.Key, b.Species, b.CallType))
		} else {
			bindings = append(bindings, fmt.Sprintf("%s=%s", b.Key, b.Species))
		}
	}
	bindingsHelp := strings.Join(bindings, "  ")

	return Model{
		state:        state,
		bindingsHelp: bindingsHelp,
	}
}

func (m Model) protocol() utils.ImageProtocol {
	if m.state.Config.ITerm {
		return utils.ProtocolITerm
	}
	if m.state.Config.Sixel {
		return utils.ProtocolSixel
	}
	return utils.ProtocolKitty
}

// Init initializes the model
func (m Model) Init() tea.Cmd {
	if m.protocol() != utils.ProtocolKitty {
		return inlineImageCmd(m.state, m.protocol())
	}
	return nil
}

// Update handles messages
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		return m.handleKey(msg)
	case playbackFinishedMsg:
		// Just triggers re-render when playback completes
		return m, nil
	}

	return m, nil
}

// segmentChangeCmd returns the appropriate command after a segment change.
// For inline protocols (sixel/iTerm2): sequence clear screen then write image (runs after View renders).
// For kitty: just clear screen (kitty images are managed in View).
func (m Model) segmentChangeCmd() tea.Cmd {
	if m.protocol() != utils.ProtocolKitty {
		return tea.Sequence(tea.ClearScreen, inlineImageCmd(m.state, m.protocol()))
	}
	return tea.ClearScreen
}

func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	// If in comment mode, route to comment handler
	if m.commentMode {
		return m.handleCommentKey(msg)
	}

	m.err = ""

	key := msg.Key()

	// Handle Enter key (check code, not string, to catch modifiers)
	if key.Code == tea.KeyEnter {
		speed := 1.0
		if key.Mod&tea.ModShift != 0 {
			speed = 0.5
		}
		if errMsg := playCurrentSegmentAtSpeed(m.state, speed); errMsg != "" {
			m.err = errMsg
		}
		return m, waitForPlaybackFinished(m.state)
	}

	// Check for Escape key for quit
	if key.Code == tea.KeyEscape || key.Code == tea.KeyEsc {
		if m.state.Player != nil {
			m.state.Player.Stop()
		}
		m.quitting = true
		return m, tea.Quit
	}

	// Check for Space key (open comment dialog)
	if key.Code == tea.KeySpace {
		m.commentText = m.state.GetCurrentComment()
		m.commentMode = true
		return m, nil
	}

	switch msg.String() {
	case "ctrl+c":
		if m.state.Player != nil {
			m.state.Player.Stop()
		}
		m.quitting = true
		return m, tea.Quit

	case ",":
		// Previous segment
		if m.state.Player != nil {
			m.state.Player.Stop()
		}
		m.state.PrevSegment()
		return m, m.segmentChangeCmd()

	case ".":
		// Next segment (no edit)
		if m.state.Player != nil {
			m.state.Player.Stop()
		}
		if moveToNext(m.state) {
			m.quitting = true
			return m, tea.Quit
		}
		return m, m.segmentChangeCmd()

	default:
		// Check for binding
		if len(msg.String()) == 1 {
			k := msg.String()
			if result := m.state.ParseKeyBuffer(k); result != nil {
				if m.state.Player != nil {
					m.state.Player.Stop()
				}
				m.state.ApplyBinding(result)
				if err := m.state.Save(); err != nil {
					m.err = err.Error()
				}
				if moveToNext(m.state) {
					m.quitting = true
					return m, tea.Quit
				}
				return m, m.segmentChangeCmd()
			}
		}
		return m, nil
	}
}

// handleCommentKey handles key presses in comment mode
func (m Model) handleCommentKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	key := msg.Key()

	// Enter: save comment
	if key.Code == tea.KeyEnter {
		m.state.SetComment(m.commentText)
		if err := m.state.Save(); err != nil {
			m.err = err.Error()
		}
		m.commentMode = false
		return m, nil
	}

	// Escape: cancel
	if key.Code == tea.KeyEscape || key.Code == tea.KeyEsc {
		m.commentMode = false
		return m, nil
	}

	// Handle via string representation
	switch msg.String() {
	case "backspace":
		if len(m.commentText) > 0 {
			m.commentText = m.commentText[:len(m.commentText)-1]
		}
		return m, nil

	case "ctrl+u":
		m.commentText = ""
		return m, nil
	}

	// Printable ASCII character
	s := msg.String()
	if len(s) == 1 && s[0] >= 32 && s[0] <= 126 {
		if len(m.commentText) < 140 {
			m.commentText += s
		}
		return m, nil
	}

	return m, nil
}

// playCurrentSegmentAtSpeed loads and plays the current segment's audio at the given speed.
// speed=1.0 is normal, speed=0.5 is half speed.
// Returns an error message string, or empty string on success.
func playCurrentSegmentAtSpeed(state *tools.ClassifyState, speed float64) string {
	df := state.CurrentFile()
	seg := state.CurrentSegment()
	if df == nil || seg == nil {
		return ""
	}

	wavPath := strings.TrimSuffix(df.FilePath, ".data")
	samples, sampleRate, err := utils.ReadWAVSamples(wavPath)
	if err != nil {
		return fmt.Sprintf("audio: %v", err)
	}

	// Initialize player lazily on first play
	if state.Player == nil {
		player, err := utils.NewAudioPlayer(sampleRate)
		if err != nil {
			return fmt.Sprintf("audio init: %v", err)
		}
		state.Player = player
	}

	segSamples := utils.ExtractSegmentSamples(samples, sampleRate, seg.StartTime, seg.EndTime)
	if len(segSamples) > 0 {
		state.PlaybackSpeed = speed
		state.Player.PlayAtSpeed(segSamples, sampleRate, speed)
	}
	return ""
}

// waitForPlaybackFinished polls until playback completes, then sends a message.
func waitForPlaybackFinished(state *tools.ClassifyState) tea.Cmd {
	return tea.Tick(50*time.Millisecond, func(t time.Time) tea.Msg {
		if state.Player == nil || !state.Player.IsPlaying() {
			return playbackFinishedMsg{}
		}
		// Still playing, continue polling
		return waitForPlaybackFinished(state)()
	})
}

func moveToNext(state *tools.ClassifyState) bool {
	return !state.NextSegment()
}

// View renders the TUI
func (m Model) View() tea.View {
	if m.quitting {
		var b strings.Builder
		if m.protocol() == utils.ProtocolKitty {
			utils.ClearKittyImages(&b)
		}
		b.WriteString("\nDone!\n")
		return tea.NewView(b.String())
	}

	var b strings.Builder

	// Clear kitty images from previous render (sixel is handled via commands)
	if m.protocol() == utils.ProtocolKitty {
		utils.ClearKittyImages(&b)
	}

	// Header: file info
	df := m.state.CurrentFile()
	seg := m.state.CurrentSegment()
	total := m.state.TotalSegments()
	current := m.state.CurrentSegmentNumber()

	if df == nil || seg == nil {
		return tea.NewView("\nNo segments to review.\n")
	}

	// Bindings help
	b.WriteString(helpStyle.Render(m.bindingsHelp))
	b.WriteString("\n")
	b.WriteString(helpStyle.Render("[esc]quit [,]prev [.]next [space]comment [enter]play [shift+enter]½speed"))
	b.WriteString("\n\n")

	// Progress bar
	progress := float64(current) / float64(total)
	barWidth := 30
	filled := int(progress * float64(barWidth))
	bar := strings.Repeat("", filled) + strings.Repeat("", barWidth-filled)

	// Title line
	wavFile := strings.TrimSuffix(df.FilePath, ".data")
	wavFile = wavFile[strings.LastIndex(wavFile, "/")+1:]
	b.WriteString(titleStyle.Render(fmt.Sprintf(" %s [%s] %d/%d ", wavFile, bar, current, total)))
	b.WriteString("\n\n")

	// Segment info
	segInfo := fmt.Sprintf("Segment: %.1fs - %.1fs (%.1fs)", seg.StartTime, seg.EndTime, seg.EndTime-seg.StartTime)
	if m.state.Player != nil && m.state.Player.IsPlaying() {
		if m.state.PlaybackSpeed == 0.5 {
			segInfo += "  ▶ Playing 0.5x..."
		} else {
			segInfo += "  ▶ Playing..."
		}
	}
	b.WriteString(segInfo)
	b.WriteString("\n\n")

	// Labels
	filterLabels := seg.GetFilterLabels(m.state.Config.Filter)
	if len(filterLabels) > 0 {
		b.WriteString(labelStyle.Render("Labels:"))
		b.WriteString("\n")
		for _, l := range filterLabels {
			b.WriteString(fmt.Sprintf("%s\n", tools.FormatLabels([]*utils.Label{l}, m.state.Config.Filter)))
		}
	}
	b.WriteString("\n")

	// Comment dialog (when active)
	if m.commentMode {
		m.renderCommentDialog(&b)
		return tea.NewView(b.String())
	}

	// Spectrogram image at bottom (kitty only — inline protocols are written via command)
	if m.protocol() == utils.ProtocolKitty {
		m.renderSpectrogram(&b, df.FilePath, seg)
	}

	// Error
	if m.err != "" {
		b.WriteString(errorStyle.Render(m.err))
	}

	v := tea.NewView(b.String())
	v.AltScreen = true
	return v
}

// renderCommentDialog renders the comment input dialog
func (m Model) renderCommentDialog(b *strings.Builder) {
	// Build dialog content
	inputLine := m.commentText + "" // cursor
	charCount := fmt.Sprintf("%d/140", len(m.commentText))
	helpLine := "[enter]save  [esc]cancel  [ctrl+u]clear"

	// Render box
	content := fmt.Sprintf("Comment:\n%s\n%s\n%s", inputLine, charCount, helpLine)
	b.WriteString(commentBoxStyle.Render(content))
}

// renderSpectrogram generates and outputs a spectrogram image via kitty protocol
func (m Model) renderSpectrogram(b *strings.Builder, dataPath string, seg *utils.Segment) {
	img := generateSpectrogramImage(m.state, dataPath, seg)
	if img == nil {
		b.WriteString("[error generating spectrogram]")
		return
	}
	utils.WriteKittyImage(img, b)
}

// generateSpectrogramImage creates a resized spectrogram image from a segment.
func generateSpectrogramImage(state *tools.ClassifyState, dataPath string, seg *utils.Segment) image.Image {
	wavPath := strings.TrimSuffix(dataPath, ".data")
	samples, sampleRate, err := utils.ReadWAVSamples(wavPath)
	if err != nil {
		return nil
	}

	segSamples := utils.ExtractSegmentSamples(samples, sampleRate, seg.StartTime, seg.EndTime)
	if len(segSamples) == 0 {
		return nil
	}

	config := utils.DefaultSpectrogramConfig(sampleRate)
	spectrogram := utils.GenerateSpectrogram(segSamples, config)
	if spectrogram == nil {
		return nil
	}

	var img image.Image
	if state.Config.Color {
		colorData := utils.ApplyL4Colormap(spectrogram)
		img = utils.CreateRGBImage(colorData)
	} else {
		img = utils.CreateGrayscaleImage(spectrogram)
	}
	if img == nil {
		return nil
	}

	imgSize := state.Config.ImageSize
	if imgSize == 0 {
		imgSize = utils.SpectrogramDisplaySize
	}
	imgSize = utils.ClampImageSize(imgSize)
	return utils.ResizeImage(img, imgSize, imgSize)
}

// inlineImageCmd returns a tea.Cmd that generates and writes an inline image
// (sixel or iTerm2) directly to the terminal, bypassing BubbleTea's renderer.
func inlineImageCmd(state *tools.ClassifyState, protocol utils.ImageProtocol) tea.Cmd {
	return func() tea.Msg {
		df := state.CurrentFile()
		seg := state.CurrentSegment()
		if df == nil || seg == nil {
			return nil
		}

		img := generateSpectrogramImage(state, df.FilePath, seg)
		if img == nil {
			return nil
		}

		fmt.Fprint(os.Stdout, "\r\n\r\n")
		utils.WriteImage(img, os.Stdout, protocol)
		return nil
	}
}