use chrono::{DateTime, Utc};
use codespan_reporting::diagnostic::{Diagnostic, Label};
use serde::Deserialize;

use crate::{
    config::Config,
    state::{FileId, TomlError, Treehouse, toml_error_to_diagnostic},
    tree::attributes::{Picture, timestamp_from_id},
};

#[derive(Debug, Clone)]
pub struct Doc {
    pub attributes: Attributes,
    pub text: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct Attributes {
    /// Template to use for generating the page.
    /// Defaults to `_tree.hbs`.
    #[serde(default)]
    pub template: Option<String>,

    /// The unique ID of the doc.
    /// Required to appear in feeds.
    ///
    /// - New format: `doc?{date}-{name}`, where `{date}` is a `YYYY-MM-DD` date, and `{name}` is
    ///   the filename of the document (or otherwise a unique name which doesn't conflict with docs
    ///   made that day.)
    /// - Old format: `b?{ulid}`, where `{ulid}` is a ULID.
    ///   This follows the format of branches.
    #[serde(default)]
    pub id: String,

    /// Title of the page.
    /// The only necessary field.
    /// Unlike tree pages, doc pages always have titles.
    pub title: String,

    /// Tags assigned to the document.
    /// Generally, you want to assign public documents to #all for them to show up on the front page.
    #[serde(default)]
    pub tags: Vec<String>,

    /// Timestamp when the document was last updated.
    /// Required for inclusion in feeds.
    /// For pages with old style IDs, this is inferred from the ID.
    #[serde(default)]
    pub updated: Option<DateTime<Utc>>,

    /// ID of picture attached to the page, to be used as a thumbnail.
    #[serde(default)]
    pub thumbnail: Option<Picture>,

    /// Additional scripts to load into to the page.
    /// These are relative to the /static/js directory.
    #[serde(default)]
    pub scripts: Vec<String>,

    /// Additional styles to load into to the page.
    /// These are relative to the /static/css directory.
    #[serde(default)]
    pub styles: Vec<String>,

    /// If not `None`, the page will get an additional 'feed' field in template data, containing
    /// a feed of pages with the specified tag.
    #[serde(default)]
    pub include_feed: Option<IncludeFeed>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct IncludeFeed {
    /// The tag to look for.
    pub tag: String,

    /// The title of the feed shown on the page.
    pub title: String,
}

impl Doc {
    pub fn parse(
        treehouse: &mut Treehouse,
        config: &Config,
        file_id: FileId,
    ) -> (Doc, Vec<Diagnostic<FileId>>) {
        let mut diagnostics = vec![];

        let source = treehouse.source(file_id).input();

        let (front_matter, text) = source.split_once("+++").unwrap_or(("", source));
        let attributes_span = 0..front_matter.len();
        let mut attributes: Attributes =
            toml_edit::de::from_str(front_matter).unwrap_or_else(|error| {
                diagnostics.push(toml_error_to_diagnostic(TomlError {
                    message: error.message().to_owned(),
                    span: error.span(),
                    file_id,
                    input_range: attributes_span.clone(),
                }));
                Attributes::default()
            });

        // Infer attributes

        if let Some(branch_id) = attributes.id.strip_prefix("b?")
            && let Some(timestamp) = timestamp_from_id(branch_id)
        {
            attributes.updated = Some(timestamp);
        }

        // Emit warnings

        if !attributes.tags.is_empty() {
            if attributes.id.is_empty() {
                diagnostics.push(
                    Diagnostic::warning()
                        .with_code("attr")
                        .with_message("doc is tagged but missing id attribute")
                        .with_labels(vec![Label::primary(file_id, attributes_span.clone())])
                        .with_notes(vec!["id is required for showing up in feeds".into()]),
                );
            } else if attributes.updated.is_none() {
                diagnostics.push(
                    Diagnostic::warning()
                        .with_code("attr")
                        .with_message("doc is tagged but missing updated attribute")
                        .with_labels(vec![Label::primary(file_id, attributes_span.clone())])
                        .with_notes(vec![
                            "updated attribute is required for showing up in feeds".into(),
                        ]),
                );
            }

            for tag in &attributes.tags {
                if !config.feed.tags.contains(tag) {
                    diagnostics.push(
                        Diagnostic::warning()
                            .with_code("attr")
                            .with_message(format!("doc has unregistered tag `{tag}`"))
                            .with_labels(vec![Label::primary(file_id, attributes_span.clone())]),
                    );
                }
            }
        }

        (
            Doc {
                attributes,
                text: text.to_owned(),
            },
            diagnostics,
        )
    }
}
