package ui

import (
	"fmt"
	"math"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/AmanTahiliani/box-box/internal/api"
	"github.com/AmanTahiliani/box-box/internal/models"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// formatSeconds converts a duration in seconds to a lap time string (e.g. "1:31.234").
func formatSeconds(s float64) string {
	if s <= 0 {
		return "--:--.---"
	}
	minutes := int(s) / 60
	secs := s - float64(minutes*60)
	return fmt.Sprintf("%d:%06.3f", minutes, secs)
}

// formatGap formats the gap_to_leader field which can be float64, string, or []interface{}.
func formatGap(v interface{}) string {
	if v == nil {
		return "LEADER"
	}
	switch val := v.(type) {
	case float64:
		if val == 0 {
			return "LEADER"
		}
		return fmt.Sprintf("+%.3fs", val)
	case string:
		return val
	case []interface{}:
		if len(val) == 0 {
			return "--"
		}
		if f, ok := val[len(val)-1].(float64); ok {
			return formatSeconds(f)
		}
		return "--"
	}
	return "--"
}

// formatDuration formats a session result duration (float64 or []float64 for qualifying).
func formatDuration(v interface{}) string {
	if v == nil {
		return "--"
	}
	switch val := v.(type) {
	case float64:
		return formatSeconds(val)
	case []interface{}:
		if len(val) == 0 {
			return "--"
		}
		if f, ok := val[len(val)-1].(float64); ok {
			return formatSeconds(f)
		}
		return "--"
	}
	return "--"
}

// hexToStyle creates a lipgloss.Style with the given hex color as the foreground.
func hexToStyle(hex string) lipgloss.Style {
	if hex == "" {
		return lipgloss.NewStyle()
	}
	if !strings.HasPrefix(hex, "#") {
		hex = "#" + hex
	}
	return lipgloss.NewStyle().Foreground(lipgloss.Color(hex))
}

// hexToBgStyle creates a lipgloss.Style with the given hex color as the background.
func hexToBgStyle(hex string) lipgloss.Style {
	if hex == "" {
		return lipgloss.NewStyle()
	}
	if !strings.HasPrefix(hex, "#") {
		hex = "#" + hex
	}
	return lipgloss.NewStyle().Background(lipgloss.Color(hex)).Foreground(lipgloss.Color(colorWhite))
}

// sparkline generates a unicode block chart for lap times.
// Pit laps (nil duration) are rendered as spaces.
func sparkline(laps []models.Lap, width int) string {
	const blocks = "▁▂▃▄▅▆▇█"
	blockRunes := []rune(blocks)

	if len(laps) == 0 {
		return styleMuted.Render(strings.Repeat("·", width))
	}

	// Collect valid lap durations
	var durations []float64
	minDur, maxDur := math.MaxFloat64, 0.0
	for _, lap := range laps {
		if lap.LapDuration != nil && *lap.LapDuration > 0 {
			d := *lap.LapDuration
			durations = append(durations, d)
			if d < minDur {
				minDur = d
			}
			if d > maxDur {
				maxDur = d
			}
		} else {
			durations = append(durations, -1)
		}
	}

	rng := maxDur - minDur
	if rng == 0 {
		rng = 1
	}

	var sb strings.Builder
	count := 0
	for _, d := range durations {
		if count >= width {
			break
		}
		if d < 0 {
			sb.WriteString(styleMuted.Render("·"))
		} else {
			norm := (d - minDur) / rng
			// Invert: fast laps = tall bar (higher index)
			idx := int((1.0-norm)*float64(len(blockRunes)-1) + 0.5)
			if idx < 0 {
				idx = 0
			}
			if idx >= len(blockRunes) {
				idx = len(blockRunes) - 1
			}
			// Color based on performance: fast=green, mid=yellow, slow=red
			var blockStyle lipgloss.Style
			switch {
			case norm < 0.25:
				blockStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorGreen))
			case norm < 0.5:
				blockStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorYellow))
			case norm < 0.75:
				blockStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorOrange))
			default:
				blockStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorF1Red))
			}
			sb.WriteString(blockStyle.Render(string(blockRunes[idx])))
		}
		count++
	}

	result := sb.String()
	resultLen := utf8.RuneCountInString(lipgloss.NewStyle().Render(result))
	_ = resultLen
	return result
}

