//! Script editor widget with completion, search, and sample finder popups.

use std::cell::Cell;

use crate::theme;
use ratatui::{
    layout::Rect,
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Clear, Paragraph},
    Frame,
};
use tui_textarea::TextArea;

/// Callback that syntax-highlights a single line, returning styled spans (bool = annotation).
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;

/// Metadata for a single autocomplete entry.
#[derive(Clone)]
pub struct CompletionCandidate {
    pub name: String,
    pub signature: String,
    pub description: String,
    pub example: String,
}

struct CompletionState {
    candidates: Vec<CompletionCandidate>,
    matches: Vec<usize>,
    cursor: usize,
    prefix: String,
    prefix_start_col: usize,
    active: bool,
    enabled: bool,
}

impl CompletionState {
    fn new() -> Self {
        Self {
            candidates: Vec::new(),
            matches: Vec::new(),
            cursor: 0,
            prefix: String::new(),
            prefix_start_col: 0,
            active: false,
            enabled: true,
        }
    }
}

struct SampleFinderState {
    query: String,
    folders: Vec<String>,
    matches: Vec<usize>,
    cursor: usize,
    active: bool,
}

impl SampleFinderState {
    fn new() -> Self {
        Self {
            query: String::new(),
            folders: Vec::new(),
            matches: Vec::new(),
            cursor: 0,
            active: false,
        }
    }
}

struct SearchState {
    query: String,
    active: bool,
}

impl SearchState {
    fn new() -> Self {
        Self {
            query: String::new(),
            active: false,
        }
    }
}

/// Multi-line text editor backed by tui_textarea.
pub struct Editor {
    text: TextArea<'static>,
    completion: CompletionState,
    sample_finder: SampleFinderState,
    search: SearchState,
    scroll_offset: Cell<u16>,
}

impl Editor {
    pub fn start_selection(&mut self) {
        self.text.start_selection();
    }

    pub fn cancel_selection(&mut self) {
        self.text.cancel_selection();
    }

    pub fn is_selecting(&self) -> bool {
        self.text.is_selecting()
    }

    pub fn move_cursor_to(&mut self, row: u16, col: u16) {
        self.text.move_cursor(tui_textarea::CursorMove::Jump(row, col));
    }

    pub fn scroll_offset(&self) -> u16 {
        self.scroll_offset.get()
    }

    pub fn copy(&mut self) {
        self.text.copy();
    }

    pub fn cut(&mut self) -> bool {
        self.text.cut()
    }

    pub fn paste(&mut self) -> bool {
        self.text.paste()
    }

    pub fn yank_text(&self) -> String {
        self.text.yank_text()
    }

    pub fn set_yank_text(&mut self, text: impl Into<String>) {
        self.text.set_yank_text(text);
    }

    pub fn select_all(&mut self) {
        self.text.select_all();
    }

    pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
        self.text.selection_range()
    }
}

impl Default for Editor {
    fn default() -> Self {
        Self::new()
    }
}

impl Editor {
    pub fn new() -> Self {
        Self {
            text: TextArea::default(),
            completion: CompletionState::new(),
            sample_finder: SampleFinderState::new(),
            search: SearchState::new(),
            scroll_offset: Cell::new(0),
        }
    }

    pub fn set_content(&mut self, lines: Vec<String>) {
        let yank = self.text.yank_text();
        self.text = TextArea::new(lines);
        if !yank.is_empty() {
            self.text.set_yank_text(yank);
        }
        self.completion.active = false;
        self.sample_finder.active = false;
        self.search.query.clear();
        self.search.active = false;
        self.scroll_offset.set(0);
    }

    pub fn set_candidates(&mut self, candidates: Vec<CompletionCandidate>) {
        self.completion.candidates = candidates;
    }

    pub fn insert_str(&mut self, s: &str) {
        self.text.insert_str(s);
    }

    pub fn content(&self) -> String {
        self.text.lines().join("\n")
    }

    pub fn lines(&self) -> &[String] {
        self.text.lines()
    }

    pub fn cursor(&self) -> (usize, usize) {
        self.text.cursor()
    }

    pub fn completion_active(&self) -> bool {
        self.completion.active
    }

    pub fn dismiss_completion(&mut self) {
        self.completion.active = false;
    }

