use crate::{
    config::Config,
    eq_discovery::{CharacterLog, discover_character_logs},
    speakers::SpeakerStore,
    workers::{KokoroStatus, WorkerEvent},
};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
    prelude::*,
    widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
};
use std::collections::VecDeque;

const LOGO: &str = r#"
███╗   ███╗██╗   ██╗██████╗ ███╗   ███╗ ██████╗ ██╗   ██╗████████╗██╗  ██╗
████╗ ████║██║   ██║██╔══██╗████╗ ████║██╔═══██╗██║   ██║╚══██╔══╝██║  ██║
██╔████╔██║██║   ██║██║  ██║██╔████╔██║██║   ██║██║   ██║   ██║   ███████║
██║╚██╔╝██║██║   ██║██║  ██║██║╚██╔╝██║██║   ██║██║   ██║   ██║   ██╔══██║
██║ ╚═╝ ██║╚██████╔╝██████╔╝██║ ╚═╝ ██║╚██████╔╝╚██████╔╝   ██║   ██║  ██║
╚═╝     ╚═╝ ╚═════╝ ╚═════╝ ╚═╝     ╚═╝ ╚═════╝  ╚═════╝    ╚═╝   ╚═╝  ╚═╝
"#;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Screen {
    Dashboard,
    Setup,
    Settings,
    Speakers,
    Help,
}

#[derive(Debug)]
pub enum UiAction {
    Quit,
    ConfigChanged,
}

pub struct App {
    pub config: Config,
    pub speakers: SpeakerStore,
    pub screen: Screen,
    pub kokoro_status: KokoroStatus,
    pub voices: Vec<String>,
    pub current_zone: Option<String>,
    pub queue_depth: usize,
    pub activity: VecDeque<String>,
    pub selected_speaker: usize,
    pub selected_voice: usize,
    pub selected_character_log: usize,
    pub settings_cursor: usize,
    pub speaker_filter: String,
    pub character_logs: Vec<CharacterLog>,
    pub splash_until: std::time::Instant,
    pub input: Option<Input>,
}

#[derive(Debug)]
pub enum Input {
    Endpoint(String),
    GameDirectory(String),
    LogPath(String),
    SpeakerSearch(String),
}

impl App {
    pub fn new(config: Config, speakers: SpeakerStore) -> Self {
        let character_logs = discover_character_logs(config.game_directory.as_deref());
        let log_is_configured = config.log_path.as_ref().is_some_and(|path| path.is_file());
        let setup_input = (!log_is_configured && character_logs.is_empty()).then(|| {
            Input::GameDirectory(
                config
                    .game_directory
                    .as_ref()
                    .map_or_else(String::new, |path| path.display().to_string()),
            )
        });
        let mut activity = VecDeque::from(["The tavern is quiet. Waiting for log lines…".into()]);
        if !log_is_configured {
            activity.push_front(if character_logs.is_empty() {
                "No EverQuest logs found. Enter your EverQuest game folder.".into()
            } else {
                format!("Found {} EverQuest character log(s).", character_logs.len())
            });
        }
        Self {
            config,
            speakers,
            screen: if log_is_configured {
                Screen::Dashboard
            } else {
                Screen::Setup
            },
            kokoro_status: KokoroStatus::Checking,
            voices: Vec::new(),
            current_zone: None,
            queue_depth: 0,
            activity,
            selected_speaker: 0,
            selected_voice: 0,
            selected_character_log: 0,
            settings_cursor: 0,
            speaker_filter: String::new(),
            character_logs,
            splash_until: std::time::Instant::now() + std::time::Duration::from_millis(1700),
            input: setup_input,
        }
    }

    pub fn receive(&mut self, event: WorkerEvent) {
        match event {
            WorkerEvent::KokoroStatus(status) => self.kokoro_status = status,
            WorkerEvent::Voices(voices) => {
                self.voices = voices;
                self.activity
                    .push_front(format!("{} Kokoro voices loaded", self.voices.len()));
            }
            WorkerEvent::QueueDepth(depth) => self.queue_depth = depth,
            WorkerEvent::Activity(message) => self.activity.push_front(message),
            WorkerEvent::Log(event) => self.activity.push_front(format!(
                "[{} {}] {}",
                event.occurred_at().format("%H:%M:%S"),
                event.label(),
                event.text()
            )),
        }
        self.activity.truncate(80);
    }

