use crossterm::event::KeyCode;
use ratatui::buffer::Buffer;
use ratatui::layout::Constraint;
use ratatui::layout::Layout;
use ratatui::layout::Rect;
use ratatui::text::Line;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;

use super::popup_consts::MAX_POPUP_ROWS;
use super::scroll_state::ScrollState;
use super::selection_popup_common::GenericDisplayRow;
use super::selection_popup_common::render_rows_single_line;
use crate::key_hint;
use crate::render::Insets;
use crate::render::RectExt;
use crate::text_formatting::truncate_text;
use codex_utils_fuzzy_match::fuzzy_match;

#[derive(Clone, Debug)]
pub(crate) struct MentionItem {
    pub(crate) display_name: String,
    pub(crate) description: Option<String>,
    pub(crate) insert_text: String,
    pub(crate) search_terms: Vec<String>,
    pub(crate) path: Option<String>,
    pub(crate) category_tag: Option<String>,
    pub(crate) sort_rank: u8,
}

const MENTION_NAME_TRUNCATE_LEN: usize = 28;

pub(crate) struct SkillPopup {
    query: String,
    mentions: Vec<MentionItem>,
    state: ScrollState,
}

impl SkillPopup {
    pub(crate) fn new(mentions: Vec<MentionItem>) -> Self {
        Self {
            query: String::new(),
            mentions,
            state: ScrollState::new(),
        }
    }

    pub(crate) fn set_mentions(&mut self, mentions: Vec<MentionItem>) {
        self.mentions = mentions;
        self.clamp_selection();
    }

    pub(crate) fn set_query(&mut self, query: &str) {
        self.query = query.to_string();
        self.clamp_selection();
    }

    pub(crate) fn calculate_required_height(&self, _width: u16) -> u16 {
        let rows = self.rows_from_matches(self.filtered());
        let visible = rows.len().clamp(1, MAX_POPUP_ROWS);
        (visible as u16).saturating_add(2)
    }

    pub(crate) fn move_up(&mut self) {
        let len = self.filtered_items().len();
        self.state.move_up_wrap(len);
        self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
    }

    pub(crate) fn move_down(&mut self) {
        let len = self.filtered_items().len();
        self.state.move_down_wrap(len);
        self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
    }

    pub(crate) fn selected_mention(&self) -> Option<&MentionItem> {
        let matches = self.filtered_items();
        let idx = self.state.selected_idx?;
        let mention_idx = matches.get(idx)?;
        self.mentions.get(*mention_idx)
    }

    fn clamp_selection(&mut self) {
        let len = self.filtered_items().len();
        self.state.clamp_selection(len);
        self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
    }

    fn filtered_items(&self) -> Vec<usize> {
        self.filtered().into_iter().map(|(idx, _, _)| idx).collect()
    }

    fn rows_from_matches(
        &self,
        matches: Vec<(usize, Option<Vec<usize>>, i32)>,
    ) -> Vec<GenericDisplayRow> {
        matches
            .into_iter()
            .map(|(idx, indices, _score)| {
                let mention = &self.mentions[idx];
                let name = truncate_text(&mention.display_name, MENTION_NAME_TRUNCATE_LEN);
                let description = match (
                    mention.category_tag.as_deref(),
                    mention.description.as_deref(),
                ) {
                    (Some(tag), Some(description)) if !description.is_empty() => {
                        Some(format!("{tag} {description}"))
                    }
                    (Some(tag), _) => Some(tag.to_string()),
                    (None, Some(description)) if !description.is_empty() => {
                        Some(description.to_string())
                    }
                    _ => None,
                };
                GenericDisplayRow {
                    name,
                    name_prefix_spans: Vec::new(),
                    match_indices: indices,
                    display_shortcut: None,
                    description,
                    category_tag: None,
                    is_disabled: false,
                    disabled_reason: None,
                    wrap_indent: None,
                }
            })
            .collect()
    }