// speedSparkline generates a unicode block chart for speed trap values.
// Higher speed = taller bar (green). Lower speed = shorter bar (red).
func speedSparkline(speeds []int, width int) string {
	const blocks = "▁▂▃▄▅▆▇█"
	blockRunes := []rune(blocks)

	if len(speeds) == 0 {
		return styleMuted.Render(strings.Repeat("·", width))
	}

	minSpd, maxSpd := speeds[0], speeds[0]
	for _, s := range speeds {
		if s < minSpd {
			minSpd = s
		}
		if s > maxSpd {
			maxSpd = s
		}
	}
	rng := maxSpd - minSpd
	if rng == 0 {
		rng = 1
	}

	var sb strings.Builder
	count := 0
	for _, s := range speeds {
		if count >= width {
			break
		}
		norm := float64(s-minSpd) / float64(rng)
		idx := int(norm*float64(len(blockRunes)-1) + 0.5)
		if idx < 0 {
			idx = 0
		}
		if idx >= len(blockRunes) {
			idx = len(blockRunes) - 1
		}
		var style lipgloss.Style
		switch {
		case norm > 0.75:
			style = lipgloss.NewStyle().Foreground(lipgloss.Color(colorCyan))
		case norm > 0.5:
			style = lipgloss.NewStyle().Foreground(lipgloss.Color(colorGreen))
		case norm > 0.25:
			style = lipgloss.NewStyle().Foreground(lipgloss.Color(colorYellow))
		default:
			style = lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted))
		}
		sb.WriteString(style.Render(string(blockRunes[idx])))
		count++
	}

	return sb.String()
}

// windArrow maps a wind direction in degrees to a unicode arrow.
func windArrow(degrees int) string {
	arrows := []string{"↑", "↗", "→", "↘", "↓", "↙", "←", "↖"}
	idx := ((degrees + 22) / 45) % 8
	return arrows[idx]
}

// tyreStyle returns a lipgloss.Style for a tyre compound.
func tyreStyle(c models.TyreCompound) lipgloss.Style {
	switch c {
	case models.CompoundSoft:
		return lipgloss.NewStyle().Foreground(lipgloss.Color(colorSoft)).Bold(true)
	case models.CompoundMedium:
		return lipgloss.NewStyle().Foreground(lipgloss.Color(colorMedium)).Bold(true)
	case models.CompoundHard:
		return lipgloss.NewStyle().Foreground(lipgloss.Color(colorHard)).Bold(true)
	case models.CompoundIntermediate:
		return lipgloss.NewStyle().Foreground(lipgloss.Color(colorInter)).Bold(true)
	case models.CompoundWet:
		return lipgloss.NewStyle().Foreground(lipgloss.Color(colorWet)).Bold(true)
	default:
		return lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted))
	}
}

// tyreBgStyle returns a lipgloss.Style with background for tyre compound.
func tyreBgStyle(c models.TyreCompound) lipgloss.Style {
	switch c {
	case models.CompoundSoft:
		return lipgloss.NewStyle().Background(lipgloss.Color(colorSoft)).Foreground(lipgloss.Color("#000000")).Bold(true)
	case models.CompoundMedium:
		return lipgloss.NewStyle().Background(lipgloss.Color(colorMedium)).Foreground(lipgloss.Color("#000000")).Bold(true)
	case models.CompoundHard:
		return lipgloss.NewStyle().Background(lipgloss.Color(colorHard)).Foreground(lipgloss.Color("#000000")).Bold(true)
	case models.CompoundIntermediate:
		return lipgloss.NewStyle().Background(lipgloss.Color(colorInter)).Foreground(lipgloss.Color("#000000")).Bold(true)
	case models.CompoundWet:
		return lipgloss.NewStyle().Background(lipgloss.Color(colorWet)).Foreground(lipgloss.Color(colorWhite)).Bold(true)
	default:
		return lipgloss.NewStyle().Background(lipgloss.Color(colorSurface2)).Foreground(lipgloss.Color(colorMuted))
	}
}

// tyreAbbrev returns a single-letter abbreviation for a tyre compound.
func tyreAbbrev(c models.TyreCompound) string {
	switch c {
	case models.CompoundSoft:
		return "S"
	case models.CompoundMedium:
		return "M"
	case models.CompoundHard:
		return "H"
	case models.CompoundIntermediate:
		return "I"
	case models.CompoundWet:
		return "W"
	default:
		return "?"
	}
}