    pub fn completion_next(&mut self) {
        if self.completion.cursor + 1 < self.completion.matches.len() {
            self.completion.cursor += 1;
        }
    }

    pub fn completion_prev(&mut self) {
        if self.completion.cursor > 0 {
            self.completion.cursor -= 1;
        }
    }

    pub fn set_completion_enabled(&mut self, enabled: bool) {
        self.completion.enabled = enabled;
        if !enabled {
            self.completion.active = false;
        }
    }

    pub fn activate_search(&mut self) {
        self.search.active = true;
        self.completion.active = false;
    }

    pub fn search_active(&self) -> bool {
        self.search.active
    }

    pub fn search_query(&self) -> &str {
        &self.search.query
    }

    pub fn search_input(&mut self, c: char) {
        self.search.query.push(c);
        self.apply_search_pattern();
    }

    pub fn search_backspace(&mut self) {
        self.search.query.pop();
        self.apply_search_pattern();
    }

    pub fn search_confirm(&mut self) {
        self.search.active = false;
    }

    pub fn search_clear(&mut self) {
        self.search.query.clear();
        self.search.active = false;
        let _ = self.text.set_search_pattern("");
    }

    pub fn search_next(&mut self) -> bool {
        if self.search.query.is_empty() {
            return false;
        }
        self.text.search_forward(false)
    }

    pub fn search_prev(&mut self) -> bool {
        if self.search.query.is_empty() {
            return false;
        }
        self.text.search_back(false)
    }

    fn apply_search_pattern(&mut self) {
        if self.search.query.is_empty() {
            let _ = self.text.set_search_pattern("");
        } else {
            let pattern = format!("(?i){}", regex::escape(&self.search.query));
            let _ = self.text.set_search_pattern(&pattern);
        }
    }

    pub fn set_sample_folders(&mut self, folders: Vec<String>) {
        self.sample_finder.folders = folders;
    }

    pub fn activate_sample_finder(&mut self) {
        self.completion.active = false;
        self.sample_finder.query.clear();
        self.sample_finder.cursor = 0;
        self.sample_finder.matches = (0..self.sample_finder.folders.len()).collect();
        self.sample_finder.active = true;
    }

    pub fn dismiss_sample_finder(&mut self) {
        self.sample_finder.active = false;
    }

    pub fn sample_finder_active(&self) -> bool {
        self.sample_finder.active
    }

    pub fn sample_finder_input(&mut self, c: char) {
        self.sample_finder.query.push(c);
        self.update_sample_finder_matches();
    }

    pub fn sample_finder_backspace(&mut self) {
        self.sample_finder.query.pop();
        self.update_sample_finder_matches();
    }

    pub fn sample_finder_next(&mut self) {
        if self.sample_finder.cursor + 1 < self.sample_finder.matches.len() {
            self.sample_finder.cursor += 1;
        }
    }

    pub fn sample_finder_prev(&mut self) {
        if self.sample_finder.cursor > 0 {
            self.sample_finder.cursor -= 1;
        }
    }

    pub fn accept_sample_finder(&mut self) {
        if self.sample_finder.matches.is_empty() {
            self.sample_finder.active = false;
            return;
        }
        let idx = self.sample_finder.matches[self.sample_finder.cursor];
        let name = self.sample_finder.folders[idx].clone();
        self.text.insert_str(&name);
        self.sample_finder.active = false;
    }

    fn update_sample_finder_matches(&mut self) {
        if self.sample_finder.query.is_empty() {
            self.sample_finder.matches = (0..self.sample_finder.folders.len()).collect();
        } else {
            let mut scored: Vec<(usize, usize)> = self
                .sample_finder
                .folders
                .iter()
                .enumerate()
                .filter_map(|(i, name)| fuzzy_match(&self.sample_finder.query, name).map(|s| (s, i)))
                .collect();
            scored.sort_by_key(|(score, _)| *score);
            self.sample_finder.matches = scored.into_iter().map(|(_, i)| i).collect();
        }
        self.sample_finder.cursor = self
            .sample_finder
            .cursor
            .min(self.sample_finder.matches.len().saturating_sub(1));
    }

