use std::collections::HashMap;

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
    layout::{Constraint, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState},
    Frame,
};
use sqlx::sqlite::SqlitePool;
use tokio::sync::mpsc;

use crate::app::{AppMessage, NotifyLevel};
use crate::calc::matchup::MatchupResult;
use crate::config::Settings;
use crate::db::models::{BatterCard, Lineup, Player};
use crate::widgets::selector::{SelectorEvent, SelectorWidget};

// =============================================================================
// Focus & slot types
// =============================================================================

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GamedayFocus {
    TeamSelector,
    PitcherSelector,
    MatchupTable,
    LineupName,
    LineupTable,
    LoadSelector,
}

#[derive(Debug, Clone)]
pub struct LineupSlot {
    pub order: usize,
    pub player_id: Option<i64>,
    pub player_name: Option<String>,
    pub position: Option<String>,
    pub matchup_rating: Option<f64>,
    pub matchup_tier: Option<String>,
}

impl LineupSlot {
    pub fn empty(order: usize) -> Self {
        Self {
            order,
            player_id: None,
            player_name: None,
            position: None,
            matchup_rating: None,
            matchup_tier: None,
        }
    }

    pub fn is_empty(&self) -> bool {
        self.player_id.is_none()
    }
}

// =============================================================================
// Gameday state
// =============================================================================

pub struct GamedayState {
    pub team_abbrev: String,
    pub season: i64,

    // Selectors
    pub team_selector: SelectorWidget<i64>,
    pub pitcher_selector: SelectorWidget<i64>,
    pub load_selector: SelectorWidget<String>,

    // Data
    pub my_batters: Vec<(Player, Option<BatterCard>)>,
    pub matchup_results: Vec<MatchupResult>,

    // Matchup table
    pub matchup_table_state: TableState,

    // Lineup
    pub lineup_slots: [LineupSlot; 9],
    pub lineup_name: String,
    pub lineup_name_cursor: usize,
    pub saved_lineups: Vec<Lineup>,
    pub lineup_table_state: TableState,

    // Focus
    pub focus: GamedayFocus,

    // Loading flags
    pub is_loading_teams: bool,
    pub is_loading_pitchers: bool,
    pub is_calculating: bool,
    pub cache_ready: bool,
}

impl GamedayState {
    pub fn new(team_abbrev: String, season: i64) -> Self {
        let lineup_slots = std::array::from_fn(|i| LineupSlot::empty(i + 1));
        Self {
            team_abbrev,
            season,
            team_selector: SelectorWidget::new("Opponent"),
            pitcher_selector: SelectorWidget::new("Pitcher"),
            load_selector: SelectorWidget::new("Load Lineup"),
            my_batters: Vec::new(),
            matchup_results: Vec::new(),
            matchup_table_state: TableState::default(),
            lineup_slots,
            lineup_name: String::new(),
            lineup_name_cursor: 0,
            saved_lineups: Vec::new(),
            lineup_table_state: TableState::default().with_selected(0),
            focus: GamedayFocus::MatchupTable,
            is_loading_teams: true,
            is_loading_pitchers: false,
            is_calculating: false,
            cache_ready: false,
        }
    }

    /// Returns true if a text input or selector popup is capturing keys.
    pub fn is_input_captured(&self) -> bool {
        matches!(self.focus, GamedayFocus::LineupName)
            || self.team_selector.is_open
            || self.pitcher_selector.is_open
            || self.load_selector.is_open
    }

    // =========================================================================
    // Mount — fire background loads
    // =========================================================================

