package ui

import (
	"fmt"
	"strings"
	"time"

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

type tabIndex int

const (
	tabDashboard  tabIndex = 0
	tabStandings  tabIndex = 1
	tabCalendar   tabIndex = 2
	tabRaceDetail tabIndex = 3
	tabDriver     tabIndex = 4
	tabLive       tabIndex = 5
	tabTrackMap   tabIndex = 6
)

const numTabs = 7

var tabNames = []string{"Home", "Standings", "Calendar", "Race", "Drivers", "Live", "Map"}
var tabIcons = []string{"🏠", "🏆", "📅", "🏁", "👤", "🔴", "🗺"}

// splashDoneMsg is sent after the splash screen duration has elapsed.
type splashDoneMsg struct{}

// trackPrefetchDoneMsg is sent (and silently ignored) when the background
// track-outline pre-fetch finishes. It carries no data — its only purpose is
// to satisfy the tea.Cmd contract.
type trackPrefetchDoneMsg struct{}

// AppModel is the root Bubble Tea model.
type AppModel struct {
	client *api.OpenF1Client

	activeTab tabIndex
	year      int
	width     int
	height    int

	standings  StandingsModel
	calendar   CalendarModel
	raceDetail RaceDetailModel
	driver     DriverModel
	dashboard  DashboardModel
	live       OfficialLiveModel
	trackMap   TrackMapModel

	meetings []models.Meeting
	nextInfo *statusBarTarget

	// Splash screen state
	showSplash    bool
	splashSpinner spinner.Model
}

type statusBarTarget struct {
	meeting *models.Meeting
	current *models.Session
	next    *models.Session
	loading bool
	stale   bool
	err     error
}

func NewAppModel(client *api.OpenF1Client) AppModel {
	year := time.Now().Year()

	sp := spinner.New()
	sp.Spinner = spinner.Points
	sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(colorF1Red))

	return AppModel{
		client:        client,
		activeTab:     tabDashboard,
		year:          year,
		standings:     NewStandingsModel(client, year),
		calendar:      NewCalendarModel(client, year),
		raceDetail:    NewRaceDetailModel(client),
		driver:        NewDriverModel(client),
		dashboard:     NewDashboardModel(client, year),
		live:          NewOfficialLiveModel(),
		trackMap:      NewTrackMapModel(client),
		showSplash:    true,
		splashSpinner: sp,
	}
}

func splashTimer() tea.Cmd {
	return tea.Tick(2*time.Second, func(t time.Time) tea.Msg {
		return splashDoneMsg{}
	})
}

// prefetchTrackOutlines fetches the season calendar and then pre-populates
// the track outline cache for every circuit. This runs as a background command
// so the UI is never blocked. Errors are silently discarded — this is a
// best-effort operation.
func prefetchTrackOutlines(client *api.OpenF1Client, year int) tea.Cmd {
	return func() tea.Msg {
		meetings, err := client.GetMeetingsForYear(year)
		if err != nil || len(meetings) == 0 {
			return trackPrefetchDoneMsg{}
		}
		client.PrefetchTrackOutlines(meetings)
		return trackPrefetchDoneMsg{}
	}
}

type statusBarScheduleLoadedMsg struct {
	target *statusBarTarget
}

func fetchStatusBarSchedule(client *api.OpenF1Client, meetings []models.Meeting) tea.Cmd {
	meetingsCopy := append([]models.Meeting(nil), meetings...)
	return func() tea.Msg {
		now := time.Now()
		target := &statusBarTarget{}

		meeting := currentMeeting(meetingsCopy, now)
		if meeting == nil {
			meeting = nextUpcomingMeeting(meetingsCopy, now)
		}
		if meeting == nil {
			return statusBarScheduleLoadedMsg{target: target}
		}

		target.meeting = meeting
		target.loading = true

		sessions, err := client.GetSessionsForMeeting(int(meeting.MeetingKey))
		if err != nil {
			target.loading = false
			target.err = err
			target.stale = client.LastResponseWasStale()
			return statusBarScheduleLoadedMsg{target: target}
		}

		sortSessionsByStart(sessions)
		target.current, target.next = currentAndNextSession(sessions, now)
		target.loading = false
		target.stale = client.LastResponseWasStale()
		return statusBarScheduleLoadedMsg{target: target}
	}
}