    pub fn input(&mut self, input: impl Into<tui_textarea::Input>) {
        let input: tui_textarea::Input = input.into();
        let has_modifier = input.ctrl || input.alt;

        if self.completion.active && !has_modifier {
            match &input {
                tui_textarea::Input { key: tui_textarea::Key::Tab, .. } => {
                    self.accept_completion();
                    return;
                }
                tui_textarea::Input { key: tui_textarea::Key::Esc, .. } => {
                    self.completion.active = false;
                    return;
                }
                tui_textarea::Input { key: tui_textarea::Key::Char(c), .. } => {
                    if !is_word_char(*c) {
                        self.completion.active = false;
                    }
                    self.text.input(input);
                    self.update_completion();
                    return;
                }
                _ => {
                    self.completion.active = false;
                }
            }
        }

        self.text.input(input);
        if !has_modifier {
            self.update_completion();
        }
    }

    fn update_completion(&mut self) {
        if !self.completion.enabled || self.completion.candidates.is_empty() || self.sample_finder.active {
            return;
        }

        let (row, col) = self.text.cursor();
        let line = &self.text.lines()[row];

        // col is a character index; convert to byte offset for slicing
        let byte_col = line.char_indices()
            .nth(col)
            .map(|(i, _)| i)
            .unwrap_or(line.len());

        let prefix_start = line[..byte_col]
            .char_indices()
            .rev()
            .take_while(|(_, c)| is_word_char(*c))
            .last()
            .map(|(i, _)| i)
            .unwrap_or(byte_col);

        let prefix = &line[prefix_start..byte_col];

        if prefix.len() < 2 {
            self.completion.active = false;
            return;
        }

        let prefix_lower = prefix.to_lowercase();
        let matches: Vec<usize> = self
            .completion
            .candidates
            .iter()
            .enumerate()
            .filter(|(_, c)| c.name.to_lowercase().starts_with(&prefix_lower))
            .map(|(i, _)| i)
            .collect();

        if matches.is_empty() {
            self.completion.active = false;
            return;
        }

        if matches.len() == 1
            && self.completion.candidates[matches[0]].name.to_lowercase() == prefix_lower
        {
            self.completion.active = false;
            return;
        }

        self.completion.prefix = prefix.to_string();
        self.completion.prefix_start_col = prefix_start;
        self.completion.matches = matches;
        self.completion.cursor = self.completion.cursor.min(
            self.completion.matches.len().saturating_sub(1),
        );
        self.completion.active = true;
    }

    fn accept_completion(&mut self) {
        if self.completion.matches.is_empty() {
            self.completion.active = false;
            return;
        }

        let idx = self.completion.matches[self.completion.cursor];
        let name = self.completion.candidates[idx].name.clone();
        let prefix_len = self.completion.prefix.len();

        for _ in 0..prefix_len {
            self.text.delete_char();
        }
        self.text.insert_str(&name);

        self.completion.active = false;
    }

    pub fn render(&self, frame: &mut Frame, area: Rect, highlighter: Highlighter) {
        let t = theme::get();
        let (cursor_row, cursor_col) = self.text.cursor();
        let cursor_style = Style::default().bg(t.editor_widget.cursor_bg).fg(t.editor_widget.cursor_fg);
        let selection_style = Style::default().bg(t.editor_widget.selection_bg);

        let selection = self.text.selection_range();

        let lines: Vec<Line> = self
            .text
            .lines()
            .iter()
            .enumerate()
            .map(|(row, line)| {
                let tokens = highlighter(row, line);
                let mut spans: Vec<Span> = Vec::new();
                let mut col = 0;

                for (base_style, text, is_annotation) in tokens {
                    for ch in text.chars() {
                        let style = if is_annotation {
                            base_style
                        } else {
                            let is_cursor = row == cursor_row && col == cursor_col;
                            let is_selected = is_in_selection(row, col, selection);
                            if is_cursor {
                                cursor_style
                            } else if is_selected {
                                base_style.bg(selection_style.bg.unwrap())
                            } else {
                                base_style
                            }
                        };
                        spans.push(Span::styled(ch.to_string(), style));
                        if !is_annotation {
                            col += 1;
                        }
                    }
                }

                if row == cursor_row && cursor_col >= col {
                    spans.push(Span::styled(" ", cursor_style));
                }

                Line::from(spans)
            })
            .collect();

        let viewport_height = area.height as usize;
        let offset = self.scroll_offset.get() as usize;
        let offset = if cursor_row < offset {
            cursor_row
        } else if cursor_row >= offset + viewport_height {
            cursor_row - viewport_height + 1
        } else {
            offset
        };
        self.scroll_offset.set(offset as u16);

        frame.render_widget(Paragraph::new(lines).scroll((offset as u16, 0)), area);

        if self.sample_finder.active && !self.sample_finder.matches.is_empty() {
            self.render_sample_finder(frame, area, cursor_row - offset);
        } else if self.completion.active && !self.completion.matches.is_empty() {
            self.render_completion(frame, area, cursor_row - offset);
        }
    }

