use crate::{dictionary, md, text, utils::ErrorCandidate};
use crossterm::event::{self, KeyCode};
use eyre::Result;
use itertools::Either::{Left, Right};
use owo_colors::OwoColorize as _;
use ratatui::{
	Frame,
	layout::{Constraint, Layout, Rect},
	style::{Color, Style},
	text::{Line, Span, Text},
	widgets::{
		Block, BorderType, Cell, Clear, List, ListItem, ListState, Row, Table, TableState,
	},
};
use std::{
	cmp::Reverse,
	fmt::Display,
	fs::{OpenOptions, read_to_string},
	io::Write,
	iter,
	path::Path,
};
use tui_input::{Input, InputRequest};

const NAME: &str = "عبد الرحمن صباهي";
const CURRENT_YEAR: &str = "2026";

pub(crate) fn run(
	file_path: impl AsRef<Path>,
	dictionary: Option<&str>,
) -> Result<()> {
	let dict = dictionary::Dictionary::init(dictionary)?;
	let dict_name = dict.name();

	let mut text = read_to_string(&file_path)?;
	let candidates = if file_path.as_ref().extension().is_some_and(|s| s == "md") {
		Left(md::get_error_candidates(&dict, &text))
	} else {
		Right(text::get_error_candidates(&dict, &text))
	};

	let (rows, action) = {
		let text = text.as_str();
		let mut model = Model::from_candidates(candidates);

		let action = ratatui::run(|terminal| -> Result<bool> {
			loop {
				terminal.draw(|frame| {
					model.render(
						frame,
						file_path.as_ref(),
						&dict_name,
						text,
						&dict,
					);
				})?;

				if let Some(action) = event::read()?
					.as_key_press_event()
					.and_then(|k| handle_key(k, &dict, &mut model))
				{
					return Ok(action);
				}
			}
		})?;

		(model.rows, action)
	};

	{
		let actions = collect_actions(rows);

		if !actions.additions.is_empty() {
			println!("{}", "ADDITIONS".bold());
			for word in &actions.additions {
				let status = if action && dict.add_word(word).is_ok() {
					"added to"
				} else {
					"would be added to"
				};
				println!("{:<12} {status} user dictionary", word.blue());
			}
		}

		if !actions.changes.is_empty() {
			println!("{}", "CHANGES".bold());
			for (range, new_word) in &actions.changes {
				let verb = if action { "replaced" } else { "would replace" };

				println!(
					"{:<12} {verb}\t {}",
					new_word.green(),
					(&text[*range]).red()
				);

				if action {
					text.replace_range(*range, new_word);
				}
			}
		}

		let mut file = OpenOptions::new().write(true).open(file_path)?;
		write!(file, "{text}")?;
	}

	Ok(())
}

struct Actions<'text> {
	changes: Vec<(std::range::Range<usize>, String)>,
	additions: Vec<&'text str>,
}

fn collect_actions(rows: Vec<(ErrorCandidate<'_>, Action)>) -> Actions<'_> {
	let mut changes = Vec::new();
	let mut additions = Vec::new();

	for row in rows {
		let (candidate, action) = row;
		match action {
			Action::Replace(replacement) => {
				for location in candidate.get_locations() {
					changes.push((*location, replacement.clone()));
				}
			}
			Action::Add => {
				additions.push(candidate.word);
			}
			Action::None | Action::Hide => {}
		}
	}
	changes.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));

	Actions { changes, additions }
}

fn handle_key(
	key: event::KeyEvent,
	dict: &dictionary::Dictionary,
	model: &mut Model<'_>,
) -> Option<bool> {
	match key.code {
		KeyCode::Char(c) if let Mode::Input(ref mut entry) = model.mode => {
			_ = entry.handle(InputRequest::InsertChar(c));
		}
		KeyCode::Backspace if let Mode::Input(ref mut entry) = model.mode => {
			_ = entry.handle(InputRequest::DeletePrevChar);
		}
		KeyCode::Enter if let Mode::Input(ref mut entry) = model.mode => {
			let entry = entry.value_and_reset();

			model.set_action(Action::Replace(entry));
			model.mode.reset();
		}
		KeyCode::Enter if let Mode::Suggestions(_) = model.mode => {
			model.mode = Mode::Input(Input::default());
		}
		KeyCode::Esc => model.mode.reset(),
		KeyCode::Char('?') => model.mode.toggle_help(),
		KeyCode::Char('h') => model.set_action(Action::Hide),
		KeyCode::Char('a') => model.set_action(Action::Add),
		KeyCode::Char('r') => model.set_action(Action::None),
		KeyCode::Char(' ') => match model.mode {
			Mode::Table => model.mode.toggle_suggestions(),
			Mode::Help => {}
			Mode::Suggestions(list_state) => {
				model.set_replace_suggestion(list_state.selected(), dict);
				model.mode.toggle_suggestions();
			}
			Mode::Input(ref mut entry) => {
				_ = entry.handle(InputRequest::InsertChar(' '));
			}
		},
		KeyCode::Char('s') => model.mode.toggle_suggestions(),
		KeyCode::Down => match model.mode {
			Mode::Table => model.state.select_next(),
			Mode::Suggestions(ref mut list) => list.select_next(),
			Mode::Help | Mode::Input(_) => {}
		},
		KeyCode::Up => match model.mode {
			Mode::Table => model.state.select_previous(),
			Mode::Suggestions(ref mut list) => list.select_previous(),
			Mode::Help | Mode::Input(_) => {}
		},
		// big finale
		KeyCode::Char('q') => return Some(false),
		KeyCode::Char('c') => return Some(true),
		_ => {}
	}

	None
}