func (m AppModel) Init() tea.Cmd {
	return tea.Batch(
		m.dashboard.Init(),
		m.live.Init(),
		m.standings.Init(),
		m.calendar.Init(),
		m.raceDetail.Init(),
		m.driver.Init(),
		m.trackMap.Init(),
		m.splashSpinner.Tick,
		splashTimer(),
		// Background: pre-fetch track outlines for all circuits this season
		// so the track map works during live sessions when the API is locked.
		prefetchTrackOutlines(m.client, m.year),
	)
}

func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd

	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.width = msg.Width
		m.height = msg.Height
		contentHeight := m.height - 5 // tab bar(2) + status bar + help + spacing
		m.raceDetail.SetSize(m.width-4, contentHeight)
		m.live.SetSize(m.width, contentHeight)
		var cmd1, cmd2, cmd3, cmd4, cmd5, cmd6, cmd7 tea.Cmd
		m.dashboard, cmd1 = m.dashboard.Update(msg)
		m.live, cmd2 = m.live.Update(msg)
		m.standings, cmd3 = m.standings.Update(msg)
		m.calendar, cmd4 = m.calendar.Update(msg)
		m.raceDetail, cmd5 = m.raceDetail.Update(msg)
		m.driver, cmd6 = m.driver.Update(msg)
		m.trackMap, cmd7 = m.trackMap.Update(msg)
		return m, tea.Batch(cmd1, cmd2, cmd3, cmd4, cmd5, cmd6, cmd7)

	case splashDoneMsg:
		m.showSplash = false
		return m, nil

	case trackPrefetchDoneMsg:
		// Background track pre-fetch finished — nothing to display.
		return m, nil

	case tea.KeyMsg:
		// Any key press during splash dismisses it
		if m.showSplash {
			m.showSplash = false
			return m, nil
		}

		// Global quit
		if matchKey(msg, GlobalKeys.Quit) {
			return m, tea.Quit
		}
		// Tab switching
		switch {
		case matchKey(msg, GlobalKeys.Tab1):
			m.activeTab = tabDashboard
			return m, nil
		case matchKey(msg, GlobalKeys.Tab2):
			m.activeTab = tabStandings
			return m, nil
		case matchKey(msg, GlobalKeys.Tab3):
			m.activeTab = tabCalendar
			return m, nil
		case matchKey(msg, GlobalKeys.Tab4):
			m.activeTab = tabRaceDetail
			return m, nil
		case matchKey(msg, GlobalKeys.Tab5):
			m.activeTab = tabDriver
			var cmd tea.Cmd
			m.driver, cmd = m.driver.TriggerLoad()
			cmds = append(cmds, cmd)
			return m, tea.Batch(cmds...)
		case matchKey(msg, GlobalKeys.Tab6):
			m.activeTab = tabLive
			return m, nil
		case matchKey(msg, GlobalKeys.Tab7):
			m.activeTab = tabTrackMap
			if !m.trackMap.HasSession() {
				var cmd tea.Cmd
				m.trackMap, cmd = m.trackMap.FetchActiveSession(m.client)
				cmds = append(cmds, cmd)
			}
			return m, tea.Batch(cmds...)
		case matchKey(msg, GlobalKeys.NextTab):
			m.activeTab = (m.activeTab + 1) % numTabs
			if m.activeTab == tabDriver {
				var cmd tea.Cmd
				m.driver, cmd = m.driver.TriggerLoad()
				cmds = append(cmds, cmd)
			}
			return m, tea.Batch(cmds...)
		case matchKey(msg, GlobalKeys.PrevTab):
			m.activeTab = (m.activeTab - 1 + numTabs) % numTabs
			if m.activeTab == tabDriver {
				var cmd tea.Cmd
				m.driver, cmd = m.driver.TriggerLoad()
				cmds = append(cmds, cmd)
			}
			return m, tea.Batch(cmds...)
		case matchKey(msg, GlobalKeys.Back):
			// Back from race detail -> calendar
			if m.activeTab == tabRaceDetail {
				m.activeTab = tabCalendar
				return m, nil
			}
		case matchKey(msg, GlobalKeys.Year):
			// Cycle years: up to current year, wrap to 2023
			if m.year >= time.Now().Year() {
				m.year = 2023
			} else {
				m.year++
			}
			m.calendar.year = m.year
			m.calendar.loading = true
			m.standings.year = m.year
			m.standings.loading = true

			return m, tea.Batch(
				m.calendar.Init(),
				m.standings.Init(),
			)
		}

	case meetingSelectedMsg:
		m.activeTab = tabRaceDetail
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
		m.driver, _ = m.driver.Update(msg)
		return m, tea.Batch(cmds...)

	case sessionsLoadedMsg:
		var cmd1, cmd2 tea.Cmd
		m.raceDetail, cmd1 = m.raceDetail.Update(msg)
		m.driver, cmd2 = m.driver.Update(msg)
		cmds = append(cmds, cmd1, cmd2)
		return m, tea.Batch(cmds...)

	case driverChampionshipLoadedMsg:
		var cmd tea.Cmd
		m.standings, cmd = m.standings.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case teamChampionshipLoadedMsg:
		var cmd tea.Cmd
		m.standings, cmd = m.standings.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case standingsDriversLoadedMsg:
		var cmd tea.Cmd
		m.standings, cmd = m.standings.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case meetingsLoadedMsg:
		if msg.err == nil {
			m.meetings = msg.meetings
			m.nextInfo = &statusBarTarget{loading: true}
			cmds = append(cmds, fetchStatusBarSchedule(m.client, msg.meetings))
		}
		var cmd tea.Cmd
		m.calendar, cmd = m.calendar.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case statusBarScheduleLoadedMsg:
		m.nextInfo = msg.target
		return m, nil

	case sessionResultsLoadedMsg:
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case sessionDriversLoadedMsg:
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case raceControlLoadedMsg:
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case weatherLoadedMsg:
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case overtakesLoadedMsg:
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case startingGridLoadedMsg:
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case loadSecondaryDataMsg:
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case driverListLoadedMsg:
		var cmd tea.Cmd
		m.driver, cmd = m.driver.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case driverStintsLoadedMsg:
		var cmd tea.Cmd
		m.driver, cmd = m.driver.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case driverLapsLoadedMsg:
		var cmd tea.Cmd
		m.driver, cmd = m.driver.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case driverPitsLoadedMsg:
		var cmd tea.Cmd
		m.driver, cmd = m.driver.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case driverPositionsLoadedMsg:
		var cmd tea.Cmd
		m.driver, cmd = m.driver.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case driverTeamRadioLoadedMsg:
		var cmd tea.Cmd
		m.driver, cmd = m.driver.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case wsDataMsg:
		var cmd tea.Cmd
		m.live, cmd = m.live.Update(msg)
		cmds = append(cmds, cmd)
		// Forward driver info to track map for car-marker colouring
		m.trackMap.InjectDriverInfo(msg.DriverInfo)
		return m, tea.Batch(cmds...)

	case trackOutlineLoadedMsg, trackCarsLoadedMsg, trackMapTickMsg:
		var cmd tea.Cmd
		m.trackMap, cmd = m.trackMap.Update(msg)
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)

	case spinner.TickMsg:
		if m.showSplash {
			var cmd tea.Cmd
			m.splashSpinner, cmd = m.splashSpinner.Update(msg)
			cmds = append(cmds, cmd)
		}
		var cmd1, cmd2, cmd3, cmd4, cmd5 tea.Cmd
		m.standings, cmd1 = m.standings.Update(msg)
		m.calendar, cmd2 = m.calendar.Update(msg)
		m.raceDetail, cmd3 = m.raceDetail.Update(msg)
		m.driver, cmd4 = m.driver.Update(msg)
		m.trackMap, cmd5 = m.trackMap.Update(msg)
		cmds = append(cmds, cmd1, cmd2, cmd3, cmd4, cmd5)
		return m, tea.Batch(cmds...)
	}

	// Route keyboard input to active tab
	switch m.activeTab {
	case tabDashboard:
		var cmd tea.Cmd
		m.dashboard, cmd = m.dashboard.Update(msg)
		cmds = append(cmds, cmd)
	case tabLive:
		var cmd tea.Cmd
		m.live, cmd = m.live.Update(msg)
		cmds = append(cmds, cmd)
	case tabStandings:
		var cmd tea.Cmd
		m.standings, cmd = m.standings.Update(msg)
		cmds = append(cmds, cmd)
	case tabCalendar:
		var cmd tea.Cmd
		m.calendar, cmd = m.calendar.Update(msg)
		cmds = append(cmds, cmd)
	case tabRaceDetail:
		var cmd tea.Cmd
		m.raceDetail, cmd = m.raceDetail.Update(msg)
		cmds = append(cmds, cmd)
	case tabDriver:
		var cmd tea.Cmd
		m.driver, cmd = m.driver.Update(msg)
		cmds = append(cmds, cmd)
	case tabTrackMap:
		var cmd tea.Cmd
		m.trackMap, cmd = m.trackMap.Update(msg)
		cmds = append(cmds, cmd)
	}

	return m, tea.Batch(cmds...)
}