    fn render_completion(&self, frame: &mut Frame, editor_area: Rect, cursor_row: usize) {
        let t = theme::get();
        let max_visible: usize = 6;
        let list_width: u16 = 18;
        let doc_width: u16 = 40;
        let total_width = list_width + doc_width;

        let visible_count = self.completion.matches.len().min(max_visible);
        let list_height = visible_count as u16;
        let doc_height = 4u16;
        let total_height = list_height.max(doc_height);

        let popup_x = (editor_area.x + self.completion.prefix_start_col as u16)
            .min(editor_area.x + editor_area.width.saturating_sub(total_width));

        let below_y = editor_area.y + cursor_row as u16 + 1;
        let popup_y = if below_y + total_height > editor_area.y + editor_area.height {
            (editor_area.y + cursor_row as u16).saturating_sub(total_height)
        } else {
            below_y
        };

        let scroll_offset = if self.completion.cursor >= max_visible {
            self.completion.cursor - max_visible + 1
        } else {
            0
        };

        // List panel
        let list_area = Rect::new(popup_x, popup_y, list_width, total_height);
        frame.render_widget(Clear, list_area);

        let highlight_style = Style::default().fg(t.editor_widget.completion_selected).add_modifier(Modifier::BOLD);
        let normal_style = Style::default().fg(t.editor_widget.completion_fg);
        let bg_style = Style::default().bg(t.editor_widget.completion_bg);

        let list_lines: Vec<Line> = (scroll_offset..scroll_offset + visible_count)
            .map(|i| {
                let idx = self.completion.matches[i];
                let name = &self.completion.candidates[idx].name;
                let style = if i == self.completion.cursor {
                    highlight_style
                } else {
                    normal_style
                };
                let prefix = if i == self.completion.cursor { "> " } else { "  " };
                let display = format!("{prefix}{name:<width$}", width = list_width as usize - 2);
                Line::from(Span::styled(display, style.bg(t.editor_widget.completion_bg)))
            })
            .collect();

        // Fill remaining height with empty bg lines
        let mut all_list_lines = list_lines;
        for _ in visible_count as u16..total_height {
            all_list_lines.push(Line::from(Span::styled(
                " ".repeat(list_width as usize),
                bg_style,
            )));
        }

        frame.render_widget(Paragraph::new(all_list_lines), list_area);

        // Doc panel
        let doc_area = Rect::new(popup_x + list_width, popup_y, doc_width, total_height);
        frame.render_widget(Clear, doc_area);

        let selected_idx = self.completion.matches[self.completion.cursor];
        let candidate = &self.completion.candidates[selected_idx];

        let name_style = Style::default()
            .fg(t.editor_widget.completion_selected)
            .add_modifier(Modifier::BOLD)
            .bg(t.editor_widget.completion_bg);
        let desc_style = Style::default()
            .fg(t.editor_widget.completion_fg)
            .bg(t.editor_widget.completion_bg);
        let example_style = Style::default()
            .fg(t.editor_widget.completion_example)
            .bg(t.editor_widget.completion_bg);

        let w = doc_width as usize;
        let mut doc_lines: Vec<Line> = Vec::new();

        let header = format!(" {} {}", candidate.name, candidate.signature);
        doc_lines.push(Line::from(Span::styled(
            format!("{header:<w$}"),
            name_style,
        )));

        let desc = format!(" {}", candidate.description);
        doc_lines.push(Line::from(Span::styled(
            format!("{desc:<w$}"),
            desc_style,
        )));

        doc_lines.push(Line::from(Span::styled(" ".repeat(w), bg_style)));

        if !candidate.example.is_empty() {
            let ex = format!(" {}", candidate.example);
            doc_lines.push(Line::from(Span::styled(
                format!("{ex:<w$}"),
                example_style,
            )));
        }

        for _ in doc_lines.len() as u16..total_height {
            doc_lines.push(Line::from(Span::styled(" ".repeat(w), bg_style)));
        }

        frame.render_widget(Paragraph::new(doc_lines), doc_area);
    }

