//! Markdown processing functionality

use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag, TagEnd};
use serde_yaml;

use super::metadata::MarkdownMetadata;

pub struct MarkdownProcessor {
    options: Options,
}

impl Default for MarkdownProcessor {
    fn default() -> Self {
        let mut options = Options::empty();
        options.insert(Options::ENABLE_STRIKETHROUGH);
        options.insert(Options::ENABLE_TABLES);
        options.insert(Options::ENABLE_FOOTNOTES);
        options.insert(Options::ENABLE_TASKLISTS);
        options.insert(Options::ENABLE_SMART_PUNCTUATION);
        options.insert(Options::ENABLE_HEADING_ATTRIBUTES);

        Self { options }
    }
}

impl MarkdownProcessor {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn parse_content(
        &self,
        markdown: &str,
    ) -> Result<(MarkdownMetadata, String), Box<dyn std::error::Error>> {
        let (metadata, content) = self.extract_frontmatter(markdown)?;
        let html = self.markdown_to_html(&content);
        Ok((metadata, html))
    }

    pub fn extract_frontmatter(
        &self,
        markdown: &str,
    ) -> Result<(MarkdownMetadata, String), Box<dyn std::error::Error>> {
        if let Some(stripped) = markdown.strip_prefix("---\n") {
            if let Some(end_pos) = stripped.find("\n---\n") {
                let frontmatter = &stripped[..end_pos];
                let content = &stripped[end_pos + 5..]; // 5 = "\n---\n".len()

                let metadata: MarkdownMetadata = serde_yaml::from_str(frontmatter)
                    .map_err(|e| format!("YAML parse error: {e} | Frontmatter: '{frontmatter}'"))?;

                return Ok((metadata, content.to_string()));
            }
        }

        Ok((MarkdownMetadata::default(), markdown.to_string()))
    }

    pub fn markdown_to_html(&self, markdown: &str) -> String {
        let parser = Parser::new_ext(markdown, self.options);
        let mut html_output = String::new();

        // Process events to handle code blocks with syntax highlighting
        let mut events = Vec::new();
        let mut in_code_block = false;
        let mut code_block_lang = None;
        let mut code_block_content = String::new();

        for event in parser {
            match event {
                Event::Start(Tag::CodeBlock(kind)) => {
                    in_code_block = true;
                    code_block_lang = match kind {
                        pulldown_cmark::CodeBlockKind::Fenced(lang) => Some(lang.to_string()),
                        _ => None,
                    };
                    code_block_content.clear();
                }
                Event::End(TagEnd::CodeBlock) => {
                    in_code_block = false;

                    // Generate syntax highlighted HTML
                    let highlighted_html =
                        self.highlight_code_block(&code_block_content, code_block_lang.as_deref());

                    // Push the highlighted HTML as a raw HTML event
                    events.push(Event::Html(CowStr::from(highlighted_html)));

                    code_block_lang = None;
                    code_block_content.clear();
                }
                Event::Text(text) if in_code_block => {
                    code_block_content.push_str(&text);
                }
                _ => events.push(event),
            }
        }

        // Convert events to HTML
        html::push_html(&mut html_output, events.into_iter());
        html_output
    }

    fn highlight_code_block(&self, code: &str, language: Option<&str>) -> String {
        let lang = language.unwrap_or("text");

        // Generate plain HTML with classes for highlight.js
        format!(
            r#"<pre><code class="language-{}" data-lang="{}">{}</code></pre>"#,
            lang,
            lang,
            html_escape(code)
        )
    }

    pub fn extract_excerpt(&self, html: &str, max_length: usize) -> String {
        let stripped = self.strip_html_tags(html);
        if stripped.len() <= max_length {
            stripped
        } else {
            let truncated = &stripped[..max_length];
            if let Some(last_space) = truncated.rfind(' ') {
                format!("{}...", &truncated[..last_space])
            } else {
                format!("{truncated}...")
            }
        }
    }

    fn strip_html_tags(&self, html: &str) -> String {
        let mut result = String::new();
        let mut in_tag = false;

        for char in html.chars() {
            match char {
                '<' => in_tag = true,
                '>' => in_tag = false,
                _ if !in_tag => result.push(char),
                _ => {}
            }
        }

        result.trim().to_string()
    }

    pub fn estimate_read_time(&self, content: &str) -> String {
        let word_count = content.split_whitespace().count();
        let minutes = (word_count / 200).max(1);
        format!("{minutes} min read")
    }
}

pub fn generate_slug(title: &str) -> String {
    title
        .to_lowercase()
        .chars()
        .map(|c| match c {
            'a'..='z' | '0'..='9' => c,
            ' ' | '-' | '_' => '-',
            _ => '-',
        })
        .collect::<String>()
        .split('-')
        .filter(|s| !s.is_empty())
        .collect::<Vec<&str>>()
        .join("-")
}

// Helper function to process a markdown file and return the processed content
pub fn process_markdown_file(
    content: &str,
) -> Result<(MarkdownMetadata, String), Box<dyn std::error::Error>> {
    let processor = MarkdownProcessor::new();
    processor.parse_content(content)
}

/// Rewrite relative image paths in rendered HTML so they resolve correctly
/// when the content is displayed at URLs that don't match the filesystem
/// layout.
///
/// Handles both markdown-style `![](images/…)` and raw HTML `<img
/// src="images/…">` embedded in markdown. Paths that are already absolute
/// (`/…`) or external (`http…`) are left untouched.
///
/// The rewritten base is `/content/{content_type}/{lang}/{slug}/` which maps to
/// the static tree populated by `copy-content-images.nu`.
pub fn rewrite_content_image_paths(
    html: &str,
    content_type: &str,
    lang: &str,
    slug: &str,
) -> String {
    let base = format!("/content/{content_type}/{lang}/{slug}/");

    // Regex-free approach: replace the well-known relative prefixes that appear
    // inside src="…" or src='…' attributes.
    let mut result = html.to_string();
    for prefix in &[
        "src=\"./images/",
        "src=\"images/",
        "src='./images/",
        "src='images/",
    ] {
        let quote = if prefix.contains('"') { "\"" } else { "'" };
        let replacement = format!("src={quote}{base}images/");
        result = result.replace(prefix, &replacement);
    }
    result
}

// HTML escape helper function
fn html_escape(s: &str) -> String {
    s.chars()
        .map(|c| match c {
            '&' => "&amp;".to_string(),
            '<' => "&lt;".to_string(),
            '>' => "&gt;".to_string(),
            '"' => "&quot;".to_string(),
            '\'' => "&#39;".to_string(),
            _ => c.to_string(),
        })
        .collect()
}