// renderDelta returns a colored position change indicator, always 2 visible columns wide.
func renderDelta(current, start int) string {
	diff := start - current // positive = gained positions
	switch {
	case diff > 0:
		s := fmt.Sprintf("▲%d", diff)
		return padRightVisible(styleDeltaUp.Render(s), 2)
	case diff < 0:
		s := fmt.Sprintf("▼%d", -diff)
		return padRightVisible(styleDeltaDown.Render(s), 2)
	default:
		return padRightVisible(styleDeltaEqual.Render("─"), 2)
	}
}

// renderPosition formats a position with podium coloring.
func renderPosition(pos int) string {
	s := fmt.Sprintf("%d", pos)
	switch pos {
	case 1:
		return stylePositionFirst.Render(s)
	case 2:
		return stylePositionSecond.Render(s)
	case 3:
		return stylePositionThird.Render(s)
	default:
		return s
	}
}

// renderPointsBar draws a horizontal progress bar for points.
func renderPointsBar(points, maxPoints float64, width int, color string) string {
	if maxPoints == 0 || width <= 0 {
		return ""
	}
	ratio := points / maxPoints
	filled := int(ratio * float64(width))
	if filled > width {
		filled = width
	}
	if filled < 0 {
		filled = 0
	}

	barStyle := lipgloss.NewStyle().
		Background(lipgloss.Color(color)).
		Foreground(lipgloss.Color(color))
	emptyStyle := lipgloss.NewStyle().
		Background(lipgloss.Color(colorSurface2)).
		Foreground(lipgloss.Color(colorSurface2))

	bar := ""
	if filled > 0 {
		bar += barStyle.Render(strings.Repeat("━", filled))
	}
	remaining := width - filled
	if remaining > 0 {
		bar += emptyStyle.Render(strings.Repeat("━", remaining))
	}
	return bar
}

// meetingStatus returns a status indicator for a meeting.
// isNext is true when this meeting is the "current or next" highlighted one.
func meetingStatus(m models.Meeting, now time.Time, isNext bool) string {
	start, err := time.Parse(time.RFC3339, m.DateStart)
	if err != nil {
		start, _ = time.Parse("2006-01-02", m.DateStart[:min(len(m.DateStart), 10)])
	} else {
		start = start.Local()
	}

	end, err := time.Parse(time.RFC3339, m.DateEnd)
	if err != nil {
		end, err = time.Parse("2006-01-02", m.DateEnd[:min(len(m.DateEnd), 10)])
		if err != nil {
			return " "
		}
		end = end.Add(24 * time.Hour)
	} else {
		end = end.Local()
	}

	// Currently underway: started but not ended
	if now.After(start) && now.Before(end) {
		return styleNext.Render("▶")
	}
	if end.Before(now) {
		return stylePast.Render("✓")
	}
	if isNext {
		return styleNext.Render("●")
	}
	return styleFuture.Render("○")
}

// truncate shortens a string to max runes, appending "…" if truncated.
func truncate(s string, max int) string {
	runes := []rune(s)
	if len(runes) <= max {
		return s
	}
	if max <= 1 {
		return "…"
	}
	return string(runes[:max-1]) + "…"
}

// padRight pads or truncates a plain string to exactly width runes.
// Only use this for strings that contain no ANSI escape codes.
func padRight(s string, width int) string {
	runes := []rune(s)
	if len(runes) >= width {
		return string(runes[:width])
	}
	return s + strings.Repeat(" ", width-len(runes))
}

// padLeft left-pads a plain string to exactly width runes.
// Only use this for strings that contain no ANSI escape codes.
func padLeft(s string, width int) string {
	runes := []rune(s)
	if len(runes) >= width {
		return string(runes[:width])
	}
	return strings.Repeat(" ", width-len(runes)) + s
}

// padRightVisible pads an ANSI-styled string to exactly width visible columns.
// Uses lipgloss.Width to measure the visible display width, ignoring escape codes.
func padRightVisible(s string, width int) string {
	vis := lipgloss.Width(s)
	if vis >= width {
		return s
	}
	return s + strings.Repeat(" ", width-vis)
}

// padLeftVisible left-pads an ANSI-styled string to exactly width visible columns.
// Uses lipgloss.Width to measure the visible display width, ignoring escape codes.
func padLeftVisible(s string, width int) string {
	vis := lipgloss.Width(s)
	if vis >= width {
		return s
	}
	return strings.Repeat(" ", width-vis) + s
}

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

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// countryFlag converts a country code to an emoji flag.
func countryFlag(countryCode string) string {
	code := strings.ToUpper(countryCode)
	if len(code) != 2 {
		return "  "
	}
	r1 := rune(0x1F1E6 + int(code[0]-'A'))
	r2 := rune(0x1F1E6 + int(code[1]-'A'))
	return string(r1) + string(r2)
}