    pub fn handle_key(&mut self, key: KeyEvent) -> Option<UiAction> {
        if self.input.is_some() {
            return self.handle_input(key);
        }
        match key.code {
            KeyCode::Char('q') => Some(UiAction::Quit),
            KeyCode::Char('?') => {
                self.screen = Screen::Help;
                None
            }
            KeyCode::Esc => {
                self.screen = Screen::Dashboard;
                None
            }
            KeyCode::Char('d') => {
                self.screen = Screen::Dashboard;
                None
            }
            KeyCode::Char('s') => {
                self.screen = Screen::Settings;
                None
            }
            KeyCode::Char('v') => {
                self.screen = Screen::Speakers;
                None
            }
            _ => match self.screen {
                Screen::Setup => self.handle_setup_key(key),
                Screen::Settings => self.handle_settings_key(key),
                Screen::Speakers => self.handle_speakers_key(key),
                _ => None,
            },
        }
    }

    fn handle_input(&mut self, key: KeyEvent) -> Option<UiAction> {
        let mut input = self.input.take().expect("input state was checked");
        match key.code {
            KeyCode::Esc => {}
            KeyCode::Enter => {
                let mut close = false;
                let changed = match &input {
                    Input::Endpoint(value) => {
                        self.config.kokoro_url = value.trim().trim_end_matches('/').to_string();
                        true
                    }
                    Input::GameDirectory(value) => {
                        self.config.game_directory = (!value.trim().is_empty())
                            .then(|| std::path::PathBuf::from(value.trim()));
                        self.refresh_character_logs();
                        true
                    }
                    Input::LogPath(value) => {
                        self.config.log_path = (!value.trim().is_empty())
                            .then(|| std::path::PathBuf::from(value.trim()));
                        true
                    }
                    Input::SpeakerSearch(value) => {
                        self.speaker_filter = value.trim().to_lowercase();
                        self.selected_speaker = 0;
                        close = true;
                        false
                    }
                };
                if changed {
                    return Some(UiAction::ConfigChanged);
                }
                if close {
                    return None;
                }
            }
            KeyCode::Backspace => match &mut input {
                Input::Endpoint(value)
                | Input::GameDirectory(value)
                | Input::LogPath(value)
                | Input::SpeakerSearch(value) => {
                    value.pop();
                }
            },
            KeyCode::Char(character) => match &mut input {
                Input::Endpoint(value)
                | Input::GameDirectory(value)
                | Input::LogPath(value)
                | Input::SpeakerSearch(value) => {
                    value.push(character);
                }
            },
            _ => {}
        }
        self.input = Some(input);
        None
    }

    fn handle_settings_key(&mut self, key: KeyEvent) -> Option<UiAction> {
        match key.code {
            KeyCode::Up | KeyCode::Char('k') => {
                self.settings_cursor = self.settings_cursor.saturating_sub(1)
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.settings_cursor = (self.settings_cursor + 1).min(4)
            }
            KeyCode::Enter | KeyCode::Char(' ') => match self.settings_cursor {
                0 => self.input = Some(Input::Endpoint(self.config.kokoro_url.clone())),
                1 => {
                    self.input = Some(Input::LogPath(
                        self.config
                            .log_path
                            .as_ref()
                            .map_or_else(String::new, |path| path.display().to_string()),
                    ))
                }
                2 => {
                    self.config.events.npc_dialogue = !self.config.events.npc_dialogue;
                    return Some(UiAction::ConfigChanged);
                }
                3 => {
                    self.config.events.zone_transitions = !self.config.events.zone_transitions;
                    return Some(UiAction::ConfigChanged);
                }
                4 => {
                    self.config.volume = if self.config.volume >= 1.0 {
                        0.2
                    } else {
                        (self.config.volume + 0.2).min(1.0)
                    };
                    return Some(UiAction::ConfigChanged);
                }
                _ => {}
            },
            _ => {}
        }
        None
    }