func (m AppModel) View() string {
	if m.showSplash {
		return m.renderSplash()
	}

	w := m.width
	if w < 40 {
		w = 40
	}

	// Red accent stripe at the very top (F1 style)
	stripe := styleTabStripe.Render(strings.Repeat("▔", w))

	// Tab bar
	tabs := renderTabBar(m.activeTab, m.year, w)

	// Content area
	var content string
	switch m.activeTab {
	case tabDashboard:
		content = m.dashboard.View()
	case tabLive:
		content = m.live.View()
	case tabStandings:
		content = m.standings.View()
	case tabCalendar:
		content = m.calendar.View()
	case tabRaceDetail:
		content = m.raceDetail.View()
	case tabDriver:
		content = m.driver.View()
	case tabTrackMap:
		content = m.trackMap.View()
	}

	statusBar := m.renderStatusBar(w)

	// Calculate how much vertical space is available for content
	usedLines := 1 + 1 + 1 + 1 // stripe + tab bar + gap + status bar
	contentHeight := m.height - usedLines
	if contentHeight < 5 {
		contentHeight = 5
	}

	// Trim content to fit available height
	contentLines := strings.Split(content, "\n")
	if len(contentLines) > contentHeight {
		contentLines = contentLines[:contentHeight]
	}
	content = strings.Join(contentLines, "\n")

	return stripe + "\n" + tabs + "\n" + content + "\n" + statusBar
}

