use std::{
    fs::read_to_string,
    path::{Path, PathBuf},
    str::FromStr,
};

use chrono::{DateTime, Utc};
use serde::Serialize;

use crate::config::Config;

#[derive(Debug, Clone, Serialize)]
pub struct Page {
    /// The path to the markdown file representing a [`Page`]
    pub path: PathBuf,

    /// Freeform object, resulting from parsing the page file's frontmatter.
    /// Some of these values are potentially transformed and copied into other
    /// fields in [`Page`]
    pub metadata: toml::map::Map<String, toml::Value>,

    /// The title of the page, parsed from the frontmatter, defaults to the
    /// file name
    pub title: String,

    /// Priority of the page, parsed from the frontmatter.
    /// Pages in a site will be sorted by their weight.
    /// Lower weight pages come before ones with higher weight.
    /// Pages without a weight will always be sorted after weighted pages, and
    /// will in turn be sorted by their URL
    pub weight: Option<i64>,

    /// Publish date of the page, parsed from the frontmatter. Note that
    /// parsing can fail, resulting in an error printed during generation and
    /// this value remaining `None`.
    /// Another important note: this must be specified in the frontmatter as a
    /// date, not as a string.
    ///
    /// # Examples
    ///
    /// ```markdown
    /// +++
    /// datetime = 2024-11-09T10:28:04+01:00 # this is correct
    /// +++
    /// ```
    ///
    /// ```markdown
    /// +++
    /// datetime = "2024-11-09T10:28:04+01:00" # this is WRONG
    /// +++
    /// ```
    pub datetime: Option<DateTime<Utc>>,

    /// The file path relative to the theme folder of the template to use
    /// for the page, parsed from the frontmatter, defaults to `__page__.html`
    pub template: String,

    /// The raw content of the markdown file, including the unparsed frontmatter
    pub content: String,

    /// The URL of the page, inferred from the file path relative to the
    /// `content` directory. If [`Config::base_url`] is specified, this will be
    /// prefixed with it, otherwise it will be an absolute path.
    pub url: String,

    /// The path to the page, relative to the `content` directory, with the
    /// file extension from the template file used.
    pub relative_path: String,

    /// The path to the markdown file, relative to the `content` directory,
    /// minus the file extension
    pub relative_path_no_ext: String,

    /// Generated page file extension, with leading dot
    pub extension: String,

    /// If true, rendering of this page will be skipped. It will still be
    /// available in the [`tera::Context`] as an item inside
    /// [`Site::pages`](crate::site::Site::pages)
    pub skip: bool,

    /// HTML of the page, rendered using [`comrak`]
    pub html: String,
}

#[derive(Debug)]
pub enum PageParseErr {
    ReadErr(std::io::Error),
    FrontmatterParseErr(toml::de::Error),
    HtmlRenderErr(std::io::Error),
    Utf8EncodingErr(std::string::FromUtf8Error),
    PathRelativizeErr(std::path::StripPrefixError),
}

