use crate::app::{Action, Screen};
use crate::beatmap::types::{Beatmap, Difficulty};
use crate::game::modifiers::Mods;
use crate::game::practice::{self, PracticeSettings};
use crate::score_store::ScoreStore;
use crate::ui::chrome::{
    difficulty_color, render_bottom_bar, render_difficulty_dots, render_top_bar,
};
use crossterm::event::KeyCode;
use ratatui::prelude::*;
use std::path::{Path, PathBuf};

pub struct SongEntry {
    pub title: String,
    pub artist: String,
    pub dir: PathBuf,
    pub slug: String,
    pub bpm: u32,
    pub duration_ms: u64,
    /// Filesystem mtime in seconds since epoch — used for "Recently added" sort.
    pub added_secs: u64,
    /// Note counts per Difficulty (Easy, Medium, Hard, Expert).
    pub note_counts: [u32; 4],
    pub present: [bool; 4],
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortMode {
    Title,
    Artist,
    Added,
    Bpm,
}

impl SortMode {
    pub fn label(self) -> &'static str {
        match self {
            SortMode::Title => "Title",
            SortMode::Artist => "Artist",
            SortMode::Added => "Recently added",
            SortMode::Bpm => "BPM",
        }
    }

    pub fn cycle(self) -> SortMode {
        match self {
            SortMode::Title => SortMode::Artist,
            SortMode::Artist => SortMode::Added,
            SortMode::Added => SortMode::Bpm,
            SortMode::Bpm => SortMode::Title,
        }
    }
}

pub struct SongSelectScreen {
    pub songs: Vec<SongEntry>,
    pub selected: usize,
    pub difficulty: Difficulty,
    pub import_mode: bool,
    pub import_input: String,
    pub import_status: Option<String>,
    pub search_mode: bool,
    pub search_query: String,
    /// Indices of songs matching the query (or all songs if no query).
    pub filtered_indices: Vec<usize>,
    pub scores: ScoreStore,
    pub sort_mode: SortMode,
    pub rename_mode: bool,
    pub rename_buf: String,
    pub rename_field: RenameField,
    pub mods: Mods,
    pub mods_overlay: bool,
    pub practice: PracticeSettings,
    pub practice_overlay: bool,
    pub practice_focus: PracticeField,
    /// Digits-only buffer for the focused MM:SS field while typing.
    pub practice_buf: String,
    pub practice_error: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenameField {
    Title,
    Artist,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PracticeField {
    Start,
    End,
    Speed,
}

impl SongSelectScreen {
    pub fn new(difficulty: Difficulty) -> Self {
        Self {
            songs: Vec::new(),
            selected: 0,
            difficulty,
            import_mode: false,
            import_input: String::new(),
            import_status: None,
            search_mode: false,
            search_query: String::new(),
            filtered_indices: Vec::new(),
            scores: ScoreStore::default(),
            sort_mode: SortMode::Title,
            rename_mode: false,
            rename_buf: String::new(),
            rename_field: RenameField::Title,
            mods: Mods::new(),
            mods_overlay: false,
            practice: PracticeSettings::new(),
            practice_overlay: false,
            practice_focus: PracticeField::Start,
            practice_buf: String::new(),
            practice_error: None,
        }
    }

    pub fn load_scores(&mut self, path: &Path) {
        self.scores = ScoreStore::load(path);
    }

    pub fn scan_songs(&mut self, songs_dir: &Path) {
        self.songs.clear();
        if !songs_dir.exists() {
            self.rebuild_filter();
            return;
        }

        let Ok(entries) = std::fs::read_dir(songs_dir) else {
            self.rebuild_filter();
            return;
        };

        for entry in entries.flatten() {
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }

            // Presence of each difficulty file.
            let diffs = Difficulty::all();
            let mut present = [false; 4];
            let mut note_counts = [0u32; 4];
            let mut bpm = 0u32;
            let mut duration_ms = 0u64;

            for (i, d) in diffs.iter().enumerate() {
                let p = path.join(d.filename());
                if !p.exists() {
                    continue;
                }
                present[i] = true;
                if let Ok(s) = std::fs::read_to_string(&p)
                    && let Ok(bm) = serde_json::from_str::<Beatmap>(&s)
                {
                    note_counts[i] = bm.notes.len() as u32;
                    if bpm == 0 {
                        bpm = bm.song.bpm;
                    }
                    if duration_ms == 0 {
                        duration_ms = bm.song.duration_ms;
                    }
                }
            }
            if !present.iter().any(|&p| p) {
                continue;
            }
            if find_audio_file(&path).is_none() {
                continue;
            }

            // Metadata
            let meta_path = path.join("metadata.json");
            let (title, artist) = if let Ok(c) = std::fs::read_to_string(&meta_path) {
                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&c) {
                    (
                        v["title"].as_str().unwrap_or("Unknown").to_string(),
                        v["artist"].as_str().unwrap_or("").to_string(),
                    )
                } else {
                    (
                        path.file_name()
                            .unwrap_or_default()
                            .to_string_lossy()
                            .to_string(),
                        String::new(),
                    )
                }
            } else {
                (
                    path.file_name()
                        .unwrap_or_default()
                        .to_string_lossy()
                        .to_string(),
                    String::new(),
                )
            };

            let slug = path
                .file_name()
                .unwrap_or_default()
                .to_string_lossy()
                .to_string();

            let added_secs = std::fs::metadata(&path)
                .and_then(|m| m.modified())
                .ok()
                .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
                .map(|d| d.as_secs())
                .unwrap_or(0);

            self.songs.push(SongEntry {
                title,
                artist,
                dir: path,
                slug,
                bpm,
                duration_ms,
                added_secs,
                note_counts,
                present,
            });
        }

        self.apply_sort();
        self.rebuild_filter();
    }

