use crate::types::NoteMeta;
use chrono::{NaiveDate, NaiveDateTime, Utc};
use gray_matter::engine::YAML;
use gray_matter::Matter;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Default)]
struct RawFrontmatter {
    id: Option<String>,
    title: Option<String>,
    tags: Option<Vec<String>>,
    pinned: Option<bool>,
    created: Option<String>,
    modified: Option<String>,
}

/// Try to parse a date string in multiple common formats.
fn parse_date_flexible(s: &str) -> Option<chrono::DateTime<Utc>> {
    let s = s.trim();
    // RFC 3339 / ISO 8601 with timezone (e.g. 2024-01-15T10:30:00+00:00)
    if let Ok(dt) = s.parse::<chrono::DateTime<Utc>>() {
        return Some(dt);
    }
    // Try parsing as DateTime with fixed offset then convert
    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
        return Some(dt.with_timezone(&Utc));
    }
    // YYYY-MM-DD HH:MM:SS (no timezone)
    if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
        return Some(ndt.and_utc());
    }
    // YYYY-MM-DDTHH:MM:SS (no timezone, with T separator)
    if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
        return Some(ndt.and_utc());
    }
    // YYYY-MM-DD (date only)
    if let Ok(nd) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
        return nd.and_hms_opt(0, 0, 0).map(|ndt| ndt.and_utc());
    }
    None
}

pub fn parse_note(raw: &str, filename: &str) -> (NoteMeta, String) {
    let matter = Matter::<YAML>::new();
    let result = matter.parse(raw);

    let fm: RawFrontmatter = result
        .data
        .and_then(|d| d.deserialize().ok())
        .unwrap_or_default();

    let title = fm.title.unwrap_or_else(|| filename_to_title(filename));

    let created = fm
        .created
        .and_then(|s| parse_date_flexible(&s))
        .unwrap_or_else(Utc::now);

    let modified = fm
        .modified
        .and_then(|s| parse_date_flexible(&s))
        .unwrap_or_else(Utc::now);

    // Don't generate UUID on read - empty string signals "no ID yet"
    let id = fm.id.unwrap_or_default();

    let meta = NoteMeta {
        id,
        title,
        tags: fm.tags.unwrap_or_default(),
        pinned: fm.pinned.unwrap_or(false),
        created,
        modified,
    };

    let content = result.content;
    (meta, content)
}

/// Ensure the body starts with `# Title` heading for external app compatibility.
/// If the body already starts with `# Title`, leave it as-is.
fn ensure_title_heading(body: &str, title: &str) -> String {
    let trimmed = body.trim_start();

    // Check if body already starts with `# Title`
    if let Some(rest) = trimmed.strip_prefix("# ") {
        let first_line_end = rest.find('\n').unwrap_or(rest.len());
        let heading_text = rest[..first_line_end].trim();
        if heading_text.eq_ignore_ascii_case(title.trim()) {
            return body.to_string();
        }
    }

    // Don't add heading to empty notes (new/blank notes)
    if trimmed.is_empty() {
        return body.to_string();
    }

    // Prepend `# Title` heading
    format!("# {}\n\n{}", title, body)
}

pub fn serialize_frontmatter(meta: &NoteMeta) -> String {
    let tags_str = if meta.tags.is_empty() {
        "[]".to_string()
    } else {
        format!(
            "[{}]",
            meta.tags
                .iter()
                .map(|t| format!("{}", t))
                .collect::<Vec<_>>()
                .join(", ")
        )
    };

    format!(
        "---\nid: \"{}\"\ntitle: \"{}\"\ntags: {}\npinned: {}\ncreated: {}\nmodified: {}\n---\n",
        meta.id,
        meta.title.replace('"', "\\\""),
        tags_str,
        meta.pinned,
        meta.created.to_rfc3339(),
        meta.modified.to_rfc3339(),
    )
}

pub fn update_note_raw(meta: &NoteMeta, body: &str) -> String {
    let fm = serialize_frontmatter(meta);
    let body_with_heading = ensure_title_heading(body, &meta.title);
    format!("{}{}", fm, body_with_heading)
}