struct Model<'text> {
	state: TableState,
	rows: Vec<(ErrorCandidate<'text>, Action)>,
	mode: Mode,
}

enum Mode {
	Table,
	Help,
	Suggestions(ListState),
	Input(Input),
}

impl Mode {
	fn reset(&mut self) {
		*self = Self::Table;
	}

	fn toggle_suggestions(&mut self) {
		match self {
			Self::Input(_) => {} // stupid hack
			Self::Suggestions(_) => *self = Self::Table,
			_ => *self = Self::Suggestions(ListState::default()),
		}
	}

	fn toggle_help(&mut self) {
		*self = match self {
			Self::Help => Self::Table,
			_ => Self::Help,
		};
	}
}

impl<'text> Model<'text> {
	fn from_candidates<T>(candidates: T) -> Self
	where
		T: Iterator<Item = ErrorCandidate<'text>>,
	{
		let state = TableState::new();
		let mut rows = candidates.map(|c| (c, Action::None)).collect::<Vec<_>>();

		rows.sort_by_key(|(c, _)| Reverse(c.count()));

		Self { state, rows, mode: Mode::Table }
	}

	fn render(
		&mut self,
		frame: &mut ratatui::Frame<'_>,
		file_path: &Path,
		dict_name: &str,
		text: &str,
		dict: &dictionary::Dictionary,
	) {
		const NAME: &str = "Hoopoe";
		const NAME_LENGTH: u16 = NAME.len() as u16;

		let layout = Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]);
		let [main, bottom] = frame.area().layout(&layout);

		let file_path = file_path.to_string_lossy();
		let [path, dict_v, title] =
			bottom.layout(&Layout::horizontal(Constraint::from_mins([
				file_path.len() as u16 + 1,
				dict_name.len() as u16 + 1,
				NAME_LENGTH,
			])));

		frame.render_widget(
			Span::from(NAME).style(Style::new().bold()).into_right_aligned_line(),
			title,
		);
		frame.render_widget(
			Span::from(dict_name)
				.style(Style::new().light_blue().dim())
				.into_centered_line(),
			dict_v,
		);
		frame.render_widget(
			Span::from(file_path).style(Style::new().dim()).into_left_aligned_line(),
			path,
		);

		self.render_table(frame, main, text);

		match self.mode {
			Mode::Table => {}
			Mode::Help => self.render_help(frame, main),
			Mode::Suggestions(_) => self.render_suggestions(frame, main, dict),
			Mode::Input(_) => self.render_input(frame, main),
		}
	}

	fn render_table(
		&mut self,
		frame: &mut Frame,
		area: Rect,
		text: &str,
	) {
		let header = Row::new(["word", "count", "context"])
			.style(Style::new().bold())
			.bottom_margin(1);

		let rows = self.rows.iter().filter(|(_, a)| *a != Action::Hide).map(|(c, a)| {
			let (before, after) = c.get_immediate_context::<50>(text);

			let style = match a {
				Action::None => Style::default(),
				Action::Replace(_) => Style::new().green(),
				Action::Hide => unreachable!("hidden!"),
				Action::Add => Style::new().blue(),
			};
			Row::new([
				if let Action::Replace(s) = a {
					Cell::new(Line::from_iter([
						Span::from(isolate_str(s)),
						Span::from(" "),
						Span::from(isolate_str(c.word))
							.style(Style::new().crossed_out().dim()),
					]))
				} else {
					Cell::new(isolate_str(c.word))
				},
				Cell::new(
					Span::from(format!("{}", c.count())).into_centered_line(),
				),
				Cell::new(Line::from_iter([
					Span::from(FSI),
					Span::from(before),
					Span::from(c.word).style(Style::new().yellow()),
					Span::from(after),
				])),
			])
			.style(style)
		});

		let widths = [Constraint::Max(15), Constraint::Max(5), Constraint::Fill(1)];
		let table = Table::new(rows, widths)
			.header(header)
			.column_spacing(1)
			.style(Color::White)
			.row_highlight_style(Style::new().on_black());

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

	fn render_help(
		&mut self,
		frame: &mut Frame<'_>,
		area: Rect,
	) {
		_ = self;
		let items = [
			["?", "toggle help window (this window)"],
			["ESC", "close popup window"],
			["h", "hide selected row for this session"],
			["a", "add selected row to user dictionary"],
			["r", "reset selected row status"],
			["s", "toggle suggestions panel"],
			["SPACE", "action selected item"],
			["\u{2191}", "go up"],
			["\u{2193}", "go down"],
			["c", "confirm and do actions "],
			["q", "quit without actioning "],
		];
		let max_width =
			10 + items.iter().map(|a| a[1].len()).reduce(std::cmp::Ord::max).unwrap()
				as u16;
		let widths = [Constraint::Length(10), Constraint::Fill(1)];
		let area = area.centered(
			Constraint::Length(10 + max_width),
			Constraint::Length(3 + items.len() as u16),
		);

		let rows = items.into_iter().map(|row| {
			Row::new([
				Span::from(row[0])
					.into_right_aligned_line()
					.style(Style::new().blue())
					.into(),
				Cell::new(row[1]),
			])
		});

		let footer = Row::new([
			Cell::default(),
			Span::from(format!("{NAME} - {CURRENT_YEAR}")).into(),
		])
		.style(Style::new().yellow());

		let table = Table::new(rows, widths)
			.style(Style::new().white())
			.block(Block::bordered())
			.column_spacing(1)
			.footer(footer);

		frame.render_widget(Clear, area);
		frame.render_widget(table, area);
	}

	fn render_suggestions(
		&mut self,
		frame: &mut Frame<'_>,
		area: Rect,
		dict: &dictionary::Dictionary,
	) {
		let Some((candidate, _)) = self.selected() else { return };
		let suggestions = candidate.get_suggestions(dict);

		let max_size = suggestions
			.iter()
			.map(std::string::String::len)
			.reduce(std::cmp::Ord::max)
			.map(|i| i + 2)
			.unwrap_or_default()
			.clamp(25, 50) as u16;

		let list = suggestions
			.iter()
			.cloned()
			.map(|s| ListItem::new(isolate_str(s)))
			.chain(iter::once(ListItem::new("<other>").style(Style::new().blue())))
			.collect::<List>()
			.style(Style::new().white())
			.block(Block::bordered())
			.highlight_style(Style::new().on_black());

		let Mode::Suggestions(ref mut list_state) = self.mode else {
			unreachable!("don't call this if not suggestions mode")
		};

		let area = area.centered(Constraint::Length(max_size), Constraint::Percentage(50));

		frame.render_widget(Clear, area);
		frame.render_stateful_widget(list, area, list_state);
	}

	fn render_input(
		&mut self,
		frame: &mut Frame<'_>,
		area: Rect,
	) {
		let Some(placeholder) = self.selected().map(|(c, _)| c.word) else {
			unreachable!("can't be called if nothing is selected")
		};
		let Mode::Input(ref current) = self.mode else {
			unreachable!("don't call this if not input mode")
		};

		let area = area.centered(Constraint::Length(25), Constraint::Length(3));
		let block = Block::bordered()
			.title_bottom(
				Span::from("<ENTER> to confirm")
					.style(Style::new().blue())
					.into_right_aligned_line(),
			)
			.border_type(BorderType::Rounded);

		frame.render_widget(Clear, area);
		frame.render_widget(&block, area);

		let area = block.inner(area);
		let contents = if current.cursor() > 0 {
			Text::from(isolate_str(current.value()))
		} else {
			Text::from(isolate_str(placeholder))
				.style(Style::new().dim())
				.right_aligned()
		};

		frame.render_widget(contents, area);
		frame.set_cursor_position((area.x + current.visual_cursor() as u16, area.y));
	}

	fn set_action(
		&mut self,
		action: Action,
	) {
		let Some((_, a)) = self.selected() else {
			return;
		};

		*a = if *a == action { Action::None } else { action };
	}

	fn set_replace_suggestion(
		&mut self,
		index: Option<usize>,
		dict: &dictionary::Dictionary,
	) {
		let Some(index) = index else { return };
		let Some((c, a)) = self.selected() else { return };

		if let Some(suggestion) = c.get_suggestions(dict).get(index) {
			*a = Action::Replace(suggestion.clone());
		} else {
			self.mode = Mode::Input(Input::default());
		}
	}

	fn selected(&mut self) -> Option<&mut (ErrorCandidate<'text>, Action)> {
		// terribly inefficient
		self.state.selected().and_then(|selected| {
			self.rows.iter_mut().filter(|(_, a)| *a != Action::Hide).nth(selected)
		})
	}
}

#[derive(PartialEq)]
enum Action {
	None,
	Replace(String),
	Hide,
	Add,
}

// Bidi forcing characters
const LRI: &str = "\u{2066}";
const FSI: &str = "\u{2066}";
const PDI: &str = "\u{2066}";

fn isolate_str(i: impl Display) -> String {
	format!("{LRI}{FSI}{i}{PDI}")
}