    fn apply_sort(&mut self) {
        match self.sort_mode {
            SortMode::Title => self.songs.sort_by_key(|a| a.title.to_lowercase()),
            SortMode::Artist => self.songs.sort_by(|a, b| {
                let ka = (a.artist.to_lowercase(), a.title.to_lowercase());
                let kb = (b.artist.to_lowercase(), b.title.to_lowercase());
                ka.cmp(&kb)
            }),
            SortMode::Added => self.songs.sort_by_key(|a| std::cmp::Reverse(a.added_secs)),
            SortMode::Bpm => self.songs.sort_by_key(|a| a.bpm),
        }
    }

    pub fn cycle_sort(&mut self) {
        self.sort_mode = self.sort_mode.cycle();
        self.apply_sort();
        self.rebuild_filter();
    }

    fn rebuild_filter(&mut self) {
        self.filtered_indices.clear();
        let q = self.search_query.to_lowercase();
        for (i, s) in self.songs.iter().enumerate() {
            if q.is_empty()
                || s.title.to_lowercase().contains(&q)
                || s.artist.to_lowercase().contains(&q)
            {
                self.filtered_indices.push(i);
            }
        }
        if self.selected >= self.filtered_indices.len() {
            self.selected = 0;
        }
    }

    fn real_index(&self) -> Option<usize> {
        self.filtered_indices.get(self.selected).copied()
    }

    pub fn selected_beatmap_path(&self) -> Option<PathBuf> {
        self.real_index()
            .and_then(|i| self.songs.get(i))
            .map(|s| s.dir.join(self.difficulty.filename()))
    }

    pub fn selected_audio_path(&self) -> Option<PathBuf> {
        self.real_index()
            .and_then(|i| self.songs.get(i))
            .and_then(|s| find_audio_file(&s.dir))
    }

    pub fn selected_song_title(&self) -> String {
        self.real_index()
            .and_then(|i| self.songs.get(i))
            .map(|s| {
                if s.artist.is_empty() {
                    s.title.clone()
                } else {
                    format!("{} — {}", s.title, s.artist)
                }
            })
            .unwrap_or_default()
    }

    /// Handle raw key for search-mode typing. Returns true if the key was consumed.
    pub fn handle_search_key(&mut self, code: KeyCode) -> bool {
        if !self.search_mode {
            return false;
        }
        match code {
            KeyCode::Char(c) => {
                self.search_query.push(c);
                self.rebuild_filter();
                true
            }
            KeyCode::Backspace => {
                self.search_query.pop();
                self.rebuild_filter();
                true
            }
            KeyCode::Esc => {
                self.search_mode = false;
                self.search_query.clear();
                self.rebuild_filter();
                true
            }
            KeyCode::Enter => {
                self.search_mode = false;
                true
            }
            _ => false,
        }
    }

    pub fn handle_action(&mut self, action: Action) -> Option<Action> {
        if self.import_mode
            || self.search_mode
            || self.rename_mode
            || self.mods_overlay
            || self.practice_overlay
        {
            return None;
        }
        match action {
            Action::MenuUp => {
                if self.selected > 0 {
                    self.selected -= 1;
                }
                None
            }
            Action::MenuDown => {
                if self.selected + 1 < self.filtered_indices.len() {
                    self.selected += 1;
                }
                None
            }
            Action::MenuSelect => {
                if !self.filtered_indices.is_empty() {
                    Some(Action::Navigate(Screen::Gameplay))
                } else {
                    None
                }
            }
            Action::Tab => {
                self.cycle_difficulty();
                None
            }
            Action::Sort => {
                self.cycle_sort();
                None
            }
            Action::Rename => {
                self.start_rename();
                None
            }
            Action::Mods => {
                self.mods_overlay = true;
                None
            }
            Action::Practice => {
                self.open_practice_overlay();
                None
            }
            Action::Delete => {
                if let Some(idx) = self.real_index() {
                    let song = &self.songs[idx];
                    let _ = std::fs::remove_dir_all(&song.dir);
                    self.songs.remove(idx);
                    self.rebuild_filter();
                    self.import_status = Some("Song deleted".to_string());
                }
                None
            }
            Action::Back | Action::Pause => Some(Action::Navigate(Screen::Menu)),
            _ => None,
        }
    }

    fn start_rename(&mut self) {
        let Some(idx) = self.real_index() else { return };
        let song = &self.songs[idx];
        self.rename_field = RenameField::Title;
        self.rename_buf = song.title.clone();
        self.rename_mode = true;
    }

