use lazy_static::lazy_static;
use markdown_it::parser::block::{BlockRule, BlockState};
use markdown_it::parser::core::Root;
use markdown_it::parser::inline::{InlineRule, InlineState};
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
use maud::{Markup, PreEscaped, Render};
use std::str::FromStr;

use crate::components::fa::FAIcon;
use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail};

lazy_static! {
	static ref MdParser: MarkdownIt = {
		let mut md = markdown_it::MarkdownIt::new();
		markdown_it::plugins::cmark::add(&mut md);
		markdown_it::plugins::html::add(&mut md);
		md.inline.add_rule::<InlineEmote>();
		md.inline.add_rule::<InlineEmote>();
		md.inline.add_rule::<InlineMdx>();
		md.block.add_rule::<FrontMatter>().before_all();
		md
	};
}

pub struct Markdown<'a>(pub &'a str);

impl Render for Markdown<'_> {
	fn render(&self) -> Markup {
		let md = Self::parse(self.0);
		let html = md.render();
		return PreEscaped(html);
	}
}

impl Markdown<'_> {
	pub fn parse(md_str: &str) -> Node {
		MdParser.parse(md_str)
	}
}

//
// MARK: extensions
//

//
// MARK: emote
//

#[derive(Debug)]
pub struct InlineEmote(String);

impl NodeValue for InlineEmote {
	fn render(&self, _node: &Node, fmt: &mut dyn Renderer) {
		fmt.text_raw(self.0.as_str());
	}
}

impl InlineRule for InlineEmote {
	const MARKER: char = ':';

	fn run(state: &mut InlineState<'_, '_>) -> Option<(Node, usize)> {
		let input = &state.src[state.pos..state.pos_max];

		if !input.starts_with(':') {
			return None;
		}

		let end_idx = input[1..].find(':')? + 1;
		let code = &input[1..end_idx];

		let mut emote = None;

		if emote.is_none()
			&& let Some(code) = code.strip_prefix("fa-")
		{
			emote = FAIcon::from_str(code).ok().map(|x| x.render().0)
		}

		if emote.is_none() {
			emote = emojis::get_by_shortcode(code).map(|x| x.to_string());
		}

		Some((Node::new(InlineEmote(emote?)), end_idx + 1))
	}
}

//
// MARK: mdx
//

#[derive(Debug)]
pub struct InlineMdx(String);

impl NodeValue for InlineMdx {
	fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
		if mdx_style(&self.0, node, fmt) {
			return;
		}

		if mdx_include(&self.0, node, fmt) {
			return;
		}

		fmt.open("code", &[]);
		fmt.text(&self.0);
		fmt.close("code");
	}
}

impl InlineRule for InlineMdx {
	const MARKER: char = '{';

	fn run(state: &mut InlineState<'_, '_>) -> Option<(Node, usize)> {
		let input = &state.src[state.pos..state.pos_max];
		if !input.starts_with('{') {
			return None;
		}

		let mut balance = 1;
		let mut end = 1;
		for i in input[1..].bytes() {
			match i {
				b'}' => balance -= 1,
				b'{' => balance += 1,
				_ => {}
			}

			if balance == 0 {
				break;
			}

			end += 1;
		}

		if balance != 0 {
			return None;
		}

		let content = &input[1..end];
		Some((Node::new(InlineMdx(content.to_owned())), content.len() + 2))
	}
}

fn mdx_style(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
	// Parse inside of mdx: `style(<style>) <content>`
	let (style, content) = {
		let mdx = mdx.trim();
		if !mdx.starts_with("style(") {
			return false;
		}

		let skip = 6;
		let mut balance = 1;
		let mut end = skip;
		for i in mdx[skip..].bytes() {
			match i {
				b')' => balance -= 1,
				b'(' => balance += 1,
				_ => {}
			}

			if balance == 0 {
				break;
			}

			end += 1;
		}

		if balance != 0 {
			return false;
		}

		let style = mdx[skip..end].trim();
		let content = mdx[end + 1..].trim();

		(style, content)
	};

	let mut style_str = String::new();

	for kv in style.split(";") {
		let mut s = kv.split(":");
		let k = s.next();
		let v = s.next();

		if k.is_none() || v.is_none() || s.next().is_some() {
			return false;
		}

		#[expect(clippy::unwrap_used)] // Checked previously
		let k = k.unwrap().trim();

		#[expect(clippy::unwrap_used)] // Checked previously
		let v = v.unwrap().trim();

		match k {
			"color" => {
				style_str.push_str("color:");
				style_str.push_str(v);
				style_str.push(';');
			}

			"color_var" => {
				style_str.push_str("color:var(--");
				style_str.push_str(v);
				style_str.push_str(");");
			}

			_ => continue,
		}
	}

	// Only works with text, could be reworked to do basic md styling
	// (italics, bold, tab, code)
	fmt.open("span", &[("style", style_str)]);
	fmt.text(content);
	fmt.close("span");

	return true;
}

fn mdx_include(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
	// Parse inside of mdx: `include(<args>)`
	let args = {
		let mdx = mdx.trim();
		if !mdx.starts_with("include(") {
			return false;
		}

		let skip = 8;
		let mut balance = 1;
		let mut end = skip;
		for i in mdx[skip..].bytes() {
			match i {
				b')' => balance -= 1,
				b'(' => balance += 1,
				_ => {}
			}

			if balance == 0 {
				break;
			}

			end += 1;
		}

		if balance != 0 {
			return false;
		}

		let args = mdx[skip..end].trim();
		let trail = mdx[end + 1..].trim();
		if !trail.is_empty() {
			return false;
		}

		args
	};

	let str = match args {
		"email_beta" => MangledBetaEmail {}.render().0,
		"email_goog" => MangledGoogleEmail {}.render().0,
		_ => return false,
	};

	fmt.text_raw(&str);

	return true;
}

//
// MARK: frontmatter
//

#[derive(Debug)]
/// AST node for front-matter
pub struct FrontMatter {
	pub content: String,
}

impl NodeValue for FrontMatter {
	fn render(&self, _node: &Node, _fmt: &mut dyn Renderer) {}
}

impl BlockRule for FrontMatter {
	fn run(state: &mut BlockState<'_, '_>) -> Option<(Node, usize)> {
		// check the parent is the document Root
		if !state.node.is::<Root>() {
			return None;
		}

		// check we are on the first line of the document
		if state.line != 0 {
			return None;
		}

		// check line starts with opening dashes
		let opening = state
			.get_line(state.line)
			.chars()
			.take_while(|c| *c == '-')
			.collect::<String>();
		if !opening.starts_with("---") {
			return None;
		}

		// Search for the end of the block
		let mut next_line = state.line;
		loop {
			next_line += 1;
			if next_line >= state.line_max {
				return None;
			}

			let line = state.get_line(next_line);
			if line.starts_with(&opening) {
				break;
			}
		}

		let (content, _) = state.get_lines(state.line + 1, next_line, 0, true);
		Some((Node::new(FrontMatter { content }), next_line + 1))
	}
}
