//! Logseq format import with block support
//!
//! Imports Logseq markdown pages into KOGRAL nodes with full block parsing.
//! Preserves all Logseq features: blocks, tasks, tags, properties, references.

use std::collections::HashMap;

use chrono::{DateTime, Utc};

use crate::block_parser::BlockParser;
use crate::error::{KbError, Result};
use crate::models::{Node, NodeStatus, NodeType};
use crate::regex_patterns::{LOGSEQ_PROPERTY_PATTERN, LOGSEQ_WIKILINK_PATTERN, TAG_PATTERN};

/// Import a Logseq page into a KOGRAL Node
///
/// # Format Expected
///
/// ```markdown
/// title:: Page Title
/// tags:: [[tag1]], [[tag2]]
/// type:: note
///
/// - Block content
/// - TODO Task
/// ```
///
/// # Errors
///
/// Returns an error if Logseq content cannot be parsed or required properties
/// are missing
pub fn import_from_logseq_page(content: &str) -> Result<Node> {
    let (properties, body) = parse_logseq_properties(content);

    // Extract core properties
    let title = properties
        .get("title")
        .ok_or_else(|| KbError::Parser("Missing title property".to_string()))?
        .clone();

    let node_type = properties
        .get("type")
        .and_then(|t| parse_node_type(t))
        .unwrap_or(NodeType::Note);

    let status = properties
        .get("status")
        .and_then(|s| parse_node_status(s))
        .unwrap_or(NodeStatus::Draft);

    // Create node
    let mut node = Node::new(node_type, title);
    node.status = status;

    // Parse timestamps if present
    if let Some(created_str) = properties.get("created") {
        node.created = parse_logseq_date(created_str);
    }

    if let Some(modified_str) = properties.get("modified") {
        node.modified = parse_logseq_date(modified_str);
    }

    // Parse tags from property
    if let Some(tags_str) = properties.get("tags") {
        node.tags = parse_logseq_tags(tags_str);
    }

    // Parse relationships
    if let Some(relates) = properties.get("relates-to") {
        node.relates_to = parse_logseq_refs(relates);
    }

    if let Some(depends) = properties.get("depends-on") {
        node.depends_on = parse_logseq_refs(depends);
    }

    if let Some(implements) = properties.get("implements") {
        node.implements = parse_logseq_refs(implements);
    }

    if let Some(extends) = properties.get("extends") {
        node.extends = parse_logseq_refs(extends);
    }

    // Set content
    node.content.clone_from(&body);

    // Parse blocks from body
    if !body.is_empty() {
        if let Ok(blocks) = BlockParser::parse(&body) {
            if !blocks.is_empty() {
                node.blocks = Some(blocks);
            }
        } else {
            // If parsing fails, keep content as-is without blocks
        }
    }

    Ok(node)
}

/// Parse Logseq property format (`key::` value)
fn parse_logseq_properties(content: &str) -> (HashMap<String, String>, String) {
    let lines: Vec<&str> = content.lines().collect();
    let mut properties = HashMap::new();
    let mut body_start = 0;

    // Parse properties until we hit a blank line or non-property line
    for (i, line) in lines.iter().enumerate() {
        let trimmed = line.trim();

        // Empty line marks end of properties
        if trimmed.is_empty() {
            body_start = i + 1;
            break;
        }

        // Check if line is a property (key:: value)
        if let Some(captures) = LOGSEQ_PROPERTY_PATTERN.captures(trimmed) {
            let key = captures[1].to_string();
            let value = captures[2].to_string();
            properties.insert(key, value);
        } else if trimmed.starts_with('-') || trimmed.starts_with('*') {
            // Hit content, stop parsing properties
            body_start = i;
            break;
        }
    }

    let body = lines[body_start..].join("\n");

    (properties, body)
}

/// Parse Logseq date format: [[Jan 17th, 2026]] or ISO format
fn parse_logseq_date(date_str: &str) -> DateTime<Utc> {
    // Remove [[ ]] if present
    let clean = date_str.trim_matches(|c| c == '[' || c == ']').trim();

    // Try ISO 8601 format first (2026-01-17T10:30:00Z or 2026-01-17)
    if let Ok(dt) = DateTime::parse_from_rfc3339(clean) {
        return dt.with_timezone(&Utc);
    }

    // Try YYYY-MM-DD format
    if let Ok(date) = chrono::NaiveDate::parse_from_str(clean, "%Y-%m-%d") {
        return DateTime::<Utc>::from_naive_utc_and_offset(date.and_hms_opt(0, 0, 0).unwrap(), Utc);
    }

    // Try Logseq format: "Jan 17th, 2026" or "January 17, 2026"
    if let Some(parsed) = parse_english_date(clean) {
        return parsed;
    }

    // Try other common formats
    if let Ok(dt) = DateTime::parse_from_rfc2822(clean) {
        return dt.with_timezone(&Utc);
    }

    // Default to current time if all parsing fails
    Utc::now()
}