    pub fn handle_rename_key(&mut self, code: KeyCode) -> bool {
        if !self.rename_mode {
            return false;
        }
        match code {
            KeyCode::Char(c) => {
                self.rename_buf.push(c);
                true
            }
            KeyCode::Backspace => {
                self.rename_buf.pop();
                true
            }
            KeyCode::Tab => {
                self.commit_rename_field();
                self.rename_field = match self.rename_field {
                    RenameField::Title => RenameField::Artist,
                    RenameField::Artist => RenameField::Title,
                };
                self.rename_buf = self
                    .real_index()
                    .and_then(|i| self.songs.get(i))
                    .map(|s| match self.rename_field {
                        RenameField::Title => s.title.clone(),
                        RenameField::Artist => s.artist.clone(),
                    })
                    .unwrap_or_default();
                true
            }
            KeyCode::Enter => {
                self.commit_rename_field();
                self.persist_rename();
                self.rename_mode = false;
                true
            }
            KeyCode::Esc => {
                self.rename_mode = false;
                self.rename_buf.clear();
                true
            }
            _ => false,
        }
    }

    fn open_practice_overlay(&mut self) {
        // Prefill an unset end to the current song's duration so the overlay
        // opens in a valid state (full song, no override).
        if self.practice.section_end_ms.is_none()
            && let Some(duration) = self.selected_song_duration_ms()
        {
            self.practice.section_end_ms = Some(duration);
        }
        if self.practice.section_start_ms.is_none() {
            self.practice.section_start_ms = Some(0);
        }
        self.practice_focus = PracticeField::Start;
        self.practice_buf = digits_for_mmss(self.practice.section_start_ms.unwrap_or(0));
        self.practice_error = None;
        self.practice_overlay = true;
    }

    fn close_practice_overlay(&mut self, apply: bool) {
        if apply {
            self.commit_practice_buf();
            if let Some(err) = self.validate_practice() {
                self.practice_error = Some(err);
                return;
            }
            // Collapse a "full song / no speed change" config back to inactive
            // so the badge doesn't stay on when user effectively cleared it.
            if self.practice_looks_like_default() {
                self.practice.clear();
            }
        }
        self.practice_overlay = false;
        self.practice_error = None;
        self.practice_buf.clear();
    }

    fn practice_looks_like_default(&self) -> bool {
        let start_zero = self.practice.section_start_ms == Some(0);
        let end_at_duration = match (
            self.practice.section_end_ms,
            self.selected_song_duration_ms(),
        ) {
            (Some(end), Some(dur)) => end >= dur,
            _ => false,
        };
        let speed_default = (self.practice.speed - 1.0).abs() < 0.001;
        start_zero && end_at_duration && speed_default
    }

    fn commit_practice_buf(&mut self) {
        match self.practice_focus {
            PracticeField::Start => {
                if let Some(ms) = mmss_from_digits(&self.practice_buf) {
                    self.practice.section_start_ms = Some(ms);
                }
            }
            PracticeField::End => {
                if let Some(ms) = mmss_from_digits(&self.practice_buf) {
                    self.practice.section_end_ms = Some(ms);
                }
            }
            PracticeField::Speed => {
                if let Ok(v) = self.practice_buf.parse::<f32>() {
                    self.practice.speed = practice::clamp_speed(v);
                }
            }
        }
    }

    fn validate_practice(&self) -> Option<String> {
        let duration = self.selected_song_duration_ms();
        let start = self.practice.section_start_ms.unwrap_or(0);
        let end = self.practice.section_end_ms.unwrap_or(u64::MAX);
        if let Some(d) = duration
            && start >= d
        {
            return Some(format!(
                "Start ({}) is past song length ({})",
                practice::format_mmss(start),
                practice::format_mmss(d),
            ));
        }
        if end <= start {
            return Some("Section end must be after start".to_string());
        }
        None
    }

    /// Arrow-key nudging. Start/End step by one second; Speed steps by 0.05.
    /// `dir` is `-1` for left, `+1` for right.
    fn nudge_focused(&mut self, dir: i64) {
        const STEP_MS: i64 = 1_000;
        let duration = self.selected_song_duration_ms();
        match self.practice_focus {
            PracticeField::Start => {
                let cur = self.practice.section_start_ms.unwrap_or(0) as i64;
                let upper = self
                    .practice
                    .section_end_ms
                    .map(|e| e.saturating_sub(1_000) as i64)
                    .unwrap_or(i64::MAX);
                let next = (cur + dir * STEP_MS).max(0).min(upper) as u64;
                self.practice.section_start_ms = Some(next);
            }
            PracticeField::End => {
                let cur = self.practice.section_end_ms.or(duration).unwrap_or(0) as i64;
                let lower = self
                    .practice
                    .section_start_ms
                    .map(|s| (s + 1_000) as i64)
                    .unwrap_or(1_000);
                let upper = duration.map(|d| d as i64).unwrap_or(i64::MAX);
                let next = (cur + dir * STEP_MS).max(lower).min(upper) as u64;
                self.practice.section_end_ms = Some(next);
            }
            PracticeField::Speed => {
                self.practice.step_speed(dir as f32 * practice::SPEED_STEP);
            }
        }
    }

