package ui

import (
	"context"
	"fmt"
	"io"
	"log"
	"math"
	"strings"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/lipgloss"

	"github.com/gmgauthier/gostations/internal/config"
	"github.com/gmgauthier/gostations/internal/data"
	playerpkg "github.com/gmgauthier/gostations/internal/player"
	"github.com/gmgauthier/gostations/internal/radio"
)

var (
	// hintKeyStyle gives trigger keys a colorful background + foreground so they pop in the hint row.
	hintKeyStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("15")).
			Background(lipgloss.Color("63")).
			Bold(true)

	// hintTextStyle dims the descriptive labels next to the keys.
	hintTextStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("245"))

	// hintBarStyle renders the entire hint row as a full-width bar with subtle background.
	// The key badges (with their own bg) will stand out on top of this bar.
	hintBarStyle = lipgloss.NewStyle().
			Background(lipgloss.Color("236")).
			Foreground(lipgloss.Color("245")).
			PaddingLeft(2)
)

// item wraps a station for the bubbles list.
type item struct {
	station    radio.Station
	isFavorite bool
}

func (i item) Title() string { return i.station.Name }
func (i item) Description() string {
	return fmt.Sprintf("%s • %s kbps • %s", i.station.Codec, i.station.Bitrate, truncate(i.station.Url, 50))
}
func (i item) FilterValue() string {
	return i.station.Name + " " + i.station.Tags + " " + i.station.Codec + " " + i.station.Url
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "…"
}

// listDelegate for nice rendering.
type listDelegate struct{}