    fn filtered(&self) -> Vec<(usize, Option<Vec<usize>>, i32)> {
        let filter = self.query.trim();
        let mut out: Vec<(usize, Option<Vec<usize>>, i32)> = Vec::new();

        for (idx, mention) in self.mentions.iter().enumerate() {
            if filter.is_empty() {
                out.push((idx, None, 0));
                continue;
            }

            let best_match =
                if let Some((indices, score)) = fuzzy_match(&mention.display_name, filter) {
                    Some((Some(indices), score))
                } else {
                    mention
                        .search_terms
                        .iter()
                        .filter(|term| *term != &mention.display_name)
                        .filter_map(|term| fuzzy_match(term, filter).map(|(_indices, score)| score))
                        .min()
                        .map(|score| (None, score))
                };

            if let Some((indices, score)) = best_match {
                out.push((idx, indices, score));
            }
        }

        out.sort_by(|a, b| {
            if filter.is_empty() {
                self.mentions[a.0]
                    .sort_rank
                    .cmp(&self.mentions[b.0].sort_rank)
            } else {
                a.1.is_none()
                    .cmp(&b.1.is_none())
                    .then_with(|| a.2.cmp(&b.2))
                    .then_with(|| {
                        self.mentions[a.0]
                            .sort_rank
                            .cmp(&self.mentions[b.0].sort_rank)
                    })
            }
            .then_with(|| {
                let an = self.mentions[a.0].display_name.as_str();
                let bn = self.mentions[b.0].display_name.as_str();
                an.cmp(bn)
            })
        });

        out
    }
}

impl WidgetRef for SkillPopup {
    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
        let (list_area, hint_area) = if area.height > 2 {
            let [list_area, _spacer_area, hint_area] = Layout::vertical([
                Constraint::Length(area.height - 2),
                Constraint::Length(1),
                Constraint::Length(1),
            ])
            .areas(area);
            (list_area, Some(hint_area))
        } else {
            (area, None)
        };
        let rows = self.rows_from_matches(self.filtered());
        render_rows_single_line(
            list_area.inset(Insets::tlbr(
                /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0,
            )),
            buf,
            &rows,
            &self.state,
            MAX_POPUP_ROWS,
            "no matches",
        );
        if let Some(hint_area) = hint_area {
            let hint_area = Rect {
                x: hint_area.x + 2,
                y: hint_area.y,
                width: hint_area.width.saturating_sub(2),
                height: hint_area.height,
            };
            skill_popup_hint_line().render(hint_area, buf);
        }
    }
}