    fn load_buf_for_focus(&mut self) {
        self.practice_buf = match self.practice_focus {
            PracticeField::Start => digits_for_mmss(self.practice.section_start_ms.unwrap_or(0)),
            PracticeField::End => digits_for_mmss(
                self.practice
                    .section_end_ms
                    .or(self.selected_song_duration_ms())
                    .unwrap_or(0),
            ),
            PracticeField::Speed => format!("{:.2}", self.practice.speed),
        };
    }

    fn selected_song_duration_ms(&self) -> Option<u64> {
        self.real_index()
            .and_then(|i| self.songs.get(i))
            .map(|s| s.duration_ms)
    }

    pub fn handle_practice_key(&mut self, code: KeyCode) -> bool {
        if !self.practice_overlay {
            return false;
        }
        self.practice_error = None;
        match code {
            KeyCode::Esc => {
                self.close_practice_overlay(false);
                true
            }
            KeyCode::Enter => {
                self.close_practice_overlay(true);
                true
            }
            KeyCode::Char('c') | KeyCode::Char('C') => {
                self.practice.clear();
                self.load_buf_for_focus();
                true
            }
            KeyCode::Tab | KeyCode::Down => {
                self.commit_practice_buf();
                self.practice_focus = match self.practice_focus {
                    PracticeField::Start => PracticeField::End,
                    PracticeField::End => PracticeField::Speed,
                    PracticeField::Speed => PracticeField::Start,
                };
                self.load_buf_for_focus();
                true
            }
            KeyCode::BackTab | KeyCode::Up => {
                self.commit_practice_buf();
                self.practice_focus = match self.practice_focus {
                    PracticeField::Start => PracticeField::Speed,
                    PracticeField::End => PracticeField::Start,
                    PracticeField::Speed => PracticeField::End,
                };
                self.load_buf_for_focus();
                true
            }
            KeyCode::Left => {
                self.commit_practice_buf();
                self.nudge_focused(-1);
                self.load_buf_for_focus();
                true
            }
            KeyCode::Right => {
                self.commit_practice_buf();
                self.nudge_focused(1);
                self.load_buf_for_focus();
                true
            }
            KeyCode::Backspace => {
                self.practice_buf.pop();
                true
            }
            KeyCode::Char(ch) => {
                match self.practice_focus {
                    PracticeField::Start | PracticeField::End => {
                        if ch.is_ascii_digit() && self.practice_buf.len() < 4 {
                            self.practice_buf.push(ch);
                        }
                    }
                    PracticeField::Speed => {
                        if (ch.is_ascii_digit() || ch == '.') && self.practice_buf.len() < 5 {
                            self.practice_buf.push(ch);
                        }
                    }
                }
                true
            }
            _ => true,
        }
    }

    pub fn handle_mods_key(&mut self, code: KeyCode) -> bool {
        if !self.mods_overlay {
            return false;
        }
        use crate::game::modifiers::Modifier;
        match code {
            KeyCode::Char('h') | KeyCode::Char('H') => {
                self.mods.toggle(Modifier::Hidden);
                true
            }
            KeyCode::Char('f') | KeyCode::Char('F') => {
                self.mods.toggle(Modifier::Flashlight);
                true
            }
            KeyCode::Char('s') | KeyCode::Char('S') => {
                self.mods.toggle(Modifier::SuddenDeath);
                true
            }
            KeyCode::Char('p') | KeyCode::Char('P') => {
                self.mods.toggle(Modifier::PerfectOnly);
                true
            }
            KeyCode::Char('c') | KeyCode::Char('C') => {
                self.mods = crate::game::modifiers::Mods::new();
                true
            }
            KeyCode::Esc | KeyCode::Enter | KeyCode::Char('m') => {
                self.mods_overlay = false;
                true
            }
            _ => false,
        }
    }

    fn commit_rename_field(&mut self) {
        let Some(idx) = self.real_index() else { return };
        let song = &mut self.songs[idx];
        match self.rename_field {
            RenameField::Title => song.title = self.rename_buf.clone(),
            RenameField::Artist => song.artist = self.rename_buf.clone(),
        }
    }

    fn persist_rename(&mut self) {
        let Some(idx) = self.real_index() else { return };
        let song = &self.songs[idx];
        let _ = crate::audio::import::rename_song(&song.dir, &song.title, &song.artist);
        self.import_status = Some(format!("Renamed: {} — {}", song.artist, song.title));
        self.apply_sort();
        self.rebuild_filter();
    }

    pub fn cycle_difficulty(&mut self) {
        self.difficulty = match self.difficulty {
            Difficulty::Easy => Difficulty::Medium,
            Difficulty::Medium => Difficulty::Hard,
            Difficulty::Hard => Difficulty::Expert,
            Difficulty::Expert => Difficulty::Easy,
        };
    }

