use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;

use crate::images::ImageConfig;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxonomyDefinition {
    #[serde(default)]
    pub singular: Option<String>,
    #[serde(default)]
    pub index_template: Option<String>,
    #[serde(default)]
    pub term_template: Option<String>,
}

pub fn default_taxonomies() -> HashMap<String, TaxonomyDefinition> {
    let mut taxonomies = HashMap::new();
    taxonomies.insert(
        "tags".to_string(),
        TaxonomyDefinition {
            singular: Some("tag".to_string()),
            index_template: None,
            term_template: None,
        },
    );
    taxonomies.insert(
        "categories".to_string(),
        TaxonomyDefinition {
            singular: Some("category".to_string()),
            index_template: None,
            term_template: None,
        },
    );
    taxonomies
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Site {
    pub config: SiteConfig,
    pub home: Option<Page>,
    pub pages: Vec<Page>,
    pub posts: Vec<Post>,
    pub collections: HashMap<String, Collection>,
    pub data: HashMap<String, Value>,
    pub assets: Vec<Asset>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SiteConfig {
    pub title: String,
    pub base_url: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub author: Option<String>,
    #[serde(default)]
    pub language: Option<String>,
    #[serde(default = "default_posts_per_page")]
    pub posts_per_page: usize,
    #[serde(default)]
    pub minify: bool,
    #[serde(default)]
    pub fingerprint: bool,
    #[serde(default = "default_syntax_theme")]
    pub syntax_theme: String,
    #[serde(default)]
    pub images: Option<ImageConfig>,
    #[serde(default = "default_taxonomies")]
    pub taxonomies: HashMap<String, TaxonomyDefinition>,
    #[serde(default)]
    pub math: bool,
    #[serde(default)]
    pub extra: HashMap<String, Value>,
}

pub fn default_posts_per_page() -> usize {
    10
}

pub fn default_syntax_theme() -> String {
    "base16-ocean.dark".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TocEntry {
    pub level: u32,
    pub id: String,
    pub title: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Content {
    pub slug: String,
    pub title: String,
    #[serde(rename = "content")]
    pub html: String,
    #[serde(skip_serializing)]
    pub raw_content: String,
    pub frontmatter: Frontmatter,
    pub path: PathBuf,
    #[serde(default)]
    pub template: Option<String>,
    #[serde(default)]
    pub weight: i32,
    #[serde(default)]
    pub word_count: usize,
    #[serde(default)]
    pub reading_time: usize,
    #[serde(default)]
    pub toc: Vec<TocEntry>,
    #[serde(default)]
    pub url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Page {
    #[serde(flatten)]
    pub content: Content,
    #[serde(default)]
    pub draft: bool,
    #[serde(default)]
    pub redirect_from: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Post {
    #[serde(flatten)]
    pub content: Content,
    pub date: DateTime<Utc>,
    #[serde(default)]
    pub excerpt: Option<String>,
    #[serde(default)]
    pub draft: bool,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub categories: Vec<String>,
    #[serde(default)]
    pub taxonomies_map: HashMap<String, Vec<String>>,
    #[serde(default)]
    pub redirect_from: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Collection {
    pub name: String,
    pub items: Vec<CollectionItem>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollectionItem {
    #[serde(flatten)]
    pub content: Content,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Asset {
    pub source: PathBuf,
    pub dest: PathBuf,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Frontmatter {
    #[serde(flatten)]
    pub raw: HashMap<String, Value>,
}

impl Frontmatter {
    pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
        self.raw
            .get(key)
            .and_then(|v| serde_json::from_value(v.clone()).ok())
    }

    pub fn get_string(&self, key: &str) -> Option<String> {
        self.raw
            .get(key)
            .and_then(|value| value.as_str().map(String::from))
    }

    pub fn get_bool(&self, key: &str) -> Option<bool> {
        self.raw.get(key).and_then(|value| value.as_bool())
    }

    pub fn get_i64(&self, key: &str) -> Option<i64> {
        self.raw.get(key).and_then(|value| value.as_i64())
    }

    pub fn get_array(&self, key: &str) -> Option<Vec<String>> {
        self.raw.get(key).and_then(|value| {
            value.as_array().map(|array| {
                array
                    .iter()
                    .filter_map(|item| item.as_str().map(String::from))
                    .collect()
            })
        })
    }
}

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

    fn frontmatter_with(key: &str, value: Value) -> Frontmatter {
        let mut raw = HashMap::new();
        raw.insert(key.to_string(), value);
        Frontmatter { raw }
    }

    #[test]
    fn test_get_string_valid() {
        let frontmatter = frontmatter_with("title", Value::String("Hello".to_string()));
        assert_eq!(frontmatter.get_string("title"), Some("Hello".to_string()));
    }

    #[test]
    fn test_get_string_missing() {
        let frontmatter = Frontmatter::default();
        assert_eq!(frontmatter.get_string("title"), None);
    }

    #[test]
    fn test_get_string_wrong_type() {
        let frontmatter = frontmatter_with("title", Value::Bool(true));
        assert_eq!(frontmatter.get_string("title"), None);
    }

    #[test]
    fn test_get_bool_valid() {
        let frontmatter = frontmatter_with("draft", Value::Bool(true));
        assert_eq!(frontmatter.get_bool("draft"), Some(true));
    }

    #[test]
    fn test_get_bool_missing() {
        let frontmatter = Frontmatter::default();
        assert_eq!(frontmatter.get_bool("draft"), None);
    }

    #[test]
    fn test_get_bool_wrong_type() {
        let frontmatter = frontmatter_with("draft", Value::String("true".to_string()));
        assert_eq!(frontmatter.get_bool("draft"), None);
    }

    #[test]
    fn test_get_i64_valid() {
        let frontmatter = frontmatter_with("weight", serde_json::json!(42));
        assert_eq!(frontmatter.get_i64("weight"), Some(42));
    }

    #[test]
    fn test_get_i64_wrong_type() {
        let frontmatter = frontmatter_with("weight", Value::String("42".to_string()));
        assert_eq!(frontmatter.get_i64("weight"), None);
    }

    #[test]
    fn test_get_array_valid() {
        let frontmatter = frontmatter_with("tags", serde_json::json!(["rust", "web"]));
        assert_eq!(
            frontmatter.get_array("tags"),
            Some(vec!["rust".to_string(), "web".to_string()])
        );
    }

    #[test]
    fn test_get_array_wrong_type() {
        let frontmatter = frontmatter_with("tags", Value::String("rust".to_string()));
        assert_eq!(frontmatter.get_array("tags"), None);
    }

    #[test]
    fn test_get_generic() {
        let frontmatter = frontmatter_with("count", serde_json::json!(5));
        assert_eq!(frontmatter.get::<i64>("count"), Some(5));
    }

    #[test]
    fn test_default_posts_per_page() {
        assert_eq!(default_posts_per_page(), 10);
    }
}