func renderTabBar(active tabIndex, year int, width int) string {
	// Build the logo
	logo := lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color(colorF1Red)).
		Background(lipgloss.Color(colorSurface0)).
		Padding(0, 1).
		Render("F1")

	// Build tabs
	var tabs []string
	for i, name := range tabNames {
		label := fmt.Sprintf(" %s %d %s ", tabIcons[i], i+1, name)
		if tabIndex(i) == active {
			tabs = append(tabs, styleActiveTab.Render(label))
		} else {
			tabs = append(tabs, styleInactiveTab.Render(label))
		}
	}

	// Year badge
	yearBadge := styleYearBadge.Render(fmt.Sprintf(" %d ", year))

	// Assemble: logo + tabs + spacer + year
	left := logo + strings.Join(tabs, "")
	leftWidth := lipgloss.Width(left)
	yearWidth := lipgloss.Width(yearBadge)
	spacerWidth := width - leftWidth - yearWidth
	if spacerWidth < 0 {
		spacerWidth = 0
	}
	spacer := lipgloss.NewStyle().
		Background(lipgloss.Color(colorSurface0)).
		Render(strings.Repeat(" ", spacerWidth))

	bar := left + spacer + yearBadge
	return styleTabBar.Render(bar)
}

func (m AppModel) renderStatusBar(width int) string {
	now := time.Now()

	// Left side: brand
	leftParts := []string{
		lipgloss.NewStyle().
			Foreground(lipgloss.Color(colorF1Red)).
			Bold(true).
			Render("BOX-BOX"),
	}

	if m.nextInfo != nil && m.nextInfo.meeting != nil {
		flag := countryFlag(m.nextInfo.meeting.CountryCode)
		raceName := styleStatusValue.Render(m.nextInfo.meeting.MeetingName)

		leftParts = append(leftParts, styleStatusLabel.Render("│"))

		switch {
		case m.nextInfo.current != nil:
			leftParts = append(leftParts,
				styleStatusLabel.Render("LIVE"),
				flag+" "+raceName,
				styleStatusLabel.Render("·"),
				styleStatusValue.Render(m.nextInfo.current.SessionName),
			)
		case m.nextInfo.next != nil:
			if start, err := sessionStartTime(*m.nextInfo.next); err == nil {
				diff := start.Sub(now)
				days := int(diff.Hours() / 24)
				hours := int(diff.Hours()) % 24
				leftParts = append(leftParts,
					styleStatusLabel.Render("NEXT"),
					flag+" "+raceName,
					styleStatusLabel.Render("·"),
					styleStatusValue.Render(m.nextInfo.next.SessionName),
					styleStatusLabel.Render("in"),
					styleCountdown.Render(fmt.Sprintf("%dd %dh", days, hours)),
				)
			}
		case meetingHasStarted(*m.nextInfo.meeting, now):
			leftParts = append(leftParts,
				styleStatusLabel.Render("DONE"),
				flag+" "+raceName,
			)
		default:
			if start, err := meetingStartTime(*m.nextInfo.meeting); err == nil {
				diff := start.Sub(now)
				days := int(diff.Hours() / 24)
				hours := int(diff.Hours()) % 24
				leftParts = append(leftParts,
					styleStatusLabel.Render("NEXT"),
					flag+" "+raceName,
					styleStatusLabel.Render("in"),
					styleCountdown.Render(fmt.Sprintf("%dd %dh", days, hours)),
				)
			}
		}
	}

	left := strings.Join(leftParts, " ")

	// Right side: cache stats + navigation hints
	cacheStats := m.client.CacheStats()
	cacheInfo := styleMuted.Render(fmt.Sprintf("cache %d/%d", cacheStats.Hits, cacheStats.Hits+cacheStats.Misses))
	right := cacheInfo + "  " + styleMuted.Render("1-7 tabs · y year · q quit")

	leftW := lipgloss.Width(left)
	rightW := lipgloss.Width(right)
	spacerW := width - leftW - rightW - 2
	if spacerW < 0 {
		spacerW = 0
	}

	bar := " " + left + strings.Repeat(" ", spacerW) + right + " "
	return styleStatusBar.Width(width).Render(bar)
}

