//! `sitemap.xml` generator.

use chrono::{DateTime, Utc};
use html_escape::encode_text;
use serde::{Deserialize, Serialize};

/// A single `<url>` entry in the sitemap.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SitemapEntry {
    /// Fully-qualified URL.
    pub loc: String,
    /// Last modification timestamp.
    pub lastmod: Option<DateTime<Utc>>,
    /// Change frequency hint for crawlers.
    pub changefreq: Option<SitemapFrequency>,
    /// Relative priority within the site (0.0–1.0).
    pub priority: Option<f32>,
    /// Per-language alternates rendered as `<xhtml:link rel="alternate">`.
    pub alternates: Vec<SitemapAlternate>,
}

impl SitemapEntry {
    pub fn new(loc: impl Into<String>) -> Self {
        Self {
            loc: loc.into(),
            lastmod: None,
            changefreq: None,
            priority: None,
            alternates: Vec::new(),
        }
    }
}

/// Alternate-language URL declared inside a `<url>` entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SitemapAlternate {
    pub lang: String,
    pub href: String,
}

/// Change frequency hint for `<changefreq>` elements.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SitemapFrequency {
    Always,
    Hourly,
    Daily,
    Weekly,
    Monthly,
    Yearly,
    Never,
}

impl SitemapFrequency {
    pub fn as_str(self) -> &'static str {
        match self {
            SitemapFrequency::Always => "always",
            SitemapFrequency::Hourly => "hourly",
            SitemapFrequency::Daily => "daily",
            SitemapFrequency::Weekly => "weekly",
            SitemapFrequency::Monthly => "monthly",
            SitemapFrequency::Yearly => "yearly",
            SitemapFrequency::Never => "never",
        }
    }
}

/// Render a `sitemap.xml` document from the given entries.
///
/// The output starts with the XML declaration and uses the `xhtml` namespace
/// when any entry has alternates.
pub fn render_sitemap_xml(entries: &[SitemapEntry]) -> String {
    let needs_xhtml_ns = entries.iter().any(|e| !e.alternates.is_empty());
    let xhtml_attr = if needs_xhtml_ns {
        r#" xmlns:xhtml="http://www.w3.org/1999/xhtml""#
    } else {
        ""
    };
    let mut out = String::new();
    out.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
    out.push_str(&format!(
        r#"<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"{xhtml_attr}>"#
    ));
    for entry in entries {
        out.push_str("<url>");
        out.push_str(&format!("<loc>{}</loc>", encode_text(&entry.loc)));
        if let Some(lastmod) = &entry.lastmod {
            out.push_str(&format!(
                "<lastmod>{}</lastmod>",
                lastmod.format("%Y-%m-%dT%H:%M:%S%:z")
            ));
        }
        if let Some(freq) = entry.changefreq {
            out.push_str(&format!("<changefreq>{}</changefreq>", freq.as_str()));
        }
        if let Some(prio) = entry.priority {
            out.push_str(&format!("<priority>{:.1}</priority>", prio.clamp(0.0, 1.0)));
        }
        for alt in &entry.alternates {
            out.push_str(&format!(
                r#"<xhtml:link rel="alternate" hreflang="{}" href="{}" />"#,
                encode_text(&alt.lang),
                encode_text(&alt.href)
            ));
        }
        out.push_str("</url>");
    }
    out.push_str("</urlset>");
    out
}

#[cfg(test)]
mod tests {
    use chrono::TimeZone;

    use super::*;

    #[test]
    fn renders_minimal_entry() {
        let entries = vec![SitemapEntry::new("https://example.com/")];
        let xml = render_sitemap_xml(&entries);
        assert!(xml.starts_with(r#"<?xml version="1.0" encoding="UTF-8"?>"#));
        assert!(xml.contains("<loc>https://example.com/</loc>"));
        assert!(xml.contains("</urlset>"));
        assert!(!xml.contains("xmlns:xhtml"));
    }

    #[test]
    fn renders_full_entry() {
        let entry = SitemapEntry {
            loc: "https://example.com/about".into(),
            lastmod: Some(
                Utc.with_ymd_and_hms(2026, 3, 5, 12, 0, 0)
                    .single()
                    .expect("valid timestamp"),
            ),
            changefreq: Some(SitemapFrequency::Monthly),
            priority: Some(0.8),
            alternates: vec![SitemapAlternate {
                lang: "es".into(),
                href: "https://example.com/es/about".into(),
            }],
        };
        let xml = render_sitemap_xml(&[entry]);
        assert!(xml.contains("xmlns:xhtml"));
        assert!(xml.contains("<lastmod>2026-03-05T12:00:00+00:00</lastmod>"));
        assert!(xml.contains("<changefreq>monthly</changefreq>"));
        assert!(xml.contains("<priority>0.8</priority>"));
        assert!(xml.contains(
            r#"<xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/about" />"#
        ));
    }

    #[test]
    fn clamps_priority_range() {
        let mut e = SitemapEntry::new("https://example.com/");
        e.priority = Some(1.7);
        let xml = render_sitemap_xml(&[e]);
        assert!(xml.contains("<priority>1.0</priority>"));
    }
}