    fn handle_setup_key(&mut self, key: KeyEvent) -> Option<UiAction> {
        match key.code {
            KeyCode::Up | KeyCode::Char('k') => {
                self.selected_character_log = self.selected_character_log.saturating_sub(1)
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.selected_character_log = (self.selected_character_log + 1)
                    .min(self.character_logs.len().saturating_sub(1))
            }
            KeyCode::Enter => {
                if let Some(log) = self.character_logs.get(self.selected_character_log) {
                    self.config.log_path = Some(log.path.clone());
                    self.config.game_directory = Some(log.game_directory.clone());
                    self.screen = Screen::Dashboard;
                    self.activity
                        .push_front(format!("Following {}", log.display_name()));
                    return Some(UiAction::ConfigChanged);
                }
            }
            KeyCode::Char('p') => {
                self.input = Some(Input::GameDirectory(
                    self.config
                        .game_directory
                        .as_ref()
                        .map_or_else(String::new, |path| path.display().to_string()),
                ));
            }
            KeyCode::Char('r') => self.refresh_character_logs(),
            _ => {}
        }
        None
    }

    fn refresh_character_logs(&mut self) {
        self.character_logs = discover_character_logs(self.config.game_directory.as_deref());
        self.selected_character_log = 0;
        self.activity.push_front(if self.character_logs.is_empty() {
            "No character logs found. In EverQuest, enable logging with /log on, then press [r]."
                .into()
        } else {
            format!(
                "Found {} EverQuest character log(s).",
                self.character_logs.len()
            )
        });
    }

    fn handle_speakers_key(&mut self, key: KeyEvent) -> Option<UiAction> {
        let keys = self.speaker_keys();
        match key.code {
            KeyCode::Char('f') => {
                self.input = Some(Input::SpeakerSearch(self.speaker_filter.clone()));
            }
            KeyCode::Up | KeyCode::Char('k') => {
                self.selected_speaker = self.selected_speaker.saturating_sub(1)
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.selected_speaker =
                    (self.selected_speaker + 1).min(keys.len().saturating_sub(1))
            }
            KeyCode::Left | KeyCode::Char('h') => {
                self.selected_voice = self.selected_voice.saturating_sub(1)
            }
            KeyCode::Right | KeyCode::Char('l') => {
                self.selected_voice =
                    (self.selected_voice + 1).min(self.voices.len().saturating_sub(1))
            }
            KeyCode::Char('p') | KeyCode::Enter => {
                if let (Some(key), Some(voice)) = (
                    keys.get(self.selected_speaker),
                    self.voices.get(self.selected_voice),
                ) {
                    self.speakers.pin(key, voice.clone());
                    return Some(UiAction::ConfigChanged);
                }
            }
            KeyCode::Char('r') => {
                if let Some(key) = keys.get(self.selected_speaker) {
                    self.speakers.set_random_each_utterance(key);
                    return Some(UiAction::ConfigChanged);
                }
            }
            KeyCode::Char('x') => {
                if let Some(key) = keys.get(self.selected_speaker) {
                    self.speakers.reset_persistent_random(key, &self.voices);
                    return Some(UiAction::ConfigChanged);
                }
            }
            _ => {}
        }
        None
    }

    pub fn speaker_keys(&self) -> Vec<String> {
        self.speakers
            .entries()
            .filter(|(_, speaker)| {
                self.speaker_filter.is_empty()
                    || speaker.name.to_lowercase().contains(&self.speaker_filter)
            })
            .map(|(key, _)| key.clone())
            .collect()
    }
}

pub fn draw(frame: &mut Frame, app: &App) {
    if std::time::Instant::now() < app.splash_until {
        draw_splash(frame);
        return;
    }
    let area = frame.area();
    let sections = Layout::vertical([
        Constraint::Length(3),
        Constraint::Min(1),
        Constraint::Length(2),
    ])
    .split(area);
    draw_header(frame, app, sections[0]);
    match app.screen {
        Screen::Dashboard => draw_dashboard(frame, app, sections[1]),
        Screen::Setup => draw_setup(frame, app, sections[1]),
        Screen::Settings => draw_settings(frame, app, sections[1]),
        Screen::Speakers => draw_speakers(frame, app, sections[1]),
        Screen::Help => draw_help(frame, sections[1]),
    }
    frame.render_widget(
        Paragraph::new(" [d] dashboard  [s] settings  [v] voices  [?] help  [q] quit ")
            .style(Style::default().fg(Color::DarkGray)),
        sections[2],
    );
    if let Some(input) = &app.input {
        draw_input(frame, input);
    }
}