func (m AppModel) renderSplash() string {
	w := m.width
	h := m.height
	if w == 0 {
		w = 80
	}
	if h == 0 {
		h = 24
	}

	// F1-themed ASCII logo
	logo := []string{
		"██████╗  ██████╗ ██╗  ██╗     ██████╗  ██████╗ ██╗  ██╗",
		"██╔══██╗██╔═══██╗╚██╗██╔╝     ██╔══██╗██╔═══██╗╚██╗██╔╝",
		"██████╔╝██║   ██║ ╚███╔╝█████╗██████╔╝██║   ██║ ╚███╔╝ ",
		"██╔══██╗██║   ██║ ██╔██╗╚════╝██╔══██╗██║   ██║ ██╔██╗ ",
		"██████╔╝╚██████╔╝██╔╝ ██╗     ██████╔╝╚██████╔╝██╔╝ ██╗",
		"╚═════╝  ╚═════╝ ╚═╝  ╚═╝     ╚═════╝  ╚═════╝ ╚═╝  ╚═╝",
	}

	redStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color(colorF1Red)).
		Bold(true)

	var logoBlock strings.Builder
	for _, line := range logo {
		logoBlock.WriteString(redStyle.Render(line) + "\n")
	}

	subtitle := lipgloss.NewStyle().
		Foreground(lipgloss.Color(colorMuted)).
		Render("Formula 1 Terminal Dashboard")

	loadingLine := fmt.Sprintf("%s  %s",
		m.splashSpinner.View(),
		styleMuted.Render("Loading data..."))

	hint := lipgloss.NewStyle().
		Foreground(lipgloss.Color(colorSurface2)).
		Render("press any key to skip")

	content := lipgloss.JoinVertical(lipgloss.Center,
		logoBlock.String(),
		"",
		subtitle,
		"",
		loadingLine,
		"",
		hint,
	)

	// Center the splash on screen
	return lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, content)
}