func (d listDelegate) Height() int                               { return 2 }
func (d listDelegate) Spacing() int                              { return 1 }
func (d listDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
func (d listDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
	i, ok := listItem.(item)
	if !ok {
		return
	}
	title := i.station.Name
	if i.isFavorite {
		title = "★ " + title
	}
	if m.Index() == index {
		title = lipgloss.NewStyle().Bold(true).Render("▶ " + title)
	} else {
		title = "  " + title
	}
	desc := fmt.Sprintf("   %s • %s kbps • %s", i.station.Codec, i.station.Bitrate, truncate(i.station.Url, 45))
	fmt.Fprintf(w, "%s\n%s", title, desc)
}

// App is the root model. Supports list selection + playback view (winamp-ish).
type App struct {
	list              list.Model
	quitting          bool
	currentSearchTerm string
	favs              *data.Favorites
	width             int
	height            int

	// player and playback state (two-stage UI)
	player      playerpkg.Player
	playing       bool
	playingItem   item
	nowPlaying    string // streamed metadata title
	paused        bool
	muted         bool
	currentVolume int
}

func NewApp(initial []radio.Station) *App {
	favs, err := data.NewFavorites()
	if err != nil {
		log.Printf("warning: could not load favorites: %v", err)
	}
	favSet := map[string]bool{}
	if favs != nil {
		for _, s := range favs.List() {
			favSet[s.Url] = true
		}
	}

	items := make([]list.Item, len(initial))
	for i, s := range initial {
		isFav := favSet[s.Url]
		items[i] = item{station: s, isFavorite: isFav}
	}

	// Heuristic: if every provided initial station is a favorite, this was a "favorites" initial load
	isFavoritesInitial := len(initial) > 0
	if isFavoritesInitial {
		for _, s := range initial {
			if !favSet[s.Url] {
				isFavoritesInitial = false
				break
			}
		}
	}

	l := list.New(items, listDelegate{}, 60, 20)
	title := "GoStations - Radio Browser (new TUI • ★ = favorite)"
	if isFavoritesInitial {
		title = "GoStations - Your Favorites"
	}
	l.Title = title
	l.SetShowStatusBar(false)
	l.SetFilteringEnabled(true)
	l.Styles.Title = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63"))

	// Use a custom substring/AND filter instead of the default fuzzy (which matches
	// any combination of individual letters, e.g. "WFMT" matching stations with W or F or M or T).
	// This makes "/WFMT" find stations containing the substring "wfmt" (case-insensitive),
	// and multi-word searches require all words to appear somewhere in the target.
	l.Filter = func(term string, targets []string) []list.Rank {
		term = strings.TrimSpace(term)
		if term == "" {
			ranks := make([]list.Rank, len(targets))
			for i := range targets {
				ranks[i] = list.Rank{Index: i}
			}
			return ranks
		}
		words := strings.Fields(strings.ToLower(term))
		var ranks []list.Rank
		for i, t := range targets {
			tl := strings.ToLower(t)
			matches := true
			for _, w := range words {
				if !strings.Contains(tl, w) {
					matches = false
					break
				}
			}
			if matches {
				ranks = append(ranks, list.Rank{Index: i})
			}
		}
		return ranks
	}

	p := newPlayerForTUI()
	return &App{
		list:   l,
		favs:   favs,
		width:  80,
		height: 24,
		player: p,
	}
}

// newPlayerForTUI creates the appropriate Player for interactive TUI use.
// Prefers mpv + IPC for background + controls + metadata; falls back to legacy
// (which will be non-interactive in TUI context).
func newPlayerForTUI() playerpkg.Player {
	pname := "mpv"
	if v, err := config.Get("player.command"); err == nil && v != "" {
		pname = v
	}
	var base []string
	if v, err := config.Get("player.options"); err == nil && v != "" {
		base = strings.Fields(v) // split e.g. "--no-video --volume=50"
	}
	if strings.Contains(pname, "mpv") {
		return playerpkg.NewMpv(pname, base...)
	}
	return playerpkg.NewLegacy(pname, base...)
}

func (a *App) Init() tea.Cmd {
	return nil
}

func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		if a.playing {
			switch msg.String() {
			case "q", "ctrl+c":
				if a.player != nil {
					_ = a.player.Stop()
				}
				a.quitting = true
				return a, tea.Quit
			case "s", "S", "x", "X":
				// stop playback and return to list view
				if a.player != nil {
					_ = a.player.Stop()
				}
				a.playing = false
				a.nowPlaying = ""
				a.paused = false
				a.muted = false
				return a, nil
			case " ", "p", "P":
				if a.player != nil {
					if a.paused {
						_ = a.player.Resume()
						a.paused = false
					} else {
						_ = a.player.Pause()
						a.paused = true
					}
				}
				return a, nil
			case "m", "M":
				if a.player != nil {
					if a.muted {
						_ = a.player.Unmute()
						a.muted = false
					} else {
						_ = a.player.Mute()
						a.muted = true
					}
				}
				return a, nil
			case "left", "h", "H":
				if a.player != nil {
					_ = a.player.Prev()
				}
				return a, nil
			case "right", "l", "L":
				if a.player != nil {
					_ = a.player.Next()
				}
				return a, nil
			case "up", "down":
				if a.player != nil {
					if msg.String() == "up" {
						_ = a.player.VolumeUp()
					} else {
						_ = a.player.VolumeDown()
					}
				}
				return a, nil
			default:
				// swallow other keys in playback (don't leak to list)
				return a, nil
			}
		}
		// not in playback: normal list key handling
		switch msg.String() {
		case "q", "ctrl+c":
			a.quitting = true
			return a, tea.Quit
		case "enter":
			if a.list.FilterState() == list.Filtering || a.list.FilterState() == list.FilterApplied {
				// User pressed enter while in filter: treat the filter term as a new lookup/search
				term := strings.TrimSpace(a.list.FilterValue())
				if term != "" {
					a.currentSearchTerm = term
					a.list.Title = fmt.Sprintf(`Searching for "%s"...`, term)
					return a, searchCmd(term)
				}
			}
			if i, ok := a.list.SelectedItem().(item); ok {
				// Transition to playback view (two-stage TUI).
				a.playing = true
				a.playingItem = i
				a.nowPlaying = i.station.Name
				a.paused = false
				a.muted = false
				a.currentVolume = 70 // default, will be updated by poll/observe
				if a.player != nil {
					if v := a.player.Volume(); v > 0 {
						a.currentVolume = v
					}
					// launch in goroutine so TUI doesn't block even if using legacy player
					// (for mpv+IPC this returns immediately anyway)
					go func() { _ = a.player.Play(i.station.Url) }()
				}
				// start polling for streamed metadata and volume (for the vertical bar)
				return a, tea.Batch(
					metadataPollCmd(a.player),
					volumePollCmd(a.player),
				)
			}
			return a, nil
		case "f":
			// Toggle favorite on the currently selected item
			if sel, ok := a.list.SelectedItem().(item); ok {
				masterIdx := a.list.GlobalIndex()
				if a.favs == nil {
					var ferr error
					a.favs, ferr = data.NewFavorites()
					if ferr != nil {
						statusCmd := a.list.NewStatusMessage("Favorites unavailable: " + ferr.Error())
						return a, statusCmd
					}
				}
				url := sel.station.Url
				wasFav := sel.isFavorite
				if wasFav {
					a.favs.Remove(url)
				} else {
					a.favs.Add(sel.station)
				}
				if saveErr := a.favs.Save(); saveErr != nil {
					statusCmd := a.list.NewStatusMessage("Failed to save favorites: " + saveErr.Error())
					return a, statusCmd
				}
				sel.isFavorite = !wasFav
				setCmd := a.list.SetItem(masterIdx, sel)
				status := "Removed from favorites"
				if sel.isFavorite {
					status = "Added to favorites ★"
				}
				statusCmd := a.list.NewStatusMessage(status)
				return a, tea.Batch(setCmd, statusCmd)
			}
			return a, nil
		}

		// Auto-start filter on first alphanumeric character (better UX than requiring / first)
		s := msg.String()
		if len(s) == 1 {
			r := rune(s[0])
			if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
				if a.list.FilterState() != list.Filtering {
					// Simulate pressing the filter key to activate, then feed the char
					slash := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}}
					var c1 tea.Cmd
					a.list, c1 = a.list.Update(slash)
					var c2 tea.Cmd
					a.list, c2 = a.list.Update(msg)
					return a, tea.Batch(c1, c2)
				}
			}
		}
	case tea.WindowSizeMsg:
		a.width = msg.Width
		a.height = msg.Height
		// Reserve space for the hint row we append below the list in View().
		a.list.SetSize(msg.Width-4, msg.Height-5)
	case metadataMsg:
		if msg.title != "" && a.playing {
			a.nowPlaying = msg.title
		}
		// continue polling while in playback
		if a.playing && a.player != nil {
			return a, metadataPollCmd(a.player)
		}
		return a, nil
	case volumeMsg:
		if a.playing {
			a.currentVolume = msg.volume
		}
		if a.playing && a.player != nil {
			return a, volumePollCmd(a.player)
		}
		return a, nil
	case searchResultsMsg:
		if msg.err != nil {
			a.list.Title = fmt.Sprintf("Search error: %v (press / to search again)", msg.err)
			return a, nil
		}
		// Rebuild items (preserve favorites)
		var favs *data.Favorites
		var ferr error
		if a.favs != nil {
			favs = a.favs
			// reload to get latest persisted state (in case of external edits)
			favs, ferr = data.NewFavorites()
			if ferr != nil {
				favs = a.favs // fallback to in-memory
			}
		} else {
			favs, ferr = data.NewFavorites()
		}
		a.favs = favs
		favSet := map[string]bool{}
		if favs != nil {
			for _, fs := range favs.List() {
				favSet[fs.Url] = true
			}
		}
		newItems := make([]list.Item, len(msg.stations))
		for i, s := range msg.stations {
			newItems[i] = item{station: s, isFavorite: favSet[s.Url]}
		}
		setCmd := a.list.SetItems(newItems)
		title := "GoStations - Radio Browser (new TUI • ★ = favorite)"
		if a.currentSearchTerm != "" {
			title = fmt.Sprintf("GoStations - Results for %q (%d)", a.currentSearchTerm, len(newItems))
		}
		a.list.Title = title
		a.list.ResetFilter()
		return a, setCmd
	}

	var cmd tea.Cmd
	a.list, cmd = a.list.Update(msg)
	return a, cmd
}