fn draw_setup(frame: &mut Frame, app: &App, area: Rect) {
    let logs = app
        .character_logs
        .iter()
        .enumerate()
        .map(|(index, log)| {
            ListItem::new(format!("{}  {}", log.display_name(), log.path.display())).style(
                if index == app.selected_character_log {
                    Style::default().fg(Color::Yellow).bold()
                } else {
                    Style::default()
                },
            )
        })
        .collect::<Vec<_>>();
    let title = if logs.is_empty() {
        " FIRST-TIME SETUP — [p] folder [r] rescan "
    } else {
        " FIRST-TIME SETUP — ↑↓ select character, Enter continue; [p] folder [r] rescan "
    };
    let empty = "No character logs found yet. Enter your EverQuest game folder.\n\n\
                 After logging into a character, use /log on and press [r] here.";
    frame.render_widget(
        List::new(logs).block(Block::default().title(title).borders(Borders::ALL)),
        area,
    );
    if app.character_logs.is_empty() {
        frame.render_widget(
            Paragraph::new(empty)
                .style(Style::default().fg(Color::Gray))
                .wrap(Wrap { trim: true }),
            area.inner(Margin {
                vertical: 1,
                horizontal: 2,
            }),
        );
    }
}

fn draw_splash(frame: &mut Frame) {
    let layout = Layout::vertical([
        Constraint::Percentage(25),
        Constraint::Length(7),
        Constraint::Length(2),
        Constraint::Percentage(25),
    ])
    .split(frame.area());
    frame.render_widget(
        Paragraph::new(LOGO)
            .alignment(Alignment::Center)
            .style(Style::default().fg(Color::LightMagenta)),
        layout[1],
    );
    frame.render_widget(
        Paragraph::new("THE LOG AWAKENS • THE DUNGEON SPEAKS")
            .alignment(Alignment::Center)
            .style(Style::default().fg(Color::Yellow)),
        layout[2],
    );
}

fn draw_header(frame: &mut Frame, app: &App, area: Rect) {
    let status = match app.kokoro_status {
        KokoroStatus::Checking => "◌ KOKORO: CHECKING",
        KokoroStatus::Connected => "● KOKORO: CONNECTED",
        KokoroStatus::Unreachable => "× KOKORO: UNREACHABLE",
    };
    let color = match app.kokoro_status {
        KokoroStatus::Checking => Color::Yellow,
        KokoroStatus::Connected => Color::Green,
        KokoroStatus::Unreachable => Color::Red,
    };
    let title = Line::from(vec![
        Span::styled(
            " MUDMOUTH ",
            Style::default().fg(Color::LightMagenta).bold(),
        ),
        Span::raw(" // "),
        Span::styled(status, Style::default().fg(color).bold()),
        Span::raw(format!("    Queue: {}", app.queue_depth)),
    ]);
    frame.render_widget(
        Paragraph::new(title).block(Block::default().borders(Borders::ALL)),
        area,
    );
}

fn draw_dashboard(frame: &mut Frame, app: &App, area: Rect) {
    let columns =
        Layout::horizontal([Constraint::Percentage(68), Constraint::Percentage(32)]).split(area);
    let items = app
        .activity
        .iter()
        .map(|line| ListItem::new(line.as_str()))
        .collect::<Vec<_>>();
    frame.render_widget(
        List::new(items)
            .block(
                Block::default()
                    .title(" LIVE TAVERN SCROLL ")
                    .borders(Borders::ALL),
            )
            .style(Style::default().fg(Color::Gray)),
        columns[0],
    );
    let log = app.config.log_path.as_ref().map_or_else(
        || "not configured".into(),
        |path| path.display().to_string(),
    );
    let details = format!(
        "LOG\n{log}\n\nZONE\n{}\n\nVOICE CATALOG\n{} voices\n\nDEFAULTS\nNPC dialogue: {}\nZone transitions: {}",
        app.current_zone.as_deref().unwrap_or("unknown"),
        app.voices.len(),
        on_off(app.config.events.npc_dialogue),
        on_off(app.config.events.zone_transitions)
    );
    frame.render_widget(
        Paragraph::new(details)
            .block(
                Block::default()
                    .title(" WORLD STATE ")
                    .borders(Borders::ALL),
            )
            .wrap(Wrap { trim: true }),
        columns[1],
    );
}

