use chrono::{DateTime, Utc};
use pulldown_cmark::{html, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Debug, Clone)]
pub struct Post {
    pub name: String,
    pub title: String,
    #[allow(dead_code)]
    pub filename: String,
    #[allow(dead_code)]
    pub markdown_content: String,
    pub html_content: String,
    pub created_at: DateTime<Utc>,
    pub modified_at: DateTime<Utc>,
}

struct Entry {
    #[allow(dead_code)]
    short_name: String,
    file_path: String,
    created_at: DateTime<Utc>,
    modified_at: DateTime<Utc>,
    status: i32,
}

pub struct PostManager {
    root_dir: PathBuf,
    posts_dir_rel: String,
    posts: HashMap<String, Arc<RwLock<Post>>>,
    no_cache: bool,
}

impl PostManager {
    pub fn new(root_dir: &str, no_cache: bool) -> Result<Self, String> {
        let root_dir = PathBuf::from(root_dir);
        let posts_dir_rel = "posts";

        let mut manager = PostManager {
            root_dir,
            posts_dir_rel: posts_dir_rel.to_string(),
            posts: HashMap::new(),
            no_cache,
        };

        manager.refresh_posts()?;
        Ok(manager)
    }

    pub fn get_root_dir(&self) -> &PathBuf {
        &self.root_dir
    }

    fn query_posts(&self) -> Result<HashMap<String, Entry>, String> {
        let output = Command::new("git")
            .arg("log")
            .arg("--raw")
            .arg("--no-merges")
            .arg("--pretty=%h - %ad - %s")
            .arg("--date=unix")
            .arg("--")
            .arg(self.posts_dir_rel.clone())
            .current_dir(&self.root_dir)
            .output()
            .map_err(|e| format!("Failed to execute git whatchanged: {}", e))?;

        if !output.status.success() {
            return Err(format!(
                "Git whatchanged command failed: {}",
                String::from_utf8_lossy(&output.stderr)
            ));
        }

        let log_output = String::from_utf8_lossy(&output.stdout);
        let mut result: HashMap<String, Entry> = HashMap::new();
        let mut current_timestamp: Option<DateTime<Utc>> = None;

        for line in log_output.lines() {
            let line = line.trim();

            // Skip empty lines
            if line.is_empty() {
                continue;
            }

            // Parse commit header line: "f4fcf0e - 1761305168 - New post"
            if !line.starts_with(':') {
                if let Some(dash_pos) = line.find(" - ") {
                    let after_first_dash = &line[dash_pos + 3..];
                    if let Some(second_dash_pos) = after_first_dash.find(" - ") {
                        let timestamp_str = after_first_dash[..second_dash_pos].trim();
                        if let Ok(timestamp) = timestamp_str.parse::<i64>() {
                            current_timestamp = DateTime::from_timestamp(timestamp, 0);
                            eprintln!("Timestamp: {:?} ({})", current_timestamp, timestamp);
                        }
                    }
                }
                continue;
            }

            // Parse file change line: ":000000 100644 0000000 6bdad65 A        posts/hello-world-2.md"
            if line.starts_with(':') {
                eprintln!("Parsing line: {}", line);
                if let Some(timestamp) = current_timestamp {
                    let parts: Vec<&str> = line.split_whitespace().collect();
                    if parts.len() >= 6 {
                        let status = parts[4]; // A (add), D (delete), M (modify)
                        let file_path = parts[5];

                        let fn_ = file_path
                            .strip_prefix("posts/")
                            .and_then(|s| s.strip_suffix(".md"));
                        if let Some(name) = fn_ {
                            let entry = result.entry(name.to_string()).or_insert(Entry {
                                short_name: name.to_string(),
                                file_path: file_path.to_string(),
                                created_at: timestamp,
                                modified_at: timestamp,
                                status: if status == "D" { -1 } else { 1 },
                            });

                            // Always use the oldest timestamp for posts creation dates
                            entry.created_at = timestamp;
                        }
                    }
                } else {
                    eprintln!("Invalid git log output, expected prior timestamp: {}", line);
                }
            }
        }

        Ok(result)
    }