/// Merge NoteMeta fields into existing frontmatter, preserving unknown YAML keys.
/// Falls back to serialize_frontmatter() if the original has no frontmatter.
pub fn merge_frontmatter(original_raw: &str, meta: &NoteMeta, body: &str) -> String {
    let matter = Matter::<YAML>::new();
    let parsed = matter.parse(original_raw);

    // If original had no frontmatter, use the simple serializer
    if parsed.matter.trim().is_empty() {
        return update_note_raw(meta, body);
    }

    // Parse existing YAML into a Mapping to preserve all keys
    let mut mapping: serde_yaml::Mapping = match serde_yaml::from_str(&parsed.matter) {
        Ok(m) => m,
        Err(_) => return update_note_raw(meta, body), // unparseable YAML, fall back
    };

    // Update only our known fields
    if !meta.id.is_empty() {
        mapping.insert(
            serde_yaml::Value::String("id".into()),
            serde_yaml::Value::String(meta.id.clone()),
        );
    }
    mapping.insert(
        serde_yaml::Value::String("title".into()),
        serde_yaml::Value::String(meta.title.clone()),
    );
    mapping.insert(
        serde_yaml::Value::String("tags".into()),
        serde_yaml::Value::Sequence(
            meta.tags
                .iter()
                .map(|t| serde_yaml::Value::String(t.clone()))
                .collect(),
        ),
    );
    mapping.insert(
        serde_yaml::Value::String("pinned".into()),
        serde_yaml::Value::Bool(meta.pinned),
    );
    mapping.insert(
        serde_yaml::Value::String("created".into()),
        serde_yaml::Value::String(meta.created.to_rfc3339()),
    );
    mapping.insert(
        serde_yaml::Value::String("modified".into()),
        serde_yaml::Value::String(meta.modified.to_rfc3339()),
    );

    // Serialize the mapping back to YAML
    let yaml_str = match serde_yaml::to_string(&serde_yaml::Value::Mapping(mapping)) {
        Ok(s) => s,
        Err(_) => return update_note_raw(meta, body),
    };

    let body_with_heading = ensure_title_heading(body, &meta.title);
    format!("---\n{}---\n{}", yaml_str, body_with_heading)
}

fn filename_to_title(filename: &str) -> String {
    let stem = filename.trim_end_matches(".md");
    // Only replace dashes/underscores acting as word separators (between non-space chars).
    // Keep dashes that are surrounded by spaces (e.g. "Title - Subtitle").
    let mut result = String::with_capacity(stem.len());
    let chars: Vec<char> = stem.chars().collect();
    for (i, &ch) in chars.iter().enumerate() {
        if (ch == '-' || ch == '_')
            && i > 0
            && i < chars.len() - 1
            && chars[i - 1] != ' '
            && chars[i + 1] != ' '
        {
            result.push(' ');
        } else {
            result.push(ch);
        }
    }
    result
}

/// Quick title extraction from raw note content (frontmatter only, no full parse)
pub fn extract_title(raw: &str) -> Option<String> {
    let matter = Matter::<YAML>::new();
    let result = matter.parse(raw);
    let fm: RawFrontmatter = result
        .data
        .and_then(|d| d.deserialize().ok())
        .unwrap_or_default();
    fm.title
}

pub fn extract_preview(content: &str, max_len: usize) -> String {
    // Filter out headings and empty lines BEFORE stripping markdown,
    // since strip_html_and_markdown collapses all whitespace into one line.
    let filtered: String = content
        .lines()
        .filter(|l| {
            let trimmed = l.trim();
            !trimmed.is_empty()
                && !trimmed.starts_with('#')
                && !trimmed.starts_with("---")
                && !trimmed.starts_with("```")
        })
        .take(5)
        .collect::<Vec<_>>()
        .join("\n");

    let text = strip_html_and_markdown(&filtered);
    let trimmed = text.trim().to_string();

    if trimmed.len() > max_len {
        let mut end = max_len;
        while end > 0 && !trimmed.is_char_boundary(end) {
            end -= 1;
        }
        format!("{}...", &trimmed[..end])
    } else {
        trimmed
    }
}