// helpBar renders a styled help bar with key/description pairs.
// Format: "key:description" pairs separated by spaces in the output.
func helpBar(hints ...string) string {
	var parts []string
	for _, h := range hints {
		// Split on first space: "key description"
		idx := strings.Index(h, " ")
		if idx > 0 {
			key := h[:idx]
			desc := h[idx+1:]
			parts = append(parts, styleHelpKey.Render(key)+" "+styleHelpDesc.Render(desc))
		} else {
			parts = append(parts, styleHelpDesc.Render(h))
		}
	}
	bar := strings.Join(parts, styleMuted.Render("  │  "))
	return styleHelp.Render(bar)
}

// divider renders a subtle horizontal divider line.
func divider(width int) string {
	if width <= 0 {
		width = 40
	}
	return styleMuted.Render(strings.Repeat("─", width))
}

// matchKey checks if a KeyMsg matches a binding.
func matchKey(msg tea.KeyMsg, binding interface{ Keys() []string }) bool {
	for _, k := range binding.Keys() {
		if msg.String() == k {
			return true
		}
	}
	return false
}

// teamColorFromName returns the known team color hex for a team name, or a fallback.
func teamColorFromName(teamName string) string {
	name := strings.ToLower(teamName)
	switch {
	case strings.Contains(name, "red bull") && !strings.Contains(name, "racing bulls"):
		return colorRedbull
	case strings.Contains(name, "mercedes"):
		return colorMercedes
	case strings.Contains(name, "ferrari"):
		return colorFerrari
	case strings.Contains(name, "mclaren"):
		return colorMclaren
	case strings.Contains(name, "aston martin"):
		return colorAstonMartin
	case strings.Contains(name, "alpine"):
		return colorAlpine
	case strings.Contains(name, "williams"):
		return colorWilliams
	case strings.Contains(name, "haas"):
		return colorHaas
	case strings.Contains(name, "racing bulls"), strings.Contains(name, "alphatauri"), strings.Contains(name, "rb "):
		return colorRB
	case strings.Contains(name, "sauber"), strings.Contains(name, "alfa romeo"), strings.Contains(name, "stake"):
		return colorSauber
	default:
		return colorMuted
	}
}

// teamColorBar renders a thin colored bar (e.g., "█") for team identity.
func teamColorBar(teamColor string) string {
	if teamColor == "" {
		teamColor = colorMuted
	}
	if !strings.HasPrefix(teamColor, "#") {
		teamColor = "#" + teamColor
	}
	return lipgloss.NewStyle().Foreground(lipgloss.Color(teamColor)).Render("┃")
}

// renderStaleBanner returns a yellow warning banner indicating that the
// displayed data was served from an expired cache (stale fallback).
// Shown at the top of any tab whose data came from stale cache during a live
// session lockout.
func renderStaleBanner() string {
	return lipgloss.NewStyle().
		Background(lipgloss.Color(colorYellow)).
		Foreground(lipgloss.Color(colorF1Black)).
		Bold(true).
		Padding(0, 1).
		Render("  ⚠  STALE DATA — served from cache (live session in progress)  ") + "\n\n"
}

// renderErrorView returns a formatted error view. If the error is the OpenF1
// live-session lockout, it shows a special informational banner instead of a
// raw error string.
func renderErrorView(err error) string {
	if api.IsLiveSessionError(err) {
		title := lipgloss.NewStyle().
			Foreground(lipgloss.Color(colorF1Red)).
			Bold(true).
			Render("  LIVE SESSION IN PROGRESS")

		body := lipgloss.NewStyle().
			Foreground(lipgloss.Color(colorWhite)).
			Render("  The OpenF1 API restricts all access (including historical data)\n  during live F1 sessions. This applies to the free tier.")

		hint := lipgloss.NewStyle().
			Foreground(lipgloss.Color(colorMuted)).
			Render("  Access will be restored ~30 minutes after the session ends.\n  Set OPENF1_API_KEY to bypass this restriction (paid tier).")

		return fmt.Sprintf("\n%s\n\n%s\n\n%s\n\n", title, body, hint) +
			helpBar("r retry", "q quit")
	}

	return styleError.Render(fmt.Sprintf("\n  Error: %v\n\n", err)) +
		helpBar("r retry", "q quit")
}