    pub fn refresh_posts(&mut self) -> Result<(), String> {
        self.posts.clear();

        // Get timestamps from git history
        let query_result = self.query_posts()?;
        for (name, entry) in query_result {
            if entry.status != 1 {
                continue;
            }
            let markdown_content = fs::read_to_string(&entry.file_path)
                .map_err(|e| format!("Failed to read post file: {}", e))?;
            let (html_content, title) = markdown_to_html(&markdown_content);
            let post = Post {
                name: name.clone(),
                title: title,
                filename: entry.file_path,
                markdown_content,
                html_content,
                created_at: entry.created_at,
                modified_at: entry.modified_at,
            };
            eprintln!("Loaded post: {} ({})", name, entry.created_at);
            self.posts.insert(name, Arc::new(RwLock::new(post)));
        }

        println!("Loaded {} posts", self.posts.len());
        Ok(())
    }

    pub async fn get_post(&self, name: &str) -> Option<Post> {
        let post_lock = self.posts.get(name)?;

        // If no_cache is enabled, always regenerate
        if self.no_cache {
            self.refresh_post_cache(name, post_lock).await;
        }

        let post = post_lock.read().await;
        Some(post.clone())
    }

    async fn refresh_post_cache(&self, name: &str, post_lock: &Arc<RwLock<Post>>) {
        let mut post = post_lock.write().await;
        let filename = post.filename.clone();

        if let Ok(markdown_content) = fs::read_to_string(&filename) {
            let (html_content, title) = markdown_to_html(&markdown_content);
            post.html_content = html_content;
            post.title = title;
            post.markdown_content = markdown_content;
            eprintln!("Refreshed post '{}'", name);
        }
    }

    // Get all posts, sorted by creation date
    pub async fn get_all_posts(&self) -> Vec<Post> {
        let mut posts = Vec::new();
        for (name, post_lock) in &self.posts {
            // If no_cache is enabled, always regenerate
            if self.no_cache {
                self.refresh_post_cache(name, post_lock).await;
            }

            let post = post_lock.read().await;
            posts.push(post.clone());
        }
        posts.sort_by(|a, b| b.created_at.cmp(&a.created_at));
        posts
    }

    // Get the timstamp of when the blog was most recently updated
    //  derived from latest post update
    pub async fn get_update_timestamp(&self) -> Result<DateTime<Utc>, String> {
        let posts = self.get_all_posts().await;
        let mut posts_sorted: Vec<&Post> = posts.iter().collect();
        posts_sorted.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
        Ok(posts_sorted
            .first()
            .ok_or("No posts found".to_string())?
            .created_at)
    }

    pub async fn get_posts_limited(&self, limit: usize) -> Vec<Post> {
        let mut posts = self.get_all_posts().await;
        posts.truncate(limit);
        posts
    }
}

fn markdown_title(markdown: &str) -> Option<String> {
    let parser = Parser::new(markdown);
    let mut in_tag = false;
    for event in parser {
        match event {
            Event::Start(Tag::Heading {
                level: HeadingLevel::H1,
                ..
            }) => in_tag = true,
            Event::End(TagEnd::Heading(HeadingLevel::H1)) => in_tag = false,
            Event::Text(txt) => {
                if in_tag {
                    return Some(txt.to_string());
                }
            }
            _ => {},
        }
    }
    None
}

fn markdown_to_html(markdown: &str) -> (String, String) {
    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);

    let title = markdown_title(markdown).unwrap_or("unknown".to_string());

    let parser = Parser::new_ext(markdown, options);
    let mut html_output = String::new();
    html::push_html(&mut html_output, parser);
    (html_output, title)
}