    fn render_sample_finder(&self, frame: &mut Frame, editor_area: Rect, cursor_row: usize) {
        let t = theme::get();
        let max_visible: usize = 8;
        let width: u16 = 24;

        let visible_count = self.sample_finder.matches.len().min(max_visible);
        let total_height = visible_count as u16 + 1; // +1 for query line

        let (_, cursor_col) = self.text.cursor();
        let popup_x = (editor_area.x + cursor_col as u16)
            .min(editor_area.x + editor_area.width.saturating_sub(width));

        let below_y = editor_area.y + cursor_row as u16 + 1;
        let popup_y = if below_y + total_height > editor_area.y + editor_area.height {
            (editor_area.y + cursor_row as u16).saturating_sub(total_height)
        } else {
            below_y
        };

        let area = Rect::new(popup_x, popup_y, width, total_height);
        frame.render_widget(Clear, area);

        let bg_style = Style::default().bg(t.editor_widget.completion_bg);
        let highlight_style = Style::default()
            .fg(t.editor_widget.completion_selected)
            .add_modifier(Modifier::BOLD);
        let normal_style = Style::default().fg(t.editor_widget.completion_fg);

        let w = width as usize;
        let mut lines: Vec<Line> = Vec::new();

        let query_display = format!("/{}", self.sample_finder.query);
        lines.push(Line::from(Span::styled(
            format!("{query_display:<w$}"),
            highlight_style.bg(t.editor_widget.completion_bg),
        )));

        let scroll_offset = if self.sample_finder.cursor >= max_visible {
            self.sample_finder.cursor - max_visible + 1
        } else {
            0
        };

        for i in scroll_offset..scroll_offset + visible_count {
            let idx = self.sample_finder.matches[i];
            let name = &self.sample_finder.folders[idx];
            let style = if i == self.sample_finder.cursor {
                highlight_style
            } else {
                normal_style
            };
            let prefix = if i == self.sample_finder.cursor { "> " } else { "  " };
            let display = format!("{prefix}{name:<width$}", width = w - 2);
            lines.push(Line::from(Span::styled(
                format!("{display:<w$}"),
                style.bg(t.editor_widget.completion_bg),
            )));
        }

        // Fill rest with bg
        for _ in lines.len() as u16..total_height {
            lines.push(Line::from(Span::styled(" ".repeat(w), bg_style)));
        }

        frame.render_widget(Paragraph::new(lines), area);
    }
}

/// Score a fuzzy match of `query` against `target`. Lower is better; `None` if no match.
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
    let target_lower: Vec<char> = target.to_lowercase().chars().collect();
    let query_lower: Vec<char> = query.to_lowercase().chars().collect();
    let mut ti = 0;
    let mut score = 0;
    let mut prev_pos = 0;
    for (qi, &qc) in query_lower.iter().enumerate() {
        loop {
            if ti >= target_lower.len() {
                return None;
            }
            if target_lower[ti] == qc {
                if qi > 0 {
                    score += ti - prev_pos;
                }
                prev_pos = ti;
                ti += 1;
                break;
            }
            ti += 1;
        }
    }
    Some(score)
}

fn is_word_char(c: char) -> bool {
    c.is_alphanumeric() || matches!(c, '!' | '@' | '?' | '.' | ':' | '_' | '#')
}

fn is_in_selection(row: usize, col: usize, selection: Option<((usize, usize), (usize, usize))>) -> bool {
    let Some(((start_row, start_col), (end_row, end_col))) = selection else {
        return false;
    };

    if row < start_row || row > end_row {
        return false;
    }

    if row == start_row && row == end_row {
        col >= start_col && col < end_col
    } else if row == start_row {
        col >= start_col
    } else if row == end_row {
        col < end_col
    } else {
        true
    }
}