    pub fn render(&self, frame: &mut Frame, area: Rect) {
        let buf = frame.buffer_mut();

        // Top bar
        let top = Rect {
            x: area.x,
            y: area.y,
            width: area.width,
            height: 1,
        };
        render_top_bar(buf, top, &["MENU", "SONGS"]);

        // Bottom bar
        let bot = Rect {
            x: area.x,
            y: area.y + area.height - 1,
            width: area.width,
            height: 1,
        };
        let bot_hints: &[(&str, &str)] = if self.import_mode {
            &[("Enter", "import"), ("Esc", "cancel")]
        } else if self.search_mode {
            &[("Enter", "confirm"), ("Esc", "clear")]
        } else if self.rename_mode {
            &[
                ("Tab", "title↔artist"),
                ("Enter", "save"),
                ("Esc", "cancel"),
            ]
        } else if self.mods_overlay {
            &[("H/F/S/P", "toggle"), ("C", "clear all"), ("Esc", "close")]
        } else if self.practice_overlay {
            &[
                ("Tab", "next field"),
                ("←/→", "nudge ±1s / ±0.05"),
                ("digits", "type value"),
                ("C", "clear"),
                ("Enter", "apply"),
                ("Esc", "close"),
            ]
        } else {
            &[
                ("↵", "play"),
                ("Tab", "difficulty"),
                ("p", "practice"),
                ("m", "mods"),
                ("s", "sort"),
                ("/", "search"),
                ("i", "import"),
                ("r", "rename"),
                ("x", "delete"),
                ("Esc", "back"),
            ]
        };
        render_bottom_bar(buf, bot, bot_hints);

        // Header title (inside content area)
        let cx = area.x + area.width / 2;
        let title = "SELECT SONG";
        buf.set_string(
            cx.saturating_sub(title.len() as u16 / 2),
            area.y + 2,
            title,
            Style::default().fg(Color::White).bold(),
        );

        // Difficulty + search status strip
        let diff_name = self.difficulty.to_string().to_uppercase();
        let diff_color = difficulty_color(self.difficulty);
        let diff_prefix = "Difficulty: ";
        let diff_total_w = (diff_prefix.len() + diff_name.len()) as u16;
        let dx = cx.saturating_sub(diff_total_w / 2);
        buf.set_string(
            dx,
            area.y + 3,
            diff_prefix,
            Style::default().fg(Color::Rgb(120, 120, 120)),
        );
        buf.set_string(
            dx + diff_prefix.len() as u16,
            area.y + 3,
            &diff_name,
            Style::default().fg(diff_color).bold(),
        );

        if self.search_mode || !self.search_query.is_empty() {
            let cursor = if self.search_mode { "_" } else { "" };
            let search_txt = format!("/ {}{}", self.search_query, cursor);
            let sw = search_txt.chars().count() as u16;
            buf.set_string(
                cx.saturating_sub(sw / 2),
                area.y + 4,
                &search_txt,
                Style::default().fg(Color::Rgb(200, 200, 100)),
            );
        }

        // Sort indicator
        let sort_txt = format!("Sort: {}", self.sort_mode.label());
        let sw = sort_txt.chars().count() as u16;
        buf.set_string(
            area.x + area.width.saturating_sub(sw + 2),
            area.y + 3,
            &sort_txt,
            Style::default().fg(Color::Rgb(110, 110, 110)),
        );

        // Active mods badge
        if !self.mods.is_empty() {
            let badge = format!("Mods: {}", self.mods.badge());
            let bw = badge.chars().count() as u16;
            buf.set_string(
                area.x + area.width.saturating_sub(bw + 2),
                area.y + 4,
                &badge,
                Style::default().fg(Color::Rgb(255, 200, 100)).bold(),
            );
        }

        // Practice badge: shown whenever practice is active.
        if let Some(duration) = self.selected_song_duration_ms()
            && let Some(badge_core) = self.practice.badge(duration)
        {
            let suffix = if !self.mods.is_empty() {
                " (mods off)"
            } else {
                ""
            };
            let badge = format!("Practice: {}{}", badge_core, suffix);
            let bw = badge.chars().count() as u16;
            buf.set_string(
                area.x + area.width.saturating_sub(bw + 2),
                area.y + 5,
                &badge,
                Style::default().fg(Color::Rgb(255, 180, 80)).bold(),
            );
        }

        if self.import_mode {
            let prompt = format!("Path to audio file: {}_", self.import_input);
            let x = area.x + 4;
            let y = area.y + area.height / 2;
            buf.set_string(x, y, &prompt, Style::default().fg(Color::White));
            buf.set_string(
                x,
                y + 2,
                "Supports: mp3, wav, flac, ogg, m4a, opus, webm",
                Style::default().fg(Color::Rgb(80, 80, 80)),
            );
        } else if self.filtered_indices.is_empty() {
            let msg = if self.search_query.is_empty() {
                "No songs found. Press 'i' to import an audio file."
            } else {
                "No songs match the query."
            };
            let x = area.x + (area.width.saturating_sub(msg.len() as u16)) / 2;
            buf.set_string(
                x,
                area.y + area.height / 2,
                msg,
                Style::default().fg(Color::Rgb(100, 100, 100)),
            );
        } else {
            self.render_song_list(buf, area);
        }

        if let Some(status) = &self.import_status {
            let y = area.y + area.height - 3;
            buf.set_string(
                area.x + 2,
                y,
                status,
                Style::default().fg(Color::Rgb(120, 120, 120)),
            );
        }

        if self.rename_mode {
            self.render_rename_overlay(buf, area);
        }
        if self.mods_overlay {
            self.render_mods_overlay(buf, area);
        }
        if self.practice_overlay {
            self.render_practice_overlay(buf, area);
        }
    }

