use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

pub const SUPPORTED_PICO_THEMES: [&str; 20] = [
    "default", "amber", "blue", "cyan", "fuchsia", "green", "grey", "indigo", "jade", "lime",
    "orange", "pink", "pumpkin", "purple", "red", "sand", "slate", "violet", "yellow", "zinc",
];

pub fn is_supported_pico_theme(theme: &str) -> bool {
    SUPPORTED_PICO_THEMES.contains(&theme)
}

pub fn pico_stylesheet_href(theme: Option<&str>) -> String {
    match theme.filter(|theme| is_supported_pico_theme(theme)) {
        Some("default") | None => "/assets/pico.min.css".to_string(),
        Some(theme) => format!("/assets/pico.{theme}.min.css"),
    }
}

fn default_max_posts() -> i32 {
    50
}

fn default_image_import_concurrency() -> i32 {
    4
}

fn deserialize_image_import_concurrency<'de, D>(deserializer: D) -> Result<i32, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = serde_json::Value::deserialize(deserializer)?;
    let parsed = value
        .as_i64()
        .map(|value| value as i32)
        .or_else(|| value.as_str().and_then(|value| value.parse::<i32>().ok()))
        .unwrap_or_else(default_image_import_concurrency);
    Ok(parsed.clamp(1, 8))
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectMetadata {
    #[serde(default)]
    pub blog_languages: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blogmark_category: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_author: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(
        default = "default_image_import_concurrency",
        deserialize_with = "deserialize_image_import_concurrency"
    )]
    pub image_import_concurrency: i32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub main_language: Option<String>,
    #[serde(default = "default_max_posts")]
    pub max_posts_per_page: i32,
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pico_theme: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub public_url: Option<String>,
    #[serde(default)]
    pub semantic_similarity_enabled: bool,
}