impl Page {
    pub fn new(
        path: PathBuf,
        root: &Path,
        config: &Config,
        comrak_opts: &comrak::Options,
        comrak_plugins: &comrak::Plugins,
    ) -> Result<Self, PageParseErr> {
        // it would be preferrable to use .file_prefix() but it's currently
        // unstable: https://github.com/rust-lang/rust/issues/86319
        let name = {
            let filename = path
                .file_name()
                .unwrap()
                .to_string_lossy()
                .to_string()
                .clone();
            let split = filename.split(".").collect::<Vec<&str>>();
            split.first().take().unwrap().to_string()
        };
        let relative_path_no_ext = path
            .strip_prefix(root.join("content"))
            .map_err(PageParseErr::PathRelativizeErr)?
            .to_string_lossy()
            .trim_end_matches(".md")
            .to_string();
        let relative_path = format!("{}.html", relative_path_no_ext);
        let mut this = Self {
            url: {
                format!(
                    "{}/{}",
                    config.base_url.clone().unwrap_or_default(),
                    if relative_path_no_ext.ends_with("index") {
                        PathBuf::from(&relative_path)
                            .parent()
                            .map(|p| p.to_string_lossy().to_string())
                            .unwrap_or("/".to_string())
                    } else if config.extensionless_pages {
                        relative_path_no_ext.clone()
                    } else {
                        relative_path.clone()
                    }
                )
            },
            relative_path,
            relative_path_no_ext,
            path,
            title: name,
            template: "__page__.html".into(),
            metadata: toml::map::Map::default(),
            content: String::default(),
            weight: None,
            datetime: None,
            extension: ".html".to_string(),
            skip: false,
            html: String::default(),
        };

        this.content = read_to_string(&this.path).map_err(PageParseErr::ReadErr)?;

        let arena = comrak::Arena::new();
        let md_root = comrak::parse_document(&arena, &this.content, comrak_opts);
        for node in md_root.descendants() {
            if let comrak::nodes::NodeValue::FrontMatter(ref frontmatter) = node.data.borrow().value
            {
                this.metadata = toml::from_str(
                    frontmatter
                        .trim()
                        .trim_start_matches("+++\n")
                        .trim_end_matches("\n+++"),
                )
                .map_err(PageParseErr::FrontmatterParseErr)?;
                for (k, v) in this.metadata.iter() {
                    match (k.as_str(), v) {
                        ("weight", toml::Value::Integer(weight)) => {
                            this.weight = Some(*weight);
                        }
                        ("title", toml::Value::String(title)) => {
                            this.title = title.clone();
                        }
                        ("skip", toml::Value::Boolean(skip)) => {
                            this.skip = *skip;
                        }
                        ("datetime", toml::Value::Datetime(dt)) => {
                            match DateTime::from_str(&dt.to_string()) {
                                Ok(dt) => this.datetime = Some(dt),
                                Err(e) => {
                                    eprintln!("Failed to parse datetime '{dt}': {e:#?}");
                                }
                            }
                        }
                        ("template", toml::Value::String(template)) => {
                            this.template = template.clone();
                            let templ_ext = PathBuf::from(&this.template)
                                .extension()
                                .unwrap_or_default()
                                .to_string_lossy()
                                .to_string();
                            if templ_ext != "html" {
                                this.extension = if templ_ext.is_empty() {
                                    String::default()
                                } else {
                                    format!(".{}", templ_ext)
                                };
                                this.relative_path = format!(
                                    "{}{}",
                                    this.relative_path.trim_end_matches(".html"),
                                    this.extension
                                );
                                if !config.extensionless_pages
                                    || this.relative_path_no_ext.ends_with("index")
                                {
                                    this.url = format!(
                                        "{}/{}",
                                        config.base_url.clone().unwrap_or_default(),
                                        this.relative_path
                                    );
                                }
                            }
                        }
                        _ => {}
                    }
                }
                break;
            }
        }
        let mut html_buf = vec![];

        comrak::format_html_with_plugins(md_root, comrak_opts, &mut html_buf, comrak_plugins)
            .map_err(PageParseErr::HtmlRenderErr)?;
        this.html = String::from_utf8(html_buf).map_err(PageParseErr::Utf8EncodingErr)?;

        Ok(this)
    }
}

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    use crate::config::Config;

    use super::Page;

    #[test]
    fn parse_page() {
        let page = Page::new(
            PathBuf::from("./examples/examplewebsite/content/about.md"),
            &Path::new("./examples/examplewebsite"),
            &Config::default(),
            &comrak::Options::default(),
            &comrak::Plugins::default(),
        )
        .unwrap();

        assert_eq!(page.metadata.len(), 3);
        assert_eq!(
            page.metadata.get("title").unwrap().as_str().unwrap(),
            "foo bar"
        );
        assert_eq!(
            page.metadata.get("image").unwrap().as_str().unwrap(),
            "/some/path"
        );
        assert_eq!(
            page.metadata
                .get("tags")
                .unwrap()
                .as_array()
                .unwrap()
                .get(0)
                .unwrap()
                .as_str()
                .unwrap(),
            "bar"
        );
        assert_eq!(
            page.metadata
                .get("tags")
                .unwrap()
                .as_array()
                .unwrap()
                .get(1)
                .unwrap()
                .as_str()
                .unwrap(),
            "baz"
        );
        assert_eq!(
            page.metadata
                .get("tags")
                .unwrap()
                .as_array()
                .unwrap()
                .get(2)
                .unwrap()
                .as_str()
                .unwrap(),
            "lorem"
        );

        assert_eq!(
            page.html,
            r#"<h1>Foo bar</h1>
<p>Lorem <strong>ipsum</strong> <em>dolor</em> sit <code>amet</code>.</p>
"#
        );
    }
}