func (a *App) View() string {
	if a.quitting {
		return "Thanks for using GoStations!\n"
	}
	if a.playing {
		// playback view (no list, custom winamp-style + optional adapted hint)
		return "\n" + a.renderPlayback() + "\n" + a.renderHint() + "\n"
	}
	return a.list.View() + "\n" + a.renderHint() + "\n"
}

// renderHint builds the terse, colorful bottom hint row as a full-width bar.
// The bar has a subtle background so the whole row looks like a distinct footer.
// Trigger keys use a colorful bg/fg badge (on top of the bar bg).
func (a *App) renderHint() string {
	k := func(key string) string {
		return hintKeyStyle.Render("[" + key + "]")
	}
	t := hintTextStyle.Render

	var content string
	if a.playing {
		content = k("S") + t(" Stop/List  ") + k("SPACE") + t(" Pause  ") + k("M") + t(" Mute  ") + k("←→") + t(" Skip  ") + k("↑↓") + t(" Vol  ") + k("Q") + t(" Quit")
	} else {
		content = k("/") + t(" Filter  (") + k("ENTER") + t(" Search / Play)  ") +
			k("Q") + t(" Quit  ") +
			k("f") + t(" Favorite Toggle")
	}

	w := a.width
	if w <= 0 {
		w = 80
	}

	// Apply dynamic width + the bar background. PaddingLeft gives margin so text
	// doesn't hug the left edge (roughly aligns with list content inset).
	return hintBarStyle.Width(w).Render(content)
}