fn strip_html_and_markdown(input: &str) -> String {
    let mut result = String::with_capacity(input.len());
    let mut chars = input.chars().peekable();
    let mut in_tag = false;

    while let Some(ch) = chars.next() {
        if in_tag {
            if ch == '>' {
                in_tag = false;
                // Add a space after closing tags to separate words
                result.push(' ');
            }
            continue;
        }

        if ch == '<' {
            in_tag = true;
            continue;
        }

        // Skip markdown image syntax: ![alt](url)
        if ch == '!' && chars.peek() == Some(&'[') {
            // Consume ![...](...) entirely
            chars.next(); // skip '['
            let mut depth = 1;
            // Skip alt text
            while let Some(c) = chars.next() {
                if c == '[' {
                    depth += 1;
                }
                if c == ']' {
                    depth -= 1;
                    if depth == 0 {
                        break;
                    }
                }
            }
            // Skip (url) if present
            if chars.peek() == Some(&'(') {
                chars.next();
                let mut depth = 1;
                while let Some(c) = chars.next() {
                    if c == '(' {
                        depth += 1;
                    }
                    if c == ')' {
                        depth -= 1;
                        if depth == 0 {
                            break;
                        }
                    }
                }
            }
            continue;
        }

        // Markdown links: [text](url) - keep the text, skip the URL
        if ch == '[' {
            let mut link_text = String::new();
            let mut depth = 1;
            while let Some(c) = chars.next() {
                if c == '[' {
                    depth += 1;
                }
                if c == ']' {
                    depth -= 1;
                    if depth == 0 {
                        break;
                    }
                }
                link_text.push(c);
            }
            // Skip (url) if present
            if chars.peek() == Some(&'(') {
                chars.next();
                let mut depth = 1;
                while let Some(c) = chars.next() {
                    if c == '(' {
                        depth += 1;
                    }
                    if c == ')' {
                        depth -= 1;
                        if depth == 0 {
                            break;
                        }
                    }
                }
                result.push_str(&link_text);
            } else {
                // Not a link, just brackets
                result.push('[');
                result.push_str(&link_text);
                result.push(']');
            }
            continue;
        }

        // Strip bold/italic markers
        if ch == '*' || ch == '_' {
            // Skip consecutive * or _
            while chars.peek() == Some(&ch) {
                chars.next();
            }
            continue;
        }

        // Strip strikethrough ~~
        if ch == '~' && chars.peek() == Some(&'~') {
            chars.next();
            continue;
        }

        // Strip inline code backticks
        if ch == '`' {
            while chars.peek() == Some(&'`') {
                chars.next();
            }
            continue;
        }

        result.push(ch);
    }

    // Collapse multiple spaces/newlines into single spaces
    let mut collapsed = String::with_capacity(result.len());
    let mut last_was_space = false;
    for ch in result.chars() {
        if ch.is_whitespace() {
            if !last_was_space {
                collapsed.push(' ');
                last_was_space = true;
            }
        } else {
            collapsed.push(ch);
            last_was_space = false;
        }
    }

    collapsed
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_extract_preview_basic() {
        let content =
            "## SIM Kaarten\n\n### Artjom\n\n**Telefoonnummer**\n\n**== 0456 74 49 91 ==**\n";
        let preview = extract_preview(content, 120);
        eprintln!("Preview: {:?}", preview);
        assert!(!preview.is_empty(), "Preview should not be empty");
    }

    #[test]
    fn test_extract_preview_no_frontmatter() {
        let content = "# Android flash\n\nAndroid flash\n\n[TWRP](https://twrp.me/)\n\nNotes;\n\n- Samsung S6\n";
        let preview = extract_preview(content, 120);
        eprintln!("Preview: {:?}", preview);
        assert!(!preview.is_empty(), "Preview should not be empty");
    }

    #[test]
    fn test_strip_html_markdown() {
        let input = "**Telefoonnummer**\n\n**== 0456 ==**";
        let stripped = strip_html_and_markdown(input);
        eprintln!("Stripped: {:?}", stripped);
        assert!(stripped.contains("Telefoonnummer"));
    }
}