    pub fn mount(&mut self, pool: &SqlitePool, tx: &mpsc::UnboundedSender<AppMessage>) {
        // Ensure score cache
        let pool_c = pool.clone();
        let tx_c = tx.clone();
        tokio::spawn(async move {
            match crate::calc::score_cache::ensure_cache_exists(&pool_c).await {
                Ok(_) => {
                    let _ = tx_c.send(AppMessage::CacheReady);
                }
                Err(e) => {
                    let _ = tx_c.send(AppMessage::CacheError(format!("{e}")));
                }
            }
        });

        // Load teams
        let pool_c = pool.clone();
        let tx_c = tx.clone();
        let season = self.season;
        tokio::spawn(async move {
            match crate::db::queries::get_all_teams(&pool_c, season, true).await {
                Ok(teams) => {
                    let _ = tx_c.send(AppMessage::TeamsLoaded(teams));
                }
                Err(e) => {
                    tracing::error!("Failed to load teams: {e}");
                    let _ = tx_c.send(AppMessage::Notify(
                        format!("Failed to load teams: {e}"),
                        NotifyLevel::Error,
                    ));
                }
            }
        });

        // Load my batters
        let pool_c = pool.clone();
        let tx_c = tx.clone();
        let abbrev = self.team_abbrev.clone();
        let season = self.season;
        tokio::spawn(async move {
            let team = crate::db::queries::get_team_by_abbrev(&pool_c, &abbrev, season).await;
            if let Ok(Some(team)) = team {
                if let Ok(batters) =
                    crate::db::queries::get_batters(&pool_c, Some(team.id), Some(season)).await
                {
                    let mut with_cards = Vec::with_capacity(batters.len());
                    for p in batters {
                        let card = crate::db::queries::get_batter_card(&pool_c, p.id)
                            .await
                            .ok()
                            .flatten();
                        with_cards.push((p, card));
                    }
                    let _ = tx_c.send(AppMessage::MyBattersLoaded(with_cards));
                }
            }
        });

        // Load saved lineups
        let pool_c = pool.clone();
        let tx_c = tx.clone();
        tokio::spawn(async move {
            if let Ok(lineups) = crate::db::queries::get_lineups(&pool_c).await {
                let _ = tx_c.send(AppMessage::LineupsLoaded(lineups));
            }
        });
    }

    // =========================================================================
    // Key handling
    // =========================================================================

    pub fn handle_key(
        &mut self,
        key: KeyEvent,
        pool: &SqlitePool,
        _settings: &Settings,
        tx: &mpsc::UnboundedSender<AppMessage>,
    ) {
        // Try selectors first (they consume keys when open)
        if self.team_selector.is_open {
            if let SelectorEvent::Selected(_) = self.team_selector.handle_key(key) {
                self.on_team_selected(pool, tx);
            }
            return;
        }
        if self.pitcher_selector.is_open {
            if let SelectorEvent::Selected(_) = self.pitcher_selector.handle_key(key) {
                self.on_pitcher_selected(pool, tx);
            }
            return;
        }
        if self.load_selector.is_open {
            if let SelectorEvent::Selected(_) = self.load_selector.handle_key(key) {
                self.on_lineup_load_selected();
            }
            return;
        }

        // Lineup name text input
        if self.focus == GamedayFocus::LineupName {
            match key.code {
                KeyCode::Esc | KeyCode::Tab => {
                    self.focus = GamedayFocus::LineupTable;
                }
                KeyCode::Char(c) => {
                    self.lineup_name.insert(self.lineup_name_cursor, c);
                    self.lineup_name_cursor += 1;
                }
                KeyCode::Backspace => {
                    if self.lineup_name_cursor > 0 {
                        self.lineup_name_cursor -= 1;
                        self.lineup_name.remove(self.lineup_name_cursor);
                    }
                }
                KeyCode::Left => {
                    self.lineup_name_cursor = self.lineup_name_cursor.saturating_sub(1);
                }
                KeyCode::Right => {
                    if self.lineup_name_cursor < self.lineup_name.len() {
                        self.lineup_name_cursor += 1;
                    }
                }
                _ => {}
            }
            return;
        }

        // Ctrl-S: save lineup
        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
            self.save_lineup(pool, tx);
            return;
        }