/// Parse English date formats: "Jan 17th, 2026", "January 17, 2026", "17 Jan
/// 2026"
fn parse_english_date(date_str: &str) -> Option<DateTime<Utc>> {
    // Month abbreviations and full names
    let months = [
        ("jan", "january", 1),
        ("feb", "february", 2),
        ("mar", "march", 3),
        ("apr", "april", 4),
        ("may", "may", 5),
        ("jun", "june", 6),
        ("jul", "july", 7),
        ("aug", "august", 8),
        ("sep", "september", 9),
        ("oct", "october", 10),
        ("nov", "november", 11),
        ("dec", "december", 12),
    ];

    let lower = date_str.to_lowercase();
    let clean = lower
        .replace("st,", "")
        .replace("nd,", "")
        .replace("rd,", "")
        .replace("th,", "")
        .replace("st", "")
        .replace("nd", "")
        .replace("rd", "")
        .replace("th", "");

    // Extract parts: month, day, year
    let parts: Vec<&str> = clean.split_whitespace().collect();

    if parts.len() < 2 {
        return None;
    }

    // Try "Month Day, Year" format: Jan 17, 2026
    if parts.len() >= 3 {
        if let Some(date) = try_parse_month_day_year(&parts, &months) {
            return Some(date);
        }
    }

    // Try "Day Month Year" format: 17 Jan 2026
    if parts.len() >= 3 {
        if let Some(date) = try_parse_day_month_year(&parts, &months) {
            return Some(date);
        }
    }

    None
}

/// Try parsing "Month Day Year" format
fn try_parse_month_day_year(
    parts: &[&str],
    months: &[(&str, &str, u32); 12],
) -> Option<DateTime<Utc>> {
    use chrono::NaiveDate;

    for (abbr, full, month_num) in months {
        if !parts[0].starts_with(abbr) && !parts[0].starts_with(full) {
            continue;
        }

        let day = parts[1].parse::<u32>().ok()?;
        let year = parts[2].parse::<i32>().ok()?;
        let date = NaiveDate::from_ymd_opt(year, *month_num, day)?;

        return Some(DateTime::<Utc>::from_naive_utc_and_offset(
            date.and_hms_opt(0, 0, 0)?,
            Utc,
        ));
    }

    None
}

/// Try parsing "Day Month Year" format
fn try_parse_day_month_year(
    parts: &[&str],
    months: &[(&str, &str, u32); 12],
) -> Option<DateTime<Utc>> {
    use chrono::NaiveDate;

    let day = parts[0].parse::<u32>().ok()?;

    for (abbr, full, month_num) in months {
        if !parts[1].starts_with(abbr) && !parts[1].starts_with(full) {
            continue;
        }

        let year = parts[2].parse::<i32>().ok()?;
        let date = NaiveDate::from_ymd_opt(year, *month_num, day)?;

        return Some(DateTime::<Utc>::from_naive_utc_and_offset(
            date.and_hms_opt(0, 0, 0)?,
            Utc,
        ));
    }

    None
}

/// Parse tags from Logseq format: [[tag1]], [[tag2]] or #tag1 #tag2
fn parse_logseq_tags(tags_str: &str) -> Vec<String> {
    let mut tags = Vec::new();

    // Parse [[tag]] format
    for cap in LOGSEQ_WIKILINK_PATTERN.captures_iter(tags_str) {
        tags.push(cap[1].to_string());
    }

    // Parse #tag format
    for cap in TAG_PATTERN.captures_iter(tags_str) {
        let tag = cap[1].to_string();
        if !tags.contains(&tag) {
            tags.push(tag);
        }
    }

    tags
}

/// Parse references from Logseq format: [[ref1]], [[ref2]]
fn parse_logseq_refs(refs_str: &str) -> Vec<String> {
    LOGSEQ_WIKILINK_PATTERN
        .captures_iter(refs_str)
        .map(|cap| cap[1].to_string())
        .collect()
}

