use {
    crate::{
        style::TextInputStyle,
        util::CursorGuard,
        validation::{Validate, run_validator},
    },
    crossterm::{
        cursor,
        event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
        execute,
        terminal::{self, Clear, ClearType},
    },
    dyn_clone::DynClone,
    miette::IntoDiagnostic,
    owo_colors::OwoColorize,
    simsearch::SimSearch,
    std::io::{Write, stdout},
};

pub type Replacement = Option<String>;

pub trait Autocomplete: DynClone {
    fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, String>;

    fn get_completion(
        &mut self,
        input: &str,
        highlighted_suggestion: Option<String>,
    ) -> Result<Replacement, String>;
}

dyn_clone::clone_trait_object!(Autocomplete);

#[derive(Clone)]
pub struct SimpleAutocomplete {
    options: Vec<String>,
}

impl SimpleAutocomplete {
    pub fn new(options: Vec<String>) -> Self {
        Self { options }
    }
}

impl Autocomplete for SimpleAutocomplete {
    fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, String> {
        if input.is_empty() {
            return Ok(self.options.clone());
        }

        Ok(self
            .options
            .iter()
            .filter(|opt| opt.to_lowercase().starts_with(&input.to_lowercase()))
            .cloned()
            .collect())
    }

    fn get_completion(
        &mut self,
        input: &str,
        highlighted_suggestion: Option<String>,
    ) -> Result<Replacement, String> {
        if let Some(suggestion) = highlighted_suggestion {
            return Ok(Some(suggestion));
        }

        let suggestions = self.get_suggestions(input)?;
        if suggestions.is_empty() {
            return Ok(None);
        }

        if suggestions.len() == 1 {
            return Ok(Some(suggestions[0].clone()));
        }

        let first = &suggestions[0];
        let mut prefix = String::new();

        for (i, c) in first.chars().enumerate() {
            if suggestions
                .iter()
                .all(|s| s.chars().nth(i).map(|sc| sc == c).unwrap_or(false))
            {
                prefix.push(c);
            } else {
                break;
            }
        }

        if prefix.len() > input.len() {
            Ok(Some(prefix))
        } else {
            Ok(None)
        }
    }
}

#[derive(Clone)]
pub struct FuzzyAutocomplete {
    options: Vec<String>,
}

impl FuzzyAutocomplete {
    pub fn new(options: Vec<String>) -> Self {
        Self { options }
    }

    fn build_engine(&self) -> SimSearch<usize> {
        let mut engine = SimSearch::new();
        for (i, opt) in self.options.iter().enumerate() {
            engine.insert(i, opt);
        }
        engine
    }
}

impl Autocomplete for FuzzyAutocomplete {
    fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, String> {
        if input.is_empty() {
            return Ok(self.options.clone());
        }

        let engine = self.build_engine();
        let ids = engine.search(input);
        Ok(ids.into_iter().map(|i| self.options[i].clone()).collect())
    }

    fn get_completion(
        &mut self,
        input: &str,
        highlighted_suggestion: Option<String>,
    ) -> Result<Replacement, String> {
        if let Some(suggestion) = highlighted_suggestion {
            return Ok(Some(suggestion));
        }

        let suggestions = self.get_suggestions(input)?;
        if suggestions.is_empty() {
            return Ok(None);
        }

        if suggestions.len() == 1 {
            return Ok(Some(suggestions[0].clone()));
        }

        Ok(None)
    }
}

#[derive(Clone)]
pub struct TextInput {
    prompt: String,
    default: Option<String>,
    placeholder: Option<String>,
    initial_value: Option<String>,
    inline: bool,
    prompt_prefix: String,
    help_message: Option<String>,
    show_suggestions: bool,
    suggestion_page_size: usize,
    allow_escape: bool,
    style: TextInputStyle,
    validation: Option<Box<dyn Validate<str>>>,
    autocomplete: Option<Box<dyn Autocomplete>>,
    _cursor_guard: CursorGuard,
}