fn draw_settings(frame: &mut Frame, app: &App, area: Rect) {
    let fields = [
        format!("Kokoro endpoint: {}", app.config.kokoro_url),
        format!(
            "EverQuest log: {}",
            app.config.log_path.as_ref().map_or_else(
                || "not configured".into(),
                |path| path.display().to_string()
            )
        ),
        format!(
            "Speak NPC dialogue: {}",
            on_off(app.config.events.npc_dialogue)
        ),
        format!(
            "Speak zone transitions: {}",
            on_off(app.config.events.zone_transitions)
        ),
        format!("Playback volume: {:.0}%", app.config.volume * 100.0),
    ];
    let items = fields
        .iter()
        .enumerate()
        .map(|(index, field)| {
            ListItem::new(field.clone()).style(if index == app.settings_cursor {
                Style::default().fg(Color::Yellow).bold()
            } else {
                Style::default()
            })
        })
        .collect::<Vec<_>>();
    frame.render_widget(
        List::new(items).block(
            Block::default()
                .title(" SETTINGS — ↑↓ select, Enter edit/toggle ")
                .borders(Borders::ALL),
        ),
        area,
    );
}

fn draw_speakers(frame: &mut Frame, app: &App, area: Rect) {
    let columns =
        Layout::horizontal([Constraint::Percentage(52), Constraint::Percentage(48)]).split(area);
    let keys = app.speaker_keys();
    let speakers = keys
        .iter()
        .enumerate()
        .filter_map(|(index, key)| {
            app.speakers.get(key).map(|speaker| {
                ListItem::new(format!("{}  {}", speaker.name, speaker.mode.display())).style(
                    if index == app.selected_speaker {
                        Style::default().fg(Color::Yellow).bold()
                    } else {
                        Style::default()
                    },
                )
            })
        })
        .collect::<Vec<_>>();
    frame.render_widget(
        List::new(speakers).block(
            Block::default()
                .title(format!(
                    " NPCS — ↑↓ choose; [f] filter{} ",
                    if app.speaker_filter.is_empty() {
                        String::new()
                    } else {
                        format!(": {}", app.speaker_filter)
                    }
                ))
                .borders(Borders::ALL),
        ),
        columns[0],
    );
    let voices = app
        .voices
        .iter()
        .enumerate()
        .map(|(index, voice)| {
            ListItem::new(voice.as_str()).style(if index == app.selected_voice {
                Style::default().fg(Color::LightCyan).bold()
            } else {
                Style::default()
            })
        })
        .collect::<Vec<_>>();
    frame.render_widget(
        List::new(voices).block(
            Block::default()
                .title(" VOICES — ←→ choose; [p] pin [r] random/line [x] reset ")
                .borders(Borders::ALL),
        ),
        columns[1],
    );
}

fn draw_help(frame: &mut Frame, area: Rect) {
    frame.render_widget(
        Paragraph::new(
            "Mudmouth gives EverQuest’s NPCs a voice.\n\n\
             [d] dashboard  [s] settings  [v] NPC voice manager\n\
             [f] filter NPCs in the voice manager\n\
             [p] pin the selected NPC to the selected Kokoro voice\n\
             [r] change selected NPC to random every utterance\n\
             [x] assign a fresh persistent random voice\n\
             [q] quit\n\n\
             NPC dialogue is on by default. Zone announcements are off by default.\n\
             The KOKORO status line tells you whether voice generation is reachable.",
        )
        .block(
            Block::default()
                .title(" GRIMOIRE / HELP ")
                .borders(Borders::ALL),
        )
        .wrap(Wrap { trim: true }),
        area,
    );
}

fn draw_input(frame: &mut Frame, input: &Input) {
    let area = centered_rect(80, 20, frame.area());
    let (label, value) = match input {
        Input::Endpoint(value) => ("Kokoro endpoint", value),
        Input::GameDirectory(value) => ("EverQuest game folder", value),
        Input::LogPath(value) => ("EverQuest log path", value),
        Input::SpeakerSearch(value) => ("Search NPC", value),
    };
    frame.render_widget(Clear, area);
    frame.render_widget(
        Paragraph::new(format!("{value}_")).block(
            Block::default()
                .title(format!(" {label} — Enter save, Esc cancel "))
                .borders(Borders::ALL),
        ),
        area,
    );
}

fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
    let vertical = Layout::vertical([
        Constraint::Percentage((100 - height) / 2),
        Constraint::Percentage(height),
        Constraint::Percentage((100 - height) / 2),
    ])
    .split(area);
    Layout::horizontal([
        Constraint::Percentage((100 - width) / 2),
        Constraint::Percentage(width),
        Constraint::Percentage((100 - width) / 2),
    ])
    .split(vertical[1])[1]
}

fn on_off(value: bool) -> &'static str {
    if value { "ON" } else { "OFF" }
}