/// Parse node type from string
fn parse_node_type(type_str: &str) -> Option<NodeType> {
    match type_str.to_lowercase().as_str() {
        "note" => Some(NodeType::Note),
        "decision" => Some(NodeType::Decision),
        "guideline" => Some(NodeType::Guideline),
        "pattern" => Some(NodeType::Pattern),
        "journal" => Some(NodeType::Journal),
        "execution" => Some(NodeType::Execution),
        _ => None,
    }
}

/// Parse node status from string
fn parse_node_status(status_str: &str) -> Option<NodeStatus> {
    match status_str.to_lowercase().as_str() {
        "draft" => Some(NodeStatus::Draft),
        "active" => Some(NodeStatus::Active),
        "superseded" => Some(NodeStatus::Superseded),
        "archived" => Some(NodeStatus::Archived),
        _ => None,
    }
}

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

    #[test]
    fn test_import_simple_page() {
        let content = r#"title:: Test Page
type:: note
tags:: [[rust]], [[testing]]
status:: active

- First block
- Second block with content"#;

        let node = import_from_logseq_page(content).unwrap();

        assert_eq!(node.title, "Test Page");
        assert_eq!(node.node_type, NodeType::Note);
        assert_eq!(node.status, NodeStatus::Active);
        assert_eq!(node.tags.len(), 2);
        assert!(node.tags.contains(&"rust".to_string()));
        assert!(node.tags.contains(&"testing".to_string()));
    }

    #[test]
    fn test_import_with_blocks() {
        let content = r#"title:: Task List
type:: note

- TODO Task 1 #important
- DONE Task 2
  - Nested detail"#;

        let node = import_from_logseq_page(content).unwrap();

        assert_eq!(node.title, "Task List");
        assert!(node.blocks.is_some());

        let blocks = node.blocks.unwrap();
        assert_eq!(blocks.len(), 2);
        assert_eq!(blocks[1].children.len(), 1);
    }

    #[test]
    fn test_import_with_relationships() {
        let content = r#"title:: Decision 1
type:: decision
relates-to:: [[node-123]], [[node-456]]
depends-on:: [[node-789]]

- Decision content"#;

        let node = import_from_logseq_page(content).unwrap();

        assert_eq!(node.title, "Decision 1");
        assert_eq!(node.node_type, NodeType::Decision);
        assert_eq!(node.relates_to.len(), 2);
        assert_eq!(node.depends_on.len(), 1);
        assert!(node.relates_to.contains(&"node-123".to_string()));
    }

    #[test]
    fn test_parse_logseq_properties() {
        let content = r#"title:: Test
type:: note
custom-prop:: value

- Content starts here"#;

        let (props, body) = parse_logseq_properties(content);

        assert_eq!(props.get("title"), Some(&"Test".to_string()));
        assert_eq!(props.get("type"), Some(&"note".to_string()));
        assert_eq!(props.get("custom-prop"), Some(&"value".to_string()));
        assert!(body.contains("Content starts here"));
    }

    #[test]
    fn test_parse_logseq_tags() {
        let tags = parse_logseq_tags("[[rust]], [[programming]], #testing");
        assert_eq!(tags.len(), 3);
        assert!(tags.contains(&"rust".to_string()));
        assert!(tags.contains(&"programming".to_string()));
        assert!(tags.contains(&"testing".to_string()));
    }

    #[test]
    fn test_parse_logseq_refs() {
        let refs = parse_logseq_refs("[[ref1]], [[ref2]], [[ref3]]");
        assert_eq!(refs.len(), 3);
        assert_eq!(refs[0], "ref1");
        assert_eq!(refs[1], "ref2");
        assert_eq!(refs[2], "ref3");
    }

    #[test]
    fn test_parse_node_type() {
        assert_eq!(parse_node_type("note"), Some(NodeType::Note));
        assert_eq!(parse_node_type("Decision"), Some(NodeType::Decision));
        assert_eq!(parse_node_type("GUIDELINE"), Some(NodeType::Guideline));
        assert_eq!(parse_node_type("invalid"), None);
    }

    #[test]
    fn test_import_missing_title() {
        let content = "type:: note\n\n- Content";
        let result = import_from_logseq_page(content);
        assert!(result.is_err());
    }
}