impl TextInput {
    pub fn new(prompt: impl Into<String>) -> Self {
        let _cursor_guard = CursorGuard::new().expect("Failed to initialize cursor guard");
        Self {
            prompt: prompt.into(),
            default: None,
            placeholder: None,
            initial_value: None,
            inline: false,
            prompt_prefix: "?".into(),
            help_message: None,
            show_suggestions: true,
            suggestion_page_size: 5,
            allow_escape: true,
            style: TextInputStyle::default(),
            validation: None,
            autocomplete: None,
            _cursor_guard,
        }
    }

    pub fn with_default(mut self, default: impl Into<String>) -> Self {
        self.default = Some(default.into());
        self
    }

    pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
        self.placeholder = Some(placeholder.into());
        self
    }

    pub fn with_initial_value(mut self, value: impl Into<String>) -> Self {
        self.initial_value = Some(value.into());
        self
    }

    pub fn with_inline(mut self, inline: bool) -> Self {
        self.inline = inline;
        self
    }

    pub fn with_prompt_prefix(mut self, prefix: impl Into<String>) -> Self {
        self.prompt_prefix = prefix.into();
        self
    }

    pub fn with_help_message(mut self, message: impl Into<String>) -> Self {
        self.help_message = Some(message.into());
        self
    }

    pub fn with_suggestions(mut self, enabled: bool) -> Self {
        self.show_suggestions = enabled;
        self
    }

    pub fn with_suggestion_page_size(mut self, size: usize) -> Self {
        self.suggestion_page_size = size;
        self
    }

    pub fn with_escape(mut self, allow: bool) -> Self {
        self.allow_escape = allow;
        self
    }

    pub fn with_style(mut self, style: TextInputStyle) -> Self {
        self.style = style;
        self
    }

    pub fn with_validation(mut self, validation: impl Validate<str> + 'static) -> Self {
        self.validation = Some(Box::new(validation));
        self
    }

    pub fn with_autocomplete<A: Autocomplete + 'static>(mut self, autocomplete: A) -> Self {
        self.autocomplete = Some(Box::new(autocomplete));
        self
    }

    pub fn prompt(&self) -> &str {
        &self.prompt
    }

    pub fn ask(&mut self) -> miette::Result<String> {
        let original_hook = std::panic::take_hook();
        std::panic::set_hook(Box::new(move |panic_info| {
            let _ = terminal::disable_raw_mode();
            std::panic::take_hook()(panic_info);
        }));

        let result = self._ask_internal();

        let _ = std::panic::take_hook();
        std::panic::set_hook(original_hook);

        result
    }

    pub fn _ask_internal(&mut self) -> miette::Result<String> {
        let mut input = self.initial_value.clone().unwrap_or_default();
        let mut cursor_pos = input.len();
        let mut suggestions: Vec<String> = Vec::new();
        let mut selected_suggestion: Option<usize> = None;
        let mut suggestion_scroll_offset: usize = 0;

        terminal::enable_raw_mode().into_diagnostic()?;

        while event::poll(std::time::Duration::from_millis(0)).into_diagnostic()? {
            event::read().into_diagnostic()?;
        }

        if let Some(ref mut ac) = self.autocomplete {
            suggestions = ac.get_suggestions(&input).unwrap_or_default();
        }

        let (mut _last_render_lines, mut last_input_line_position) = self.render(
            &mut stdout(),
            &input,
            cursor_pos,
            &suggestions,
            selected_suggestion,
            suggestion_scroll_offset,
        )?;
        stdout().flush().into_diagnostic()?;

        loop {
            if let Event::Key(key_event) = event::read().into_diagnostic()? {
                if key_event.kind != KeyEventKind::Press {
                    continue;
                }

                match self.handle_key(
                    key_event,
                    &mut input,
                    &mut cursor_pos,
                    &mut suggestions,
                    &mut selected_suggestion,
                    &mut suggestion_scroll_offset,
                    &mut stdout(),
                ) {
                    Ok(Some(answer)) => {
                        terminal::disable_raw_mode().into_diagnostic()?;

                        if last_input_line_position > 0 {
                            execute!(stdout(), cursor::MoveUp(last_input_line_position as u16))
                                .into_diagnostic()?;
                        }
                        execute!(stdout(), cursor::MoveToColumn(0)).into_diagnostic()?;
                        execute!(stdout(), Clear(ClearType::FromCursorDown)).into_diagnostic()?;
                        self.show_result(&mut stdout(), &answer)?;
                        return Ok(answer);
                    }
                    Ok(None) => {
                        if last_input_line_position > 0 {
                            execute!(stdout(), cursor::MoveUp(last_input_line_position as u16))
                                .into_diagnostic()?;
                        }
                        execute!(stdout(), cursor::MoveToColumn(0)).into_diagnostic()?;
                        execute!(stdout(), Clear(ClearType::FromCursorDown)).into_diagnostic()?;
                        let (lines, input_pos) = self.render(
                            &mut stdout(),
                            &input,
                            cursor_pos,
                            &suggestions,
                            selected_suggestion,
                            suggestion_scroll_offset,
                        )?;
                        _last_render_lines = lines;
                        last_input_line_position = input_pos;
                        stdout().flush().into_diagnostic()?;
                    }
                    Err(e) => {
                        terminal::disable_raw_mode().into_diagnostic()?;

                        if last_input_line_position > 0 {
                            execute!(stdout(), cursor::MoveUp(last_input_line_position as u16))
                                .into_diagnostic()?;
                        }
                        execute!(stdout(), cursor::MoveToColumn(0)).into_diagnostic()?;
                        execute!(stdout(), Clear(ClearType::FromCursorDown)).into_diagnostic()?;
                        self.show_error(&mut stdout(), &e)?;
                        stdout().flush().into_diagnostic()?;
                        return Err(miette::miette!(e));
                    }
                }
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn handle_key(
        &mut self,
        key_event: KeyEvent,
        input: &mut String,
        cursor_pos: &mut usize,
        suggestions: &mut Vec<String>,
        selected_suggestion: &mut Option<usize>,
        suggestion_scroll_offset: &mut usize,
        _out: &mut std::io::Stdout,
    ) -> Result<Option<String>, String> {
        if key_event.modifiers.contains(KeyModifiers::CONTROL)
            && matches!(key_event.code, KeyCode::Char('c'))
        {
            let _ = terminal::disable_raw_mode();
            std::process::exit(130);
        }

        match key_event.code {
            KeyCode::Enter => {
                if let Some(idx) = *selected_suggestion
                    && let Some(suggestion) = suggestions.get(idx)
                {
                    *input = suggestion.clone();
                    *cursor_pos = input.len();
                }

                let final_input = if input.is_empty() {
                    self.default.clone().unwrap_or_default()
                } else {
                    input.clone()
                };

                self.validate_and_return(&final_input)
            }
            KeyCode::Char(c) => {
                input.insert(*cursor_pos, c);
                *cursor_pos += 1;
                *selected_suggestion = None;
                *suggestion_scroll_offset = 0;

                if let Some(ref mut ac) = self.autocomplete {
                    *suggestions = ac.get_suggestions(input).unwrap_or_default();
                }

                Ok(None)
            }
            KeyCode::Backspace if *cursor_pos > 0 => {
                *cursor_pos -= 1;
                input.remove(*cursor_pos);
                *selected_suggestion = None;
                *suggestion_scroll_offset = 0;

                if let Some(ref mut ac) = self.autocomplete {
                    *suggestions = ac.get_suggestions(input).unwrap_or_default();
                }

                Ok(None)
            }
            KeyCode::Left if *cursor_pos > 0 => {
                *cursor_pos -= 1;
                Ok(None)
            }
            KeyCode::Right if *cursor_pos < input.len() => {
                *cursor_pos += 1;
                Ok(None)
            }
            KeyCode::Home => {
                *cursor_pos = 0;
                Ok(None)
            }
            KeyCode::End => {
                *cursor_pos = input.len();
                Ok(None)
            }
            KeyCode::Up if !suggestions.is_empty() => {
                *selected_suggestion = Some(match *selected_suggestion {
                    None => suggestions.len() - 1,
                    Some(0) => suggestions.len() - 1,
                    Some(n) => n - 1,
                });

                if let Some(selected) = *selected_suggestion {
                    if selected < *suggestion_scroll_offset {
                        *suggestion_scroll_offset = selected;
                    } else if selected >= *suggestion_scroll_offset + self.suggestion_page_size {
                        *suggestion_scroll_offset =
                            selected.saturating_sub(self.suggestion_page_size - 1);
                    }
                }
                Ok(None)
            }
            KeyCode::Down if !suggestions.is_empty() => {
                *selected_suggestion = Some(match *selected_suggestion {
                    None => 0,
                    Some(n) if n >= suggestions.len() - 1 => 0,
                    Some(n) => n + 1,
                });

                if let Some(selected) = *selected_suggestion {
                    if selected < *suggestion_scroll_offset {
                        *suggestion_scroll_offset = selected;
                    } else if selected >= *suggestion_scroll_offset + self.suggestion_page_size {
                        *suggestion_scroll_offset =
                            selected.saturating_sub(self.suggestion_page_size - 1);
                    }
                }
                Ok(None)
            }
            KeyCode::Tab if self.autocomplete.is_some() => {
                if let Some(ref mut ac) = self.autocomplete {
                    let highlighted =
                        selected_suggestion.and_then(|idx| suggestions.get(idx).cloned());

                    if let Ok(Some(replacement)) = ac.get_completion(input, highlighted) {
                        *input = replacement;
                        *cursor_pos = input.len();
                        *selected_suggestion = None;
                        *suggestion_scroll_offset = 0;

                        *suggestions = ac.get_suggestions(input).unwrap_or_default();
                    }
                }
                Ok(None)
            }
            KeyCode::Esc if self.allow_escape => Err("Cancelled".into()),
            _ => Ok(None),
        }
    }

    pub fn validate_and_return(&self, value: &str) -> Result<Option<String>, String> {
        if let Some(ref validator) = self.validation {
            run_validator(validator.as_ref(), value)?;
        }

        Ok(Some(value.to_string()))
    }

    pub fn render(
        &self,
        out: &mut std::io::Stdout,
        input: &str,
        cursor_pos: usize,
        suggestions: &[String],
        selected_suggestion: Option<usize>,
        suggestion_scroll_offset: usize,
    ) -> miette::Result<(usize, usize)> {
        let tw = crate::util::term_width();
        let mut line_count = 0;
        let mut prompt_prefix_for_cursor = 0;

        if self.inline {
            let line = format!(
                "{} {} ",
                self.prompt_prefix.style(self.style.prompt_prefix),
                self.prompt.style(self.style.prompt),
            );
            prompt_prefix_for_cursor = crate::util::visible_width(&line);
            write!(out, "{}", line).into_diagnostic()?;
        } else {
            let line = format!(
                "{} {}",
                self.prompt_prefix.style(self.style.prompt_prefix),
                self.prompt.style(self.style.prompt),
            );
            line_count += crate::util::writeln_physical(out, &line, tw)?;
        }

        if let Some(ref help) = self.help_message {
            let line = format!("  {}", help.style(self.style.hint));
            line_count += crate::util::writeln_physical(out, &line, tw)?;
        }

        let display_text = if input.is_empty() {
            self.placeholder
                .as_deref()
                .unwrap_or("")
                .style(self.style.placeholder)
                .to_string()
        } else {
            input.style(self.style.input).to_string()
        };

        let mut input_line = format!("  {} ", display_text);

        if input.is_empty()
            && let Some(default) = self.default.clone()
        {
            input_line = format!(
                "{}(default: {}) ",
                input_line,
                &default.style(self.style.default_value)
            );
        }

        line_count += crate::util::writeln_physical(out, &input_line, tw)?;

        let input_line_position = line_count - 1;

        if self.show_suggestions && !suggestions.is_empty() {
            let end_offset =
                (suggestion_scroll_offset + self.suggestion_page_size).min(suggestions.len());
            let visible_suggestions: Vec<_> = suggestions[suggestion_scroll_offset..end_offset]
                .iter()
                .enumerate()
                .map(|(rel_idx, s)| (suggestion_scroll_offset + rel_idx, s))
                .collect();

            if !visible_suggestions.is_empty() {
                let line = format!("  {}", "Suggestions:".style(self.style.hint));
                line_count += crate::util::writeln_physical(out, &line, tw)?;

                for (abs_idx, suggestion) in visible_suggestions {
                    let marker = if Some(abs_idx) == selected_suggestion {
                        "▸"
                    } else {
                        " "
                    };

                    let style = if Some(abs_idx) == selected_suggestion {
                        self.style.selected
                    } else {
                        self.style.suggestion
                    };

                    let line = format!(
                        "    {} {}",
                        marker.style(self.style.selected),
                        suggestion.style(style)
                    );
                    line_count += crate::util::writeln_physical(out, &line, tw)?;
                }

                let (above_text, below_text) = (
                    if suggestion_scroll_offset > 0 {
                        format!("↑ {} more above", suggestion_scroll_offset)
                    } else {
                        String::new()
                    },
                    if end_offset < suggestions.len() {
                        format!("↓ {} more below", suggestions.len() - end_offset)
                    } else {
                        String::new()
                    },
                );

                let parts: Vec<&str> = [
                    if !above_text.is_empty() {
                        Some(above_text.as_str())
                    } else {
                        None
                    },
                    if !below_text.is_empty() {
                        Some(below_text.as_str())
                    } else {
                        None
                    },
                ]
                .iter()
                .filter_map(|&x| x)
                .collect();

                if !parts.is_empty() {
                    let line = format!(
                        "    {}",
                        format!("({})", parts.join(" | ")).style(self.style.hint)
                    );
                    line_count += crate::util::writeln_physical(out, &line, tw)?;
                }
            }
        }

        let mut hints = vec![];
        if self.autocomplete.is_some() {
            hints.push("Tab to autocomplete");
        }
        if !suggestions.is_empty() {
            hints.push("↑↓ to navigate");
        }
        hints.push("Enter to submit");
        if self.allow_escape {
            hints.push("Esc to cancel");
        }

        if !hints.is_empty() {
            let line = format!("  {}", hints.join(", ").style(self.style.hint));
            line_count += crate::util::writeln_physical(out, &line, tw)?;
        }

        let lines_to_move_up = line_count - input_line_position;
        if lines_to_move_up > 0 {
            execute!(out, cursor::MoveUp(lines_to_move_up as u16)).into_diagnostic()?;
        }

        let text_before_cursor = &input[..cursor_pos.min(input.len())];
        let cursor_column =
            prompt_prefix_for_cursor + 2 + crate::util::visible_width(text_before_cursor);
        execute!(out, cursor::MoveToColumn(cursor_column as u16)).into_diagnostic()?;
        execute!(out, cursor::Show).into_diagnostic()?;

        Ok((line_count, input_line_position))
    }

    pub fn show_error(&self, out: &mut std::io::Stdout, error: &str) -> miette::Result<()> {
        let tw = crate::util::term_width();
        let line = format!(
            "{} {}",
            "✗".style(self.style.error),
            error.style(self.style.error_hint),
        );
        crate::util::writeln_physical(out, &line, tw)?;

        Ok(())
    }

    pub fn show_result(&self, out: &mut std::io::Stdout, answer: &str) -> miette::Result<()> {
        let tw = crate::util::term_width();
        let line = format!(
            "{} {} {}",
            self.prompt_prefix.style(self.style.prompt_prefix),
            self.prompt.style(self.style.prompt),
            answer.style(self.style.input).bold(),
        );
        crate::util::writeln_physical(out, &line, tw)?;

        out.flush().into_diagnostic()?;
        Ok(())
    }
}