impl ProjectMetadata {
    /// Validate metadata fields per spec constraints.
    pub fn validate(&self) -> Result<(), String> {
        if self.max_posts_per_page < 1 || self.max_posts_per_page > 500 {
            return Err(format!(
                "maxPostsPerPage must be 1..500, got {}",
                self.max_posts_per_page
            ));
        }
        if !(1..=8).contains(&self.image_import_concurrency) {
            return Err(format!(
                "imageImportConcurrency must be 1..8, got {}",
                self.image_import_concurrency
            ));
        }
        if let Some(theme) = self.pico_theme.as_deref()
            && !is_supported_pico_theme(theme)
        {
            return Err(format!("unsupported picoTheme: {theme}"));
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CategorySettings {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub list_template_slug: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub post_template_slug: Option<String>,
    #[serde(default = "default_true")]
    pub render_in_lists: bool,
    #[serde(default = "default_true")]
    pub show_title: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub titles: BTreeMap<String, String>,
}

impl CategorySettings {
    pub fn title_for(&self, language: &str, main_language: &str) -> Option<&str> {
        if language.eq_ignore_ascii_case(main_language) {
            self.title.as_deref()
        } else {
            self.titles
                .iter()
                .find(|(candidate, _)| candidate.eq_ignore_ascii_case(language))
                .map(|(_, title)| title.as_str())
        }
        .map(str::trim)
        .filter(|title| !title.is_empty())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TagEntry {
    #[serde(skip_serializing_if = "option_is_none_or_blank")]
    pub color: Option<String>,
    pub name: String,
    #[serde(
        skip_serializing_if = "option_is_none_or_blank",
        rename = "postTemplateSlug"
    )]
    pub post_template_slug: Option<String>,
}

fn option_is_none_or_blank(value: &Option<String>) -> bool {
    value.as_deref().is_none_or(str::is_empty)
}

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

    #[test]
    fn project_metadata_roundtrip() {
        let meta = ProjectMetadata {
            name: "Test Blog".into(),
            description: None,
            public_url: Some("https://example.com".into()),
            main_language: Some("en".into()),
            default_author: None,
            max_posts_per_page: 50,
            image_import_concurrency: 4,
            blogmark_category: None,
            pico_theme: None,
            semantic_similarity_enabled: false,
            blog_languages: vec!["en".into(), "de".into()],
        };
        let json = serde_json::to_string_pretty(&meta).unwrap();
        let parsed: ProjectMetadata = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.name, "Test Blog");
        assert_eq!(parsed.max_posts_per_page, 50);
        assert_eq!(parsed.blog_languages, vec!["en", "de"]);
        // Verify camelCase
        assert!(json.contains("publicUrl"));
        assert!(json.contains("maxPostsPerPage"));
        assert!(json.contains("blogLanguages"));
    }

    #[test]
    fn project_metadata_defaults() {
        let json = r#"{"name": "Minimal"}"#;
        let meta: ProjectMetadata = serde_json::from_str(json).unwrap();
        assert_eq!(meta.max_posts_per_page, 50);
        assert_eq!(meta.image_import_concurrency, 4);
        assert!(!meta.semantic_similarity_enabled);
        assert!(meta.blog_languages.is_empty());
    }

    #[test]
    fn category_settings_defaults() {
        let json = "{}";
        let settings: CategorySettings = serde_json::from_str(json).unwrap();
        assert!(settings.render_in_lists);
        assert!(settings.show_title);
    }

    #[test]
    fn category_settings_camel_case() {
        let settings = CategorySettings {
            title: Some("Article".into()),
            titles: BTreeMap::new(),
            render_in_lists: false,
            show_title: true,
            post_template_slug: Some("article-tpl".into()),
            list_template_slug: None,
        };
        let json = serde_json::to_string(&settings).unwrap();
        assert!(json.contains("title"));
        assert!(json.contains("renderInLists"));
        assert!(json.contains("showTitle"));
        assert!(json.contains("postTemplateSlug"));
    }

    #[test]
    fn category_settings_select_title_for_render_language() {
        let settings = CategorySettings {
            title: Some("Artikel".into()),
            titles: std::collections::BTreeMap::from([("en".into(), "Articles".into())]),
            render_in_lists: true,
            show_title: true,
            post_template_slug: None,
            list_template_slug: None,
        };

        assert_eq!(settings.title_for("de", "de"), Some("Artikel"));
        assert_eq!(settings.title_for("en", "de"), Some("Articles"));
        assert_eq!(settings.title_for("fr", "de"), None);
    }

    #[test]
    fn tag_entry_roundtrip() {
        let tag = TagEntry {
            name: "rust".into(),
            color: Some("#ff0000".into()),
            post_template_slug: Some("code-tpl".into()),
        };
        let json = serde_json::to_string(&tag).unwrap();
        assert!(json.contains("postTemplateSlug"));
        let parsed: TagEntry = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.name, "rust");
        assert_eq!(parsed.color.as_deref(), Some("#ff0000"));
    }

    #[test]
    fn tag_entry_minimal() {
        let json = r#"{"name": "go"}"#;
        let tag: TagEntry = serde_json::from_str(json).unwrap();
        assert_eq!(tag.name, "go");
        assert!(tag.color.is_none());
        assert!(tag.post_template_slug.is_none());
    }

    #[test]
    fn max_posts_per_page_validation() {
        let mut meta = ProjectMetadata {
            name: "Test".into(),
            description: None,
            public_url: None,
            main_language: None,
            default_author: None,
            max_posts_per_page: 50,
            image_import_concurrency: 4,
            blogmark_category: None,
            pico_theme: None,
            semantic_similarity_enabled: false,
            blog_languages: vec![],
        };
        assert!(meta.validate().is_ok());

        meta.max_posts_per_page = 0;
        assert!(meta.validate().is_err());

        meta.max_posts_per_page = 501;
        assert!(meta.validate().is_err());

        meta.max_posts_per_page = 1;
        assert!(meta.validate().is_ok());

        meta.max_posts_per_page = 500;
        assert!(meta.validate().is_ok());
    }

    #[test]
    fn pico_theme_validation_accepts_only_bundled_themes() {
        let mut meta: ProjectMetadata = serde_json::from_str(r#"{"name":"Test"}"#).unwrap();
        meta.pico_theme = Some("amber".into());
        assert!(meta.validate().is_ok());

        meta.pico_theme = Some("nightfall".into());
        assert!(meta.validate().is_err());
    }
}