    fn render_practice_overlay(&self, buf: &mut Buffer, area: Rect) {
        let cx = area.x + area.width / 2;
        let cy = area.y + area.height / 2;
        let w: u16 = 64u16.min(area.width.saturating_sub(4)).max(48);
        let h: u16 = 12;
        let x = cx.saturating_sub(w / 2);
        let y = cy.saturating_sub(h / 2);

        let bg = Color::Rgb(20, 20, 28);
        for dy in 0..h {
            for dx in 0..w {
                buf.set_string(x + dx, y + dy, " ", Style::default().bg(bg));
            }
        }
        for dx in 0..w {
            buf.set_string(
                x + dx,
                y,
                "─",
                Style::default().fg(Color::Rgb(80, 80, 100)).bg(bg),
            );
            buf.set_string(
                x + dx,
                y + h - 1,
                "─",
                Style::default().fg(Color::Rgb(80, 80, 100)).bg(bg),
            );
        }

        let header = "PRACTICE MODE";
        buf.set_string(
            cx - header.len() as u16 / 2,
            y + 1,
            header,
            Style::default().fg(Color::White).bg(bg).bold(),
        );

        let subtitle = "loop a section • slow it down • no score saved";
        let sw = subtitle.chars().count() as u16;
        buf.set_string(
            cx.saturating_sub(sw / 2),
            y + 2,
            subtitle,
            Style::default().fg(Color::Rgb(130, 130, 140)).bg(bg),
        );

        let start_ms = self.practice.section_start_ms.unwrap_or(0);
        let end_ms = self
            .practice
            .section_end_ms
            .or(self.selected_song_duration_ms())
            .unwrap_or(0);
        let field_row =
            |buf: &mut Buffer, row: u16, label: &str, value: String, focused: bool, hint: &str| {
                let label_color = Color::Rgb(160, 160, 180);
                let (value_color, bracket_color) = if focused {
                    (Color::White, Color::Rgb(255, 200, 120))
                } else {
                    (Color::Rgb(180, 180, 180), Color::Rgb(90, 90, 100))
                };
                buf.set_string(
                    x + 4,
                    row,
                    label,
                    Style::default().fg(label_color).bg(bg).bold(),
                );
                buf.set_string(x + 14, row, "[", Style::default().fg(bracket_color).bg(bg));
                buf.set_string(
                    x + 15,
                    row,
                    &value,
                    Style::default().fg(value_color).bg(bg).bold(),
                );
                let close_x = x + 15 + value.chars().count() as u16;
                buf.set_string(close_x, row, "]", Style::default().fg(bracket_color).bg(bg));
                if focused {
                    buf.set_string(
                        x + 2,
                        row,
                        "▸",
                        Style::default().fg(Color::Rgb(255, 200, 120)).bg(bg).bold(),
                    );
                }
                let hint_x = close_x + 2;
                if hint_x + hint.chars().count() as u16 + 2 < x + w {
                    buf.set_string(
                        hint_x,
                        row,
                        hint,
                        Style::default().fg(Color::Rgb(110, 110, 120)).bg(bg),
                    );
                }
            };

        let start_value = if matches!(self.practice_focus, PracticeField::Start) {
            format_typed_mmss(&self.practice_buf, true)
        } else {
            practice::format_mmss(start_ms)
        };
        let end_value = if matches!(self.practice_focus, PracticeField::End) {
            format_typed_mmss(&self.practice_buf, true)
        } else {
            practice::format_mmss(end_ms)
        };
        let speed_value = if matches!(self.practice_focus, PracticeField::Speed) {
            format!("{}_", self.practice_buf)
        } else {
            format!("{:.2}×", self.practice.speed)
        };

        field_row(
            buf,
            y + 4,
            "Section",
            start_value,
            matches!(self.practice_focus, PracticeField::Start),
            "←/→ ±1s",
        );
        field_row(
            buf,
            y + 5,
            "to",
            end_value,
            matches!(self.practice_focus, PracticeField::End),
            "←/→ ±1s",
        );
        field_row(
            buf,
            y + 7,
            "Speed",
            speed_value,
            matches!(self.practice_focus, PracticeField::Speed),
            "←/→ ±0.05",
        );

        let status = if let Some(err) = &self.practice_error {
            (err.as_str(), Color::Rgb(255, 120, 120))
        } else if self.practice.is_active() {
            ("✓ practice active", Color::Rgb(120, 220, 140))
        } else {
            (
                "practice inactive (full song, no speed change)",
                Color::Rgb(110, 110, 120),
            )
        };
        let sw = status.0.chars().count() as u16;
        buf.set_string(
            cx.saturating_sub(sw / 2),
            y + h - 2,
            status.0,
            Style::default().fg(status.1).bg(bg),
        );
    }