// renderPlayback draws a classic winamp-ish playback screen with metadata viewer
// and a row of control "buttons". Called when a.playing.
func (a *App) renderPlayback() string {
	w := a.width
	if w < 20 {
		w = 60
	}
	boxW := min(w-4, 70)

	box := lipgloss.NewStyle().
		Border(lipgloss.RoundedBorder()).
		BorderForeground(lipgloss.Color("63")).
		Padding(1, 2).
		Width(boxW)

	dispW := min(boxW-10, 50) // leave room for vertical vol bar (~2) + separator
	display := lipgloss.NewStyle().
		Background(lipgloss.Color("235")).
		Foreground(lipgloss.Color("46")). // classic green lcd
		Width(dispW).
		Height(5).
		Padding(1, 1).
		Align(lipgloss.Left)

	// build the metadata content
	metaLines := []string{
		lipgloss.NewStyle().Bold(true).Render("NOW PLAYING"),
		"",
		truncate(a.playingItem.station.Name, dispW-2),
	}
	if a.nowPlaying != "" {
		metaLines = append(metaLines, truncate(a.nowPlaying, dispW-2))
	} else {
		metaLines = append(metaLines, "(waiting for stream metadata...)")
	}
	metaLines = append(metaLines, truncate(a.playingItem.station.Url, dispW-4))

	metadata := display.Render(strings.Join(metaLines, "\n"))

	// vertical volume bar to the right of the metadata display
	volBar := renderVolumeBar(a.currentVolume, 5, 2)

	// place side-by-side (top aligned). Add a small separator space.
	viewer := lipgloss.JoinHorizontal(lipgloss.Top, metadata, " ", volBar)

	// button row (text buttons, stateful)
	playBtn := "[ > ]"
	if a.paused {
		playBtn = "[|| ]"
	}
	muteBtn := "[M]"
	if a.muted {
		muteBtn = "[M*]"
	}
	btnRow := fmt.Sprintf("%s  %s  %s  %s  %s  %s  %s",
		"[<<]", "[>>]", muteBtn, playBtn, "[VOL-]", "[VOL+]", "[ X ]")

	help := lipgloss.NewStyle().Faint(true).Render("left/right or h/l: skip  |  ↑↓: volume  |  space/p: pause  |  m: mute  |  s/x: stop & list")

	inner := lipgloss.JoinVertical(lipgloss.Left,
		lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")).Render("GoStations"),
		"",
		viewer,
		"",
		btnRow,
		help,
	)

	return box.Render(inner)
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// renderVolumeBar draws a vertical volume indicator bar.
// height matches the metadata display (e.g. 5).
// background is dark gray ("236"), filled indicator uses the green ("46") from the lcd display.
func renderVolumeBar(vol int, height, width int) string {
	if height <= 0 {
		height = 5
	}
	if width <= 0 {
		width = 2
	}
	if vol < 0 {
		vol = 0
	}
	if vol > 100 {
		vol = 100
	}
	filled := int(math.Round(float64(vol) * float64(height) / 100.0))

	darkGray := lipgloss.Color("236")
	green := lipgloss.Color("46")

	var lines []string
	for i := 0; i < height; i++ {
		// i=0 is top (high volume), fill from bottom up
		isFilled := i >= (height - filled)
		style := lipgloss.NewStyle().
			Width(width).
			Background(darkGray)
		if isFilled {
			style = style.Background(green)
		}
		// use block char for the indicator
		seg := "█"
		if !isFilled {
			seg = " "
		}
		lines = append(lines, style.Render(strings.Repeat(seg, width)))
	}
	return lipgloss.JoinVertical(lipgloss.Left, lines...)
}

// searchCmd performs an async station search (used for in-TUI lookups via the filter box).
func searchCmd(name string) tea.Cmd {
	return func() tea.Msg {
		stations, err := radio.Search(context.Background(), name, "", "", "", false)
		return searchResultsMsg{stations: stations, err: err}
	}
}

// searchResultsMsg is sent when a background search completes.
type searchResultsMsg struct {
	stations []radio.Station
	err      error
}

// metadataMsg carries an update to the now-playing stream title from the player.
type metadataMsg struct {
	title string
}

// volumeMsg carries volume level update (0-100).
type volumeMsg struct {
	volume int
}

// metadataPollCmd returns a repeating-ish poll that checks the player's
// Metadata() and emits updates. (Simple, works whether player is mpvIPC or stub.)
func metadataPollCmd(p playerpkg.Player) tea.Cmd {
	if p == nil {
		return nil
	}
	return tea.Tick(800*time.Millisecond, func(t time.Time) tea.Msg {
		if title := p.Metadata(); title != "" {
			return metadataMsg{title: title}
		}
		return nil // no change; next tick will try again
	})
}

// volumePollCmd polls the player's Volume() for the vertical bar.
func volumePollCmd(p playerpkg.Player) tea.Cmd {
	if p == nil {
		return nil
	}
	return tea.Tick(600*time.Millisecond, func(t time.Time) tea.Msg {
		v := p.Volume()
		if v < 0 {
			v = 0
		}
		if v > 100 {
			v = 100
		}
		return volumeMsg{volume: v}
	})
}

// Run starts the TUI (alt screen).
func Run(initial []radio.Station) error {
	p := tea.NewProgram(NewApp(initial), tea.WithAltScreen())
	_, err := p.Run()
	return err
}

// Short is a small helper (duplicated from old for TUI list desc; can be shared later).
func Short(s string, i int) string {
	runes := []rune(s)
	if len(runes) > i {
		return string(runes[:i])
	}
	return s
}