fn skill_popup_hint_line() -> Line<'static> {
    Line::from(vec![
        "Press ".into(),
        key_hint::plain(KeyCode::Enter).into(),
        " to insert or ".into(),
        key_hint::plain(KeyCode::Esc).into(),
        " to close".into(),
    ])
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;
    use ratatui::buffer::Buffer;
    use ratatui::layout::Rect;

    fn mention_item(index: usize) -> MentionItem {
        MentionItem {
            display_name: format!("Mention {index:02}"),
            description: Some(format!("Description {index:02}")),
            insert_text: format!("$mention-{index:02}"),
            search_terms: vec![format!("mention-{index:02}")],
            path: Some(format!("skill://mention-{index:02}")),
            category_tag: Some("[Skill]".to_string()),
            sort_rank: 1,
        }
    }

    fn ranked_mention_item(
        display_name: &str,
        search_terms: &[&str],
        category_tag: &str,
        sort_rank: u8,
    ) -> MentionItem {
        MentionItem {
            display_name: display_name.to_string(),
            description: None,
            insert_text: format!("${display_name}"),
            search_terms: search_terms
                .iter()
                .map(|term| (*term).to_string())
                .collect(),
            path: None,
            category_tag: Some(category_tag.to_string()),
            sort_rank,
        }
    }

    fn named_mention_item(display_name: &str, search_terms: &[&str]) -> MentionItem {
        ranked_mention_item(display_name, search_terms, "[Skill]", /*sort_rank*/ 1)
    }

    fn plugin_mention_item(display_name: &str, search_terms: &[&str]) -> MentionItem {
        ranked_mention_item(display_name, search_terms, "[Plugin]", /*sort_rank*/ 0)
    }

    #[test]
    fn filtered_mentions_preserve_results_beyond_popup_height() {
        let popup = SkillPopup::new((0..(MAX_POPUP_ROWS + 2)).map(mention_item).collect());

        let filtered_names: Vec<String> = popup
            .filtered_items()
            .into_iter()
            .map(|idx| popup.mentions[idx].display_name.clone())
            .collect();

        assert_eq!(
            filtered_names,
            (0..(MAX_POPUP_ROWS + 2))
                .map(|idx| format!("Mention {idx:02}"))
                .collect::<Vec<_>>()
        );
        assert_eq!(
            popup.calculate_required_height(72),
            (MAX_POPUP_ROWS as u16) + 2
        );
    }

    fn render_popup(popup: &SkillPopup, width: u16) -> String {
        let area = Rect::new(0, 0, width, popup.calculate_required_height(width));
        let mut buf = Buffer::empty(area);
        popup.render_ref(area, &mut buf);
        format!("{buf:?}")
    }

    #[test]
    fn scrolling_mentions_shifts_rendered_window_snapshot() {
        let mut popup = SkillPopup::new((0..(MAX_POPUP_ROWS + 2)).map(mention_item).collect());

        for _ in 0..=MAX_POPUP_ROWS {
            popup.move_down();
        }

        insta::assert_snapshot!("skill_popup_scrolled", render_popup(&popup, /*width*/ 72));
    }

    #[test]
    fn display_name_match_sorting_beats_worse_secondary_search_term_matches() {
        let mut popup = SkillPopup::new(vec![
            named_mention_item("pr-review-triage", &["pr-review-triage"]),
            named_mention_item("prd", &["prd"]),
            named_mention_item("PR Babysitter", &["babysit-pr", "PR Babysitter"]),
            named_mention_item("Plugin Creator", &["plugin-creator", "Plugin Creator"]),
            named_mention_item(
                "Logging Best Practices",
                &["logging-best-practices", "Logging Best Practices"],
            ),
        ]);
        popup.set_query("pr");

        let filtered_names: Vec<String> = popup
            .filtered_items()
            .into_iter()
            .map(|idx| popup.mentions[idx].display_name.clone())
            .collect();

        assert_eq!(
            filtered_names,
            vec![
                "PR Babysitter".to_string(),
                "pr-review-triage".to_string(),
                "prd".to_string(),
                "Plugin Creator".to_string(),
                "Logging Best Practices".to_string(),
            ]
        );
    }

    #[test]
    fn query_match_score_sorts_before_plugin_rank_bias() {
        let mut popup = SkillPopup::new(vec![
            plugin_mention_item("GitHub", &["github", "pull requests", "pr"]),
            named_mention_item("pr-review-triage", &["pr-review-triage"]),
            named_mention_item("prd", &["prd"]),
            named_mention_item("Plugin Creator", &["plugin-creator", "Plugin Creator"]),
            named_mention_item(
                "Logging Best Practices",
                &["logging-best-practices", "Logging Best Practices"],
            ),
            named_mention_item("PR Babysitter", &["babysit-pr", "PR Babysitter"]),
        ]);
        popup.set_query("pr");

        let filtered_items: Vec<(String, Option<String>)> = popup
            .filtered_items()
            .into_iter()
            .map(|idx| {
                (
                    popup.mentions[idx].display_name.clone(),
                    popup.mentions[idx].category_tag.clone(),
                )
            })
            .collect();

        assert_eq!(
            filtered_items,
            vec![
                ("PR Babysitter".to_string(), Some("[Skill]".to_string())),
                ("pr-review-triage".to_string(), Some("[Skill]".to_string())),
                ("prd".to_string(), Some("[Skill]".to_string())),
                ("Plugin Creator".to_string(), Some("[Skill]".to_string())),
                (
                    "Logging Best Practices".to_string(),
                    Some("[Skill]".to_string())
                ),
                ("GitHub".to_string(), Some("[Plugin]".to_string())),
            ]
        );
    }
}