    fn render_mods_overlay(&self, buf: &mut Buffer, area: Rect) {
        use crate::game::modifiers::Modifier;
        let cx = area.x + area.width / 2;
        let cy = area.y + area.height / 2;
        // Width adapts to terminal: target 92, shrink if needed.
        let w: u16 = 92u16.min(area.width.saturating_sub(4)).max(50);
        let h: u16 = 12;
        let x = cx.saturating_sub(w / 2);
        let y = cy.saturating_sub(h / 2);

        let bg = Color::Rgb(20, 20, 28);
        for dy in 0..h {
            for dx in 0..w {
                buf.set_string(x + dx, y + dy, " ", Style::default().bg(bg));
            }
        }
        for dx in 0..w {
            buf.set_string(
                x + dx,
                y,
                "─",
                Style::default().fg(Color::Rgb(80, 80, 100)).bg(bg),
            );
            buf.set_string(
                x + dx,
                y + h - 1,
                "─",
                Style::default().fg(Color::Rgb(80, 80, 100)).bg(bg),
            );
        }

        let header = "MODIFIERS";
        buf.set_string(
            cx - header.len() as u16 / 2,
            y + 1,
            header,
            Style::default().fg(Color::White).bg(bg).bold(),
        );

        let rows = [
            ('H', Modifier::Hidden),
            ('F', Modifier::Flashlight),
            ('S', Modifier::SuddenDeath),
            ('P', Modifier::PerfectOnly),
        ];

        // Layout offsets — description column is sized to whatever space remains.
        let col_label = 4u16;
        let col_mark = 6u16;
        let col_name = 11u16;
        let col_desc = 26u16;
        let desc_max = w.saturating_sub(col_desc + 2) as usize; // 2 = right padding

        for (i, (key, m)) in rows.iter().enumerate() {
            let row_y = y + 3 + i as u16;
            let on = self.mods.contains(*m);
            let mark = if on { "[●]" } else { "[ ]" };
            let mark_color = if on {
                Color::Rgb(255, 200, 100)
            } else {
                Color::Rgb(100, 100, 100)
            };
            buf.set_string(
                x + col_label,
                row_y,
                key.to_string(),
                Style::default().fg(Color::Rgb(180, 180, 200)).bg(bg).bold(),
            );
            buf.set_string(
                x + col_mark,
                row_y,
                mark,
                Style::default().fg(mark_color).bg(bg).bold(),
            );
            buf.set_string(
                x + col_name,
                row_y,
                m.label(),
                Style::default().fg(Color::White).bg(bg).bold(),
            );
            let desc = m.description();
            let truncated: String = if desc.chars().count() > desc_max {
                desc.chars()
                    .take(desc_max.saturating_sub(1))
                    .chain(std::iter::once('…'))
                    .collect()
            } else {
                desc.to_string()
            };
            buf.set_string(
                x + col_desc,
                row_y,
                &truncated,
                Style::default().fg(Color::Rgb(140, 140, 150)).bg(bg),
            );
        }
    }

    fn render_rename_overlay(&self, buf: &mut Buffer, area: Rect) {
        let cx = area.x + area.width / 2;
        let cy = area.y + area.height / 2;
        let w: u16 = 60;
        let h: u16 = 7;
        let x = cx.saturating_sub(w / 2);
        let y = cy.saturating_sub(h / 2);

        for dy in 0..h {
            for dx in 0..w {
                buf.set_string(
                    x + dx,
                    y + dy,
                    " ",
                    Style::default().bg(Color::Rgb(20, 20, 28)),
                );
            }
        }
        for dx in 0..w {
            buf.set_string(
                x + dx,
                y,
                "─",
                Style::default()
                    .fg(Color::Rgb(80, 80, 100))
                    .bg(Color::Rgb(20, 20, 28)),
            );
            buf.set_string(
                x + dx,
                y + h - 1,
                "─",
                Style::default()
                    .fg(Color::Rgb(80, 80, 100))
                    .bg(Color::Rgb(20, 20, 28)),
            );
        }

        let title_focused = matches!(self.rename_field, RenameField::Title);
        let song = self.real_index().and_then(|i| self.songs.get(i));
        let (cur_title, cur_artist) = song
            .map(|s| (s.title.clone(), s.artist.clone()))
            .unwrap_or_default();

        let header = "RENAME SONG";
        buf.set_string(
            cx - header.len() as u16 / 2,
            y + 1,
            header,
            Style::default()
                .fg(Color::White)
                .bg(Color::Rgb(20, 20, 28))
                .bold(),
        );

        let title_value = if title_focused {
            format!("{}_", self.rename_buf)
        } else {
            cur_title
        };
        let artist_value = if !title_focused {
            format!("{}_", self.rename_buf)
        } else {
            cur_artist
        };

        let label = |buf: &mut Buffer, focused: bool, label: &str, value: &str, row: u16| {
            let label_color = if focused {
                Color::Rgb(255, 240, 150)
            } else {
                Color::Rgb(120, 120, 120)
            };
            let value_color = if focused {
                Color::White
            } else {
                Color::Rgb(160, 160, 160)
            };
            buf.set_string(
                x + 2,
                row,
                label,
                Style::default()
                    .fg(label_color)
                    .bg(Color::Rgb(20, 20, 28))
                    .bold(),
            );
            buf.set_string(
                x + 12,
                row,
                value,
                Style::default().fg(value_color).bg(Color::Rgb(20, 20, 28)),
            );
        };

        label(buf, title_focused, "Title:", &title_value, y + 3);
        label(buf, !title_focused, "Artist:", &artist_value, y + 4);
    }