        // Tab: cycle focus
        if key.code == KeyCode::Tab {
            self.cycle_focus();
            return;
        }

        // Focus-specific keys
        match self.focus {
            GamedayFocus::TeamSelector => match key.code {
                KeyCode::Enter | KeyCode::Char(' ') => {
                    self.team_selector.is_open = true;
                    self.team_selector.is_focused = true;
                }
                _ => {}
            },
            GamedayFocus::PitcherSelector => match key.code {
                KeyCode::Enter | KeyCode::Char(' ') => {
                    self.pitcher_selector.is_open = true;
                    self.pitcher_selector.is_focused = true;
                }
                _ => {}
            },
            GamedayFocus::MatchupTable => self.handle_matchup_table_key(key),
            GamedayFocus::LineupTable => self.handle_lineup_table_key(key, pool, tx),
            GamedayFocus::LoadSelector => match key.code {
                KeyCode::Enter | KeyCode::Char(' ') => {
                    self.load_selector.is_open = true;
                    self.load_selector.is_focused = true;
                }
                _ => {}
            },
            GamedayFocus::LineupName => {} // handled above
        }
    }

    fn cycle_focus(&mut self) {
        self.unfocus_all_selectors();
        self.focus = match self.focus {
            GamedayFocus::MatchupTable => GamedayFocus::LineupTable,
            GamedayFocus::LineupTable => GamedayFocus::LineupName,
            GamedayFocus::LineupName => GamedayFocus::TeamSelector,
            GamedayFocus::TeamSelector => GamedayFocus::PitcherSelector,
            GamedayFocus::PitcherSelector => GamedayFocus::LoadSelector,
            GamedayFocus::LoadSelector => GamedayFocus::MatchupTable,
        };
        self.update_selector_focus();
    }

    fn unfocus_all_selectors(&mut self) {
        self.team_selector.is_focused = false;
        self.pitcher_selector.is_focused = false;
        self.load_selector.is_focused = false;
    }

    fn update_selector_focus(&mut self) {
        self.team_selector.is_focused = self.focus == GamedayFocus::TeamSelector;
        self.pitcher_selector.is_focused = self.focus == GamedayFocus::PitcherSelector;
        self.load_selector.is_focused = self.focus == GamedayFocus::LoadSelector;
    }

    fn handle_matchup_table_key(&mut self, key: KeyEvent) {
        match key.code {
            KeyCode::Char('j') | KeyCode::Down => {
                let len = self.matchup_results.len();
                if len > 0 {
                    let i = self.matchup_table_state.selected().unwrap_or(0);
                    self.matchup_table_state.select(Some((i + 1) % len));
                }
            }
            KeyCode::Char('k') | KeyCode::Up => {
                let len = self.matchup_results.len();
                if len > 0 {
                    let i = self.matchup_table_state.selected().unwrap_or(0);
                    self.matchup_table_state
                        .select(Some(i.checked_sub(1).unwrap_or(len - 1)));
                }
            }
            KeyCode::Enter => {
                self.add_selected_to_lineup();
            }
            _ => {}
        }
    }

    fn handle_lineup_table_key(
        &mut self,
        key: KeyEvent,
        pool: &SqlitePool,
        tx: &mpsc::UnboundedSender<AppMessage>,
    ) {
        let sel = self.lineup_table_state.selected().unwrap_or(0);
        match key.code {
            KeyCode::Char('j') | KeyCode::Down => {
                if sel < 8 {
                    self.lineup_table_state.select(Some(sel + 1));
                }
            }
            KeyCode::Char('k') | KeyCode::Up => {
                self.lineup_table_state
                    .select(Some(sel.saturating_sub(1)));
            }
            KeyCode::Char('x') => {
                // Remove player from slot
                self.lineup_slots[sel] = LineupSlot::empty(sel + 1);
            }
            KeyCode::Char('p') => {
                // Cycle position
                self.cycle_position(sel);
            }
            KeyCode::Char('J') => {
                // Move slot down
                if sel < 8 {
                    self.lineup_slots.swap(sel, sel + 1);
                    // Fix order numbers
                    self.lineup_slots[sel].order = sel + 1;
                    self.lineup_slots[sel + 1].order = sel + 2;
                    self.lineup_table_state.select(Some(sel + 1));
                }
            }
            KeyCode::Char('K') => {
                // Move slot up
                if sel > 0 {
                    self.lineup_slots.swap(sel - 1, sel);
                    self.lineup_slots[sel - 1].order = sel;
                    self.lineup_slots[sel].order = sel + 1;
                    self.lineup_table_state.select(Some(sel - 1));
                }
            }
            KeyCode::Char('l') => {
                // Open load selector
                self.refresh_load_selector();
                self.focus = GamedayFocus::LoadSelector;
                self.load_selector.is_focused = true;
                self.load_selector.is_open = true;
            }
            KeyCode::Enter => {
                // Add from matchup table to this slot
                self.add_to_specific_slot(sel);
            }
            KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.save_lineup(pool, tx);
            }
            _ => {}
        }
    }

    // =========================================================================
    // Lineup operations
    // =========================================================================

    fn add_selected_to_lineup(&mut self) {
        let Some(idx) = self.matchup_table_state.selected() else {
            return;
        };
        let Some(result) = self.matchup_results.get(idx) else {
            return;
        };

        // Find first empty slot
        let slot_idx = self.lineup_slots.iter().position(|s| s.is_empty());
        let Some(slot_idx) = slot_idx else {
            return; // lineup full
        };

        // Don't add duplicates
        let pid = result.player.id;
        if self.lineup_slots.iter().any(|s| s.player_id == Some(pid)) {
            return;
        }

        self.lineup_slots[slot_idx] = LineupSlot {
            order: slot_idx + 1,
            player_id: Some(pid),
            player_name: Some(result.player.name.clone()),
            position: result.player.pos_1.clone(),
            matchup_rating: result.rating,
            matchup_tier: Some(result.tier.clone()),
        };
    }

    fn add_to_specific_slot(&mut self, slot_idx: usize) {
        let Some(matchup_idx) = self.matchup_table_state.selected() else {
            return;
        };
        let Some(result) = self.matchup_results.get(matchup_idx) else {
            return;
        };

        let pid = result.player.id;
        if self.lineup_slots.iter().any(|s| s.player_id == Some(pid)) {
            return;
        }

        self.lineup_slots[slot_idx] = LineupSlot {
            order: slot_idx + 1,
            player_id: Some(pid),
            player_name: Some(result.player.name.clone()),
            position: result.player.pos_1.clone(),
            matchup_rating: result.rating,
            matchup_tier: Some(result.tier.clone()),
        };
    }

    fn cycle_position(&mut self, slot_idx: usize) {
        let slot = &mut self.lineup_slots[slot_idx];
        let Some(pid) = slot.player_id else { return };

        // Get player's eligible positions
        let eligible: Vec<String> = self
            .my_batters
            .iter()
            .find(|(p, _)| p.id == pid)
            .map(|(p, _)| p.positions().iter().map(|s| s.to_string()).collect())
            .unwrap_or_default();

        if eligible.is_empty() {
            return;
        }

        let current = slot.position.as_deref().unwrap_or("");
        let current_idx = eligible.iter().position(|p| p == current).unwrap_or(0);
        let next_idx = (current_idx + 1) % eligible.len();
        slot.position = Some(eligible[next_idx].clone());
    }

    fn refresh_load_selector(&mut self) {
        let items: Vec<(String, String)> = self
            .saved_lineups
            .iter()
            .map(|l| (l.name.clone(), l.name.clone()))
            .collect();
        self.load_selector.set_items(items);
    }

    fn on_lineup_load_selected(&mut self) {
        let Some(name) = self.load_selector.selected_value().cloned() else {
            return;
        };
        let Some(lineup) = self.saved_lineups.iter().find(|l| l.name == name) else {
            return;
        };

        self.lineup_name = lineup.name.clone();
        self.lineup_name_cursor = self.lineup_name.len();

        let order = lineup.batting_order_vec();
        let positions_map = lineup.positions_map();

        // Clear all slots
        for i in 0..9 {
            self.lineup_slots[i] = LineupSlot::empty(i + 1);
        }

        // Populate from saved lineup
        for (i, pid) in order.iter().enumerate() {
            if i >= 9 {
                break;
            }
            let player = self.my_batters.iter().find(|(p, _)| p.id == *pid);
            if let Some((p, _)) = player {
                // Find position from map or default to pos_1
                let pos = positions_map
                    .iter()
                    .find(|(_, v)| **v == *pid)
                    .map(|(k, _)| k.clone())
                    .or_else(|| p.pos_1.clone());

                // Find matchup data if available
                let matchup = self.matchup_results.iter().find(|r| r.player.id == *pid);

                self.lineup_slots[i] = LineupSlot {
                    order: i + 1,
                    player_id: Some(*pid),
                    player_name: Some(p.name.clone()),
                    position: pos,
                    matchup_rating: matchup.and_then(|m| m.rating),
                    matchup_tier: matchup.map(|m| m.tier.clone()),
                };
            }
        }
        self.focus = GamedayFocus::LineupTable;
    }

    fn save_lineup(&self, pool: &SqlitePool, tx: &mpsc::UnboundedSender<AppMessage>) {
        if self.lineup_name.is_empty() {
            let _ = tx.send(AppMessage::Notify(
                "Enter a lineup name first".to_string(),
                NotifyLevel::Error,
            ));
            return;
        }

        let batting_order: Vec<i64> = self
            .lineup_slots
            .iter()
            .filter_map(|s| s.player_id)
            .collect();

        let mut positions: HashMap<String, i64> = HashMap::new();
        for slot in &self.lineup_slots {
            if let (Some(pid), Some(pos)) = (slot.player_id, &slot.position) {
                positions.insert(pos.clone(), pid);
            }
        }

        let pitcher_id = self.pitcher_selector.selected_value().copied();
        let name = self.lineup_name.clone();
        let pool = pool.clone();
        let tx = tx.clone();

        tokio::spawn(async move {
            match crate::db::queries::save_lineup(
                &pool,
                &name,
                &batting_order,
                &positions,
                "gameday",
                None,
                pitcher_id,
            )
            .await
            {
                Ok(_) => {
                    let _ = tx.send(AppMessage::LineupSaved(name));
                }
                Err(e) => {
                    let _ = tx.send(AppMessage::LineupSaveError(format!("{e}")));
                }
            }
        });
    }

    // =========================================================================
    // Cascading selectors
    // =========================================================================

    fn on_team_selected(&mut self, pool: &SqlitePool, tx: &mpsc::UnboundedSender<AppMessage>) {
        let Some(&team_id) = self.team_selector.selected_value() else {
            return;
        };

        // Clear pitcher selector and matchups
        self.pitcher_selector.set_items(Vec::new());
        self.matchup_results.clear();
        self.is_loading_pitchers = true;

        let pool = pool.clone();
        let tx = tx.clone();
        let season = self.season;
        tokio::spawn(async move {
            match crate::db::queries::get_pitchers(&pool, Some(team_id), Some(season)).await {
                Ok(pitchers) => {
                    let _ = tx.send(AppMessage::PitchersLoaded(pitchers));
                }
                Err(e) => {
                    tracing::error!("Failed to load pitchers: {e}");
                    let _ = tx.send(AppMessage::Notify(
                        format!("Failed to load pitchers: {e}"),
                        NotifyLevel::Error,
                    ));
                }
            }
        });
    }

    fn on_pitcher_selected(&mut self, pool: &SqlitePool, tx: &mpsc::UnboundedSender<AppMessage>) {
        let Some(&pitcher_id) = self.pitcher_selector.selected_value() else {
            return;
        };

        self.is_calculating = true;
        self.matchup_results.clear();

        let pool = pool.clone();
        let tx = tx.clone();
        let batters = self.my_batters.clone();
        tokio::spawn(async move {
            // Get pitcher and card
            let pitcher = crate::db::queries::get_player_by_id(&pool, pitcher_id).await;
            let Ok(Some(pitcher)) = pitcher else { return };
            let pitcher_card = crate::db::queries::get_pitcher_card(&pool, pitcher_id)
                .await
                .ok()
                .flatten();

            match crate::calc::matchup::calculate_team_matchups_cached(
                &pool,
                &batters,
                &pitcher,
                pitcher_card.as_ref(),
            )
            .await
            {
                Ok(results) => {
                    let _ = tx.send(AppMessage::MatchupsCalculated(results));
                }
                Err(e) => {
                    tracing::error!("Matchup calculation failed: {e}");
                    let _ = tx.send(AppMessage::Notify(
                        format!("Matchup calculation failed: {e}"),
                        NotifyLevel::Error,
                    ));
                }
            }
        });
    }

    // =========================================================================
    // Message handling
    // =========================================================================

    pub fn handle_message(&mut self, msg: AppMessage) {
        match msg {
            AppMessage::CacheReady => {
                self.cache_ready = true;
            }
            AppMessage::CacheError(e) => {
                self.cache_ready = false;
                // Cache error is non-fatal; real-time calc will still work
                tracing::warn!("Cache error: {e}");
            }
            AppMessage::TeamsLoaded(teams) => {
                self.is_loading_teams = false;
                let items: Vec<(String, i64)> = teams
                    .into_iter()
                    .filter(|t| t.abbrev != self.team_abbrev)
                    .map(|t| (format!("{} ({})", t.abbrev, t.short_name), t.id))
                    .collect();
                self.team_selector.set_items(items);
            }
            AppMessage::MyBattersLoaded(batters) => {
                self.my_batters = batters;
            }
            AppMessage::LineupsLoaded(lineups) => {
                self.saved_lineups = lineups;
            }
            AppMessage::PitchersLoaded(pitchers) => {
                self.is_loading_pitchers = false;
                let items: Vec<(String, i64)> = pitchers
                    .into_iter()
                    .map(|p| {
                        let hand = p.hand.as_deref().unwrap_or("?");
                        (format!("{} ({})", p.name, hand), p.id)
                    })
                    .collect();
                self.pitcher_selector.set_items(items);
            }
            AppMessage::MatchupsCalculated(results) => {
                self.is_calculating = false;
                self.matchup_results = results;
                if !self.matchup_results.is_empty() {
                    self.matchup_table_state.select(Some(0));
                }
            }
            AppMessage::LineupSaved(name) => {
                // Refresh saved lineups list
                let _ = name; // name used for notification in Notify
            }
            AppMessage::LineupSaveError(_) => {}
            _ => {}
        }
    }

    // =========================================================================
    // Rendering
    // =========================================================================

    pub fn render(&self, frame: &mut Frame, area: Rect, tick_count: u64) {
        // 60/40 horizontal split
        let panels = Layout::horizontal([Constraint::Percentage(60), Constraint::Percentage(40)])
            .split(area);

        self.render_left_panel(frame, panels[0], tick_count);
        self.render_right_panel(frame, panels[1], tick_count);

        // Render popup overlays last (on top)
        // Team selector popup anchored to its closed position
        if self.team_selector.is_open {
            let left_chunks = Layout::vertical([
                Constraint::Length(3),
                Constraint::Length(3),
                Constraint::Min(0),
            ])
            .split(panels[0]);
            self.team_selector.render_popup(frame, left_chunks[0]);
        }
        if self.pitcher_selector.is_open {
            let left_chunks = Layout::vertical([
                Constraint::Length(3),
                Constraint::Length(3),
                Constraint::Min(0),
            ])
            .split(panels[0]);
            self.pitcher_selector.render_popup(frame, left_chunks[1]);
        }
        if self.load_selector.is_open {
            let right_chunks = Layout::vertical([
                Constraint::Length(3),
                Constraint::Length(3),
                Constraint::Min(0),
            ])
            .split(panels[1]);
            self.load_selector.render_popup(frame, right_chunks[0]);
        }
    }

    fn render_left_panel(&self, frame: &mut Frame, area: Rect, tick_count: u64) {
        let chunks = Layout::vertical([
            Constraint::Length(3), // team selector
            Constraint::Length(3), // pitcher selector
            Constraint::Min(0),   // matchup table
        ])
        .split(area);

        self.team_selector.render_closed(frame, chunks[0]);
        self.pitcher_selector.render_closed(frame, chunks[1]);
        self.render_matchup_table(frame, chunks[2], tick_count);
    }

    fn render_matchup_table(&self, frame: &mut Frame, area: Rect, tick_count: u64) {
        let is_focused = self.focus == GamedayFocus::MatchupTable;
        let border_style = if is_focused {
            Style::default().fg(Color::Yellow)
        } else {
            Style::default()
        };

        if self.is_calculating {
            let spinner = ['|', '/', '-', '\\'][(tick_count as usize / 2) % 4];
            let widget = Paragraph::new(format!("{spinner} Calculating matchups..."))
                .block(
                    Block::default()
                        .borders(Borders::ALL)
                        .title("Matchups")
                        .border_style(border_style),
                );
            frame.render_widget(widget, area);
            return;
        }

        if self.matchup_results.is_empty() {
            let msg = if self.pitcher_selector.selected_idx.is_some() {
                "No matchup data"
            } else {
                "Select a team and pitcher"
            };
            let widget = Paragraph::new(msg).block(
                Block::default()
                    .borders(Borders::ALL)
                    .title("Matchups")
                    .border_style(border_style),
            );
            frame.render_widget(widget, area);
            return;
        }

        let lineup_pids: Vec<i64> = self
            .lineup_slots
            .iter()
            .filter_map(|s| s.player_id)
            .collect();

        let header = Row::new(vec!["", "Name", "Hand", "Pos", "Tier", "Rating"])
            .style(Style::default().add_modifier(Modifier::BOLD));

        let rows: Vec<Row> = self
            .matchup_results
            .iter()
            .map(|r| {
                let in_lineup = if lineup_pids.contains(&r.player.id) {
                    "*"
                } else {
                    " "
                };
                let hand = r.batter_hand.as_str();
                let pos = r.player.pos_1.as_deref().unwrap_or("-");
                Row::new(vec![
                    Cell::from(in_lineup),
                    Cell::from(r.player.name.as_str()),
                    Cell::from(hand),
                    Cell::from(pos),
                    Cell::from(r.tier.as_str()),
                    Cell::from(r.rating_display()),
                ])
            })
            .collect();

        let table = Table::new(
            rows,
            [
                Constraint::Length(1),
                Constraint::Min(15),
                Constraint::Length(4),
                Constraint::Length(3),
                Constraint::Length(4),
                Constraint::Length(7),
            ],
        )
        .header(header)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .title("Matchups")
                .border_style(border_style),
        )
        .row_highlight_style(Style::default().bg(Color::DarkGray));

        frame.render_stateful_widget(table, area, &mut self.matchup_table_state.clone());
    }

    fn render_right_panel(&self, frame: &mut Frame, area: Rect, _tick_count: u64) {
        let chunks = Layout::vertical([
            Constraint::Length(3), // lineup name / load selector
            Constraint::Min(0),   // lineup table
            Constraint::Length(1), // key hints
        ])
        .split(area);

        self.render_lineup_header(frame, chunks[0]);
        self.render_lineup_table(frame, chunks[1]);
        self.render_lineup_hints(frame, chunks[2]);
    }

    fn render_lineup_header(&self, frame: &mut Frame, area: Rect) {
        let cols = Layout::horizontal([Constraint::Percentage(60), Constraint::Percentage(40)])
            .split(area);

        // Lineup name input
        let name_focused = self.focus == GamedayFocus::LineupName;
        let name_style = if name_focused {
            Style::default().fg(Color::Yellow)
        } else {
            Style::default()
        };
        let display = if self.lineup_name.is_empty() {
            "untitled".to_string()
        } else {
            self.lineup_name.clone()
        };
        let cursor_line = if name_focused {
            let mut s = display.clone();
            if self.lineup_name_cursor <= s.len() {
                s.insert(self.lineup_name_cursor, '|');
            }
            s
        } else {
            display
        };
        let name_widget = Paragraph::new(cursor_line)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .title("Lineup Name")
                    .border_style(name_style),
            );
        frame.render_widget(name_widget, cols[0]);

        // Load selector
        self.load_selector.render_closed(frame, cols[1]);
    }

    fn render_lineup_table(&self, frame: &mut Frame, area: Rect) {
        let is_focused = self.focus == GamedayFocus::LineupTable;
        let border_style = if is_focused {
            Style::default().fg(Color::Yellow)
        } else {
            Style::default()
        };

        let header = Row::new(vec!["#", "Name", "Pos", "Tier", "Rating"])
            .style(Style::default().add_modifier(Modifier::BOLD));

        let rows: Vec<Row> = self
            .lineup_slots
            .iter()
            .map(|slot| {
                let order = format!("{}", slot.order);
                if slot.is_empty() {
                    Row::new(vec![
                        Cell::from(order),
                        Cell::from("---"),
                        Cell::from("-"),
                        Cell::from("-"),
                        Cell::from("-"),
                    ])
                    .style(Style::default().fg(Color::DarkGray))
                } else {
                    let name = slot.player_name.as_deref().unwrap_or("?");
                    let pos = slot.position.as_deref().unwrap_or("-");
                    let tier = slot.matchup_tier.as_deref().unwrap_or("-");
                    let rating = slot
                        .matchup_rating
                        .map(|r| {
                            if r >= 0.0 {
                                format!("+{:.0}", r)
                            } else {
                                format!("{:.0}", r)
                            }
                        })
                        .unwrap_or_else(|| "-".to_string());
                    Row::new(vec![
                        Cell::from(order),
                        Cell::from(name),
                        Cell::from(pos),
                        Cell::from(tier),
                        Cell::from(rating),
                    ])
                }
            })
            .collect();

        let table = Table::new(
            rows,
            [
                Constraint::Length(2),
                Constraint::Min(12),
                Constraint::Length(3),
                Constraint::Length(4),
                Constraint::Length(7),
            ],
        )
        .header(header)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .title("Lineup")
                .border_style(border_style),
        )
        .row_highlight_style(Style::default().bg(Color::DarkGray));

        frame.render_stateful_widget(table, area, &mut self.lineup_table_state.clone());
    }

    fn render_lineup_hints(&self, frame: &mut Frame, area: Rect) {
        let hints = match self.focus {
            GamedayFocus::MatchupTable => {
                " Enter:add  j/k:nav  Tab:lineup"
            }
            GamedayFocus::LineupTable => {
                " x:remove  p:pos  J/K:move  l:load  Ctrl-S:save"
            }
            GamedayFocus::LineupName => {
                " Type name  Tab/Esc:done"
            }
            _ => " Tab:cycle focus",
        };
        let line = Line::from(vec![Span::styled(
            hints,
            Style::default().fg(Color::DarkGray),
        )]);
        let widget = Paragraph::new(line);
        frame.render_widget(widget, area);
    }
}
