use anyhow::anyhow;
use ratatui::{
    Frame,
    crossterm::event,
    layout::{Constraint, Layout, Rect},
    style::{Color, Modifier, Style, Stylize},
    text::Text,
    widgets::{
        Block, BorderType, Borders, Cell, HighlightSpacing, Paragraph, Row, Table, TableState,
    },
};
use ratatui_textarea::{Input, Key, TextArea};

use pickems::datatypes::{Map, Name, Rating, Seed, Team};

/// Footer content to render when in browse mode.
const BROWSE_FOOTER: [&str; 2] = [
    "(Esc) quit | (Ctrl + S) save | (Ctrl + N) add team | (Del) remove team",
    "(↑) up | (↓) down | (←) left | (→) right | (Enter) edit field",
];

/// Footer content to render when in edit mode.
const EDITOR_FOOTER: &str = "(Esc) cancel edit | (Enter) commit edit";

/// Interactive data-entry wizard backed by `ratatui`.
pub struct Wizard<'a> {
    save: bool,
    cancel: bool,
    teams: Vec<(Name, Team)>,
    problems: Vec<String>,
    state: TableState,
    editor: Option<TextArea<'a>>,
}

impl Wizard<'_> {
    /// Run an instance of the wizard to completion, handling ratatui and returning a vector of teams.
    pub fn run() -> anyhow::Result<Option<Map>> {
        let mut wizard = Self {
            save: false,
            cancel: false,
            teams: Vec::with_capacity(16),
            problems: Vec::with_capacity(4),
            state: TableState::default().with_selected(0),
            editor: None,
        };

        for _ in 0..16 {
            wizard.add_new_team();
        }

        let mut terminal = ratatui::init();

        while !(wizard.cancel || wizard.save) {
            if let Err(e) = {
                terminal.draw(|frame| wizard.render(frame))?;
                wizard.handle_crossterm_events()?;
                Ok(())
            } {
                ratatui::restore();
                return Err(e);
            }
        }

        ratatui::restore();

        if wizard.save {
            Ok(Some(Map::from(wizard.teams)))
        } else {
            Ok(None)
        }
    }

    /// Handle crossterm events and dispatch key inputs.
    fn handle_crossterm_events(&mut self) -> anyhow::Result<()> {
        if let Some(editor) = self.editor.as_mut() {
            match event::read()?.into() {
                Input {
                    key: Key::Char('c' | 'C'),
                    ctrl: true,
                    ..
                } => self.cancel = true,
                Input { key: Key::Esc, .. } => self.editor = None,
                Input {
                    key: Key::Enter, ..
                } => self.commit_edit(),
                input => {
                    if editor.input(input) {
                        self.validate_editor();
                    }
                }
            }
        } else {
            match event::read()?.into() {
                Input { key: Key::Esc, .. }
                | Input {
                    key: Key::Char('c' | 'C'),
                    ctrl: true,
                    ..
                } => self.cancel = true,
                Input {
                    key: Key::Char('s' | 'S'),
                    ctrl: true,
                    ..
                } if self.problems.is_empty() => {
                    self.save = true;
                }
                Input {
                    key: Key::Char('n' | 'N'),
                    ctrl: true,
                    ..
                } => self.add_new_team(),
                Input {
                    key: Key::Delete, ..
                } => self.remove_selected_team(),
                Input {
                    key: Key::Enter, ..
                } => self.start_edit(),
                Input {
                    key: Key::Down | Key::Char('j'),
                    ..
                } => self.next_row(),
                Input {
                    key: Key::Up | Key::Char('k'),
                    ..
                } => self.previous_row(),
                Input {
                    key: Key::Right | Key::Char('l'),
                    ..
                } => self.next_column(),
                Input {
                    key: Key::Left | Key::Char('h'),
                    ..
                } => self.previous_column(),
                _ => {}
            }
        }

        Ok(())
    }

    /// Attempt to parse current editor contents into a valid team.
    fn parse_edit(&self) -> anyhow::Result<(Name, Team)> {
        if let Some((row, col)) = self.state.selected_cell() {
            let (mut name, mut data) = self.teams[row].clone();

            if let Some(editor) = &self.editor {
                match col {
                    0 => {
                        let seed = Seed::try_new(editor.lines()[0].parse::<u16>()?)?;
                        data.seed = seed;
                        Ok((name, data))
                    }
                    1 => {
                        let new_name = Name::try_new(&editor.lines()[0])?;

                        if self.teams.iter().any(|(name, _)| name == &new_name) {
                            Err(anyhow!("name already exists, must be unique"))
                        } else {
                            name.clone_from(&new_name);
                            Ok((name, data))
                        }
                    }
                    2 => {
                        data.rating = Rating::try_new(editor.lines()[0].parse::<u16>()?)?;
                        Ok((name, data))
                    }
                    _ => unreachable!(),
                }
            } else {
                Err(anyhow!("failed to access editor (cancel edit and retry)"))
            }
        } else {
            Err(anyhow!("failed to select team (cancel edit and retry)"))
        }
    }

    /// Validate that current editor contents will produce a team.
    fn validate_editor(&mut self) -> bool {
        let parse = self.parse_edit();

        self.editor.as_mut().is_some_and(|editor| {
            if let Err(err) = parse {
                editor.set_block(
                    Block::default()
                        .borders(Borders::ALL)
                        .border_type(BorderType::Rounded)
                        .border_style(Color::LightRed)
                        .title(format!("ERROR: {err}")),
                );
                false
            } else {
                editor.set_block(
                    Block::default()
                        .borders(Borders::ALL)
                        .border_type(BorderType::Rounded)
                        .border_style(Color::LightGreen)
                        .title("OK"),
                );
                true
            }
        })
    }

    /// Start edit mode on the currently selected cell.
    fn start_edit(&mut self) {
        if let Some((_, col)) = self.state.selected_cell() {
            let mut textarea = TextArea::default();

            let placeholder = match col {
                0 => "Enter a valid seed (1-16)",
                1 => "Enter the team name",
                2 => "Enter the global ranking points for the team",
                _ => unreachable!(),
            };

            textarea.set_placeholder_text(placeholder);
            self.editor = Some(textarea);

            // Validate immediately so an empty editor shows the correct state.
            self.validate_editor();
        }
    }

    /// Commit current editor contents and exit editor if it is valid.
    fn commit_edit(&mut self) {
        if let Ok(team) = self.parse_edit() {
            if let Some(index) = self.state.selected() {
                self.teams[index] = team;
            } else {
                self.teams.push(team);
            }

            self.on_teams_change();
            self.editor = None;
        }
    }

    /// Add a new team to the table.
    fn add_new_team(&mut self) {
        let seeds = self
            .teams
            .iter()
            .map(|(_, data)| data.seed)
            .collect::<Vec<_>>();

        let Some(seed) = Seed::iter_all().find(|n| !seeds.contains(n)) else {
            return;
        };

        let name = Name::try_new(format!("Team {seed}")).unwrap();
        let rating = Rating::try_new(1000).unwrap();
        self.teams.push((name, Team { seed, rating }));

        self.on_teams_change();

        self.state.select(
            self.teams
                .iter()
                .enumerate()
                .find_map(|(i, (_, data))| if data.seed == seed { Some(i) } else { None }),
        );
    }

    /// Remove the team at the currently selected row.
    fn remove_selected_team(&mut self) {
        if let Some(i) = self.state.selected() {
            self.teams.remove(i);

            if i >= self.teams.len() {
                self.state.select(self.teams.len().checked_sub(1));
            }
        }

        self.on_teams_change();
    }

    /// Resort teams and check for problems. Call when the teams vec is changed.
    fn on_teams_change(&mut self) {
        self.teams.sort_by_key(|(_, a)| a.seed);
        self.problems.clear();

        if self.teams.len() < 16 {
            self.problems
                .push(format!("Not enough teams ({}/16)", self.teams.len()));
        }

        let seeds = self
            .teams
            .iter()
            .map(|(_, data)| data.seed)
            .collect::<Vec<_>>();

        for seed in &seeds {
            if !Seed::iter_all().any(|valid_seed| valid_seed == *seed) {
                self.problems.push(format!("Invalid seed ({seed})"));
            }
        }

        for i in Seed::iter_all() {
            if seeds.iter().filter(|&&seed| seed == i).count() > 1 {
                self.problems.push(format!("Duplicate seed ({i})"));
            }
        }

        // `Name` equality is case-insensitive, so this catches names that only
        // differ by case as duplicates too.
        for (i, (name, _)) in self.teams.iter().enumerate() {
            if self
                .teams
                .iter()
                .skip(i + 1)
                .any(|(other_name, _)| name == other_name)
            {
                self.problems.push(format!("Duplicate name ({name})"));
            }
        }
    }

    /// Move the cursor to the next row.
    const fn next_row(&mut self) {
        let i = match self.state.selected() {
            Some(i) => {
                if i >= self.teams.len() - 1 {
                    0
                } else {
                    i + 1
                }
            }
            None => 0,
        };

        self.state.select(Some(i));
    }

    /// Move the cursor to the previous row.
    const fn previous_row(&mut self) {
        let i = match self.state.selected() {
            Some(i) => {
                if i == 0 {
                    self.teams.len() - 1
                } else {
                    i - 1
                }
            }
            None => 0,
        };

        self.state.select(Some(i));
    }

    /// Move the cursor to the next column.
    fn next_column(&mut self) {
        self.state.select_next_column();
    }

    /// Move the cursor to the previous column.
    fn previous_column(&mut self) {
        self.state.select_previous_column();
    }

    /// Render the user interface.
    #[allow(clippy::cast_possible_truncation)]
    fn render(&mut self, frame: &mut Frame) {
        let rects = Layout::vertical([
            Constraint::Min(17),
            Constraint::Length(if self.problems.is_empty() {
                1
            } else {
                self.problems.len() as u16 + 2
            }),
            Constraint::Length(if self.editor.is_some() { 3 } else { 0 }),
            Constraint::Length(if self.editor.is_some() { 3 } else { 4 }),
        ])
        .split(frame.area());

        self.render_table(frame, rects[0]);
        self.render_problems(frame, rects[1]);
        self.render_editor(frame, rects[2]);
        self.render_footer(frame, rects[3]);
    }

    /// Render the table displaying the teams.
    fn render_table(&mut self, frame: &mut Frame, area: Rect) {
        let header_style = Style::default();

        let selected_row_style = if self.editor.is_some() {
            Style::default()
        } else {
            Style::default().add_modifier(Modifier::REVERSED)
        };

        let selected_cell_style = if self.editor.is_some() {
            Style::default()
                .add_modifier(Modifier::BOLD)
                .add_modifier(Modifier::UNDERLINED)
                .add_modifier(Modifier::REVERSED)
        } else {
            Style::default()
                .add_modifier(Modifier::BOLD)
                .add_modifier(Modifier::UNDERLINED)
        };

        let header = ["Seed", "Team", "Rating"]
            .into_iter()
            .map(Cell::from)
            .collect::<Row>()
            .style(header_style)
            .height(1);

        let rows = self.teams.iter().map(|(name, data)| {
            let item = [
                format!(" {}.", data.seed),
                name.to_string(),
                format!("{}", data.rating),
            ];

            item.into_iter()
                .map(|content| Cell::from(Text::from(content)))
                .collect::<Row>()
                .height(1)
        });

        let t = Table::new(
            rows,
            [
                Constraint::Length(7),
                Constraint::Min(20),
                Constraint::Min(8),
            ],
        )
        .header(header)
        .row_highlight_style(selected_row_style)
        .cell_highlight_style(selected_cell_style)
        .highlight_spacing(HighlightSpacing::Always);

        frame.render_stateful_widget(t, area, &mut self.state);
    }

    /// Render any detected problems.
    fn render_problems(&self, frame: &mut Frame, area: Rect) {
        let problems = if self.problems.is_empty() {
            Paragraph::new(Text::from("No problems")).light_green()
        } else {
            Paragraph::new(self.problems.iter().map(String::as_str).collect::<Text>()).block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_type(BorderType::Rounded)
                    .border_style(Color::LightRed)
                    .title("Problems"),
            )
        };

        frame.render_widget(problems, area);
    }

    /// Render the editor when in edit mode.
    fn render_editor(&self, frame: &mut Frame, area: Rect) {
        if let Some(editor) = &self.editor {
            frame.render_widget(editor, area);
        }
    }

    /// Render the footer showing common keybinds.
    fn render_footer(&self, frame: &mut Frame, area: Rect) {
        let footer = if self.editor.is_some() {
            Paragraph::new(Text::from(EDITOR_FOOTER))
                .centered()
                .block(Block::bordered().border_type(BorderType::Rounded))
        } else {
            Paragraph::new(Text::from_iter(BROWSE_FOOTER))
                .centered()
                .block(Block::bordered().border_type(BorderType::Rounded))
        };

        frame.render_widget(footer, area);
    }
}