    fn render_song_list(&self, buf: &mut Buffer, area: Rect) {
        let list_y = area.y + 6;
        let list_h = area.height.saturating_sub(8) as usize;
        let max_visible = list_h.max(1);
        let start = if self.selected >= max_visible {
            self.selected - max_visible + 1
        } else {
            0
        };
        let diff_idx = Difficulty::all()
            .iter()
            .position(|d| *d == self.difficulty)
            .unwrap_or(2);
        let cx = area.x + area.width / 2;

        // Each row: 2 lines — title+dots+best, then meta strip (bpm/dur/notes).
        let row_h: u16 = 2;
        let mut drawn: u16 = 0;
        for (i, real_idx) in self.filtered_indices.iter().skip(start).enumerate() {
            let y = list_y + drawn;
            if y + row_h > area.y + area.height - 2 {
                break;
            }
            let is_sel = start + i == self.selected;
            let song = &self.songs[*real_idx];

            let (prefix, title_style) = if is_sel {
                ("▸ ", Style::default().fg(Color::White).bold())
            } else {
                ("  ", Style::default().fg(Color::Rgb(130, 130, 130)))
            };

            // Accent bar on the left for selected row.
            if is_sel {
                for dy in 0..row_h {
                    buf.set_string(
                        area.x + 2,
                        y + dy,
                        "│",
                        Style::default()
                            .fg(difficulty_color(self.difficulty))
                            .bold(),
                    );
                }
            }

            // Row 1: title + artist
            let title_text = if song.artist.is_empty() {
                format!("{}{}", prefix, song.title)
            } else {
                format!("{}{} — {}", prefix, song.title, song.artist)
            };
            let row_x = area.x + 5;
            let max_title_w = (area.width.saturating_sub(26 + 12)) as usize;
            let truncated: String = title_text.chars().take(max_title_w).collect();
            buf.set_string(row_x, y, &truncated, title_style);

            // Best score on the far right of row 1.
            let best_txt = self
                .scores
                .get(&song.slug, &self.difficulty.to_string())
                .map(|b| format!("★ {} ({})", b.score, b.grade))
                .unwrap_or_else(|| String::from("—"));
            let bw = best_txt.chars().count() as u16;
            let bx = area.x + area.width.saturating_sub(bw + 3);
            let best_style = if best_txt == "—" {
                Style::default().fg(Color::Rgb(60, 60, 60))
            } else {
                Style::default().fg(Color::Rgb(255, 215, 0))
            };
            buf.set_string(bx, y, &best_txt, best_style);

            // Row 2: meta
            let y2 = y + 1;
            let bpm_txt = format!("{} BPM", song.bpm);
            let dur_txt = format!(
                "{}:{:02}",
                song.duration_ms / 60_000,
                (song.duration_ms / 1000) % 60
            );
            let notes_txt = format!("{} notes", song.note_counts[diff_idx]);
            let meta = format!("  {}  •  {}  •  {}", bpm_txt, dur_txt, notes_txt);
            buf.set_string(
                row_x,
                y2,
                &meta,
                Style::default().fg(Color::Rgb(110, 110, 110)),
            );

            // Difficulty dots (far right of row 2).
            let _ = cx;
            let dots_x = area.x + area.width.saturating_sub(12);
            render_difficulty_dots(buf, dots_x, y2, song.present, Some(self.difficulty));

            drawn += row_h;
        }
    }
}

/// `"01:30"` → `"0130"`. Drops the colon so typing is a single buffer.
fn digits_for_mmss(ms: u64) -> String {
    let total = ms / 1000;
    let mm = total / 60;
    let ss = total % 60;
    format!("{:02}{:02}", mm.min(99), ss)
}

/// Render the digit buffer in `MM:SS` form with a trailing cursor if active.
fn format_typed_mmss(digits: &str, with_cursor: bool) -> String {
    let padded = format!("{:0>4}", digits);
    let (mm, ss) = padded.split_at(2);
    let cursor = if with_cursor { "_" } else { "" };
    format!("{}:{}{}", mm, ss, cursor)
}

/// Parse a typed digit buffer (up to 4 chars, auto-padded) back to ms.
/// `""` → 0, `"1"` → 00:01, `"30"` → 00:30, `"130"` → 01:30, `"0130"` → 01:30.
fn mmss_from_digits(buf: &str) -> Option<u64> {
    if buf.is_empty() {
        return Some(0);
    }
    let padded = format!("{:0>4}", buf);
    let (mm, ss) = padded.split_at(2);
    let m: u64 = mm.parse().ok()?;
    let s: u64 = ss.parse().ok()?;
    if s > 59 {
        return None;
    }
    Some((m * 60 + s) * 1000)
}

/// Find audio file in a song directory. Supports mp3, m4a, webm, opus, ogg, wav, flac.
pub fn find_audio_file(dir: &Path) -> Option<PathBuf> {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return None;
    };
    for entry in entries.flatten() {
        let name = entry.file_name();
        let name_str = name.to_string_lossy();
        if name_str.starts_with("audio.") {
            let ext = name_str.rsplit('.').next().unwrap_or("");
            match ext {
                "mp3" | "m4a" | "webm" | "opus" | "ogg" | "wav" | "flac" => {
                    return Some(entry.path());
                }
                _ => {}
            }
        }
    }
    None
}
