mod func_link_graph;
mod funcs;
mod markdown;

use sable_core::config::Config;
use sable_vault::SharedVault;
use tera::{Context, Tera};

static DEFAULT_TEMPLATE_NAME: &str = "--sable-default.html";
static DEFAULT_TEMPLATE: &str = include_str!("default.html");

#[derive(Debug, miette::Diagnostic, thiserror::Error)]
pub enum TemplatesError {
    #[error("failed to convert path as its not valid utf-8")]
    PathNotUtf8,
    #[error("failed to load tera templates")]
    TeraInit(#[source] tera::Error),
    #[error("failed to render template")]
    RenderTemplate(#[source] tera::Error),
}

/// A wrapper around [`Tera`] that provides the built-in template and custom filters/functions.
#[derive(Debug)]
pub struct Templates {
    tera: Tera,

    default_template: Option<String>,
}

impl Templates {
    pub fn load(config: &Config, vault: SharedVault) -> Result<Self, TemplatesError> {
        let dir_str = config
            .templates
            .to_str()
            .ok_or(TemplatesError::PathNotUtf8)?;
        let dir_pattern = format!("{dir_str}/**/*");

        let mut tera = Tera::new(&dir_pattern).map_err(TemplatesError::TeraInit)?;

        tera.add_raw_template(DEFAULT_TEMPLATE_NAME, DEFAULT_TEMPLATE)
            .expect("failed to add default fallback template");

        tera.register_filter("markdown", markdown::Markdown::new(vault.clone()));

        tera.register_function("link_graph", func_link_graph::LinkGraph::new(vault));

        for template in tera.get_template_names() {
            tracing::debug!(template=%template, "loaded tera template");
        }

        Ok(Self {
            tera,

            default_template: config.default_template.clone(),
        })
    }

    // pub fn reload(&mut self) {
    //     self.tera.full_reload();
    // }

    /// Returns an iterator over the names of all registered templates in an unspecified order.
    pub fn get_template_names(&self) -> impl Iterator<Item = &str> {
        self.tera.get_template_names()
    }

    /// Checks if the given template is loaded.
    #[must_use]
    pub fn has_template(&self, name: &str) -> bool {
        self.get_template_names().any(|tn| tn == name)
    }

    pub fn render(&self, template_name: &str, context: &Context) -> Result<String, TemplatesError> {
        let name = if self.has_template(template_name) {
            template_name
        } else if let Some(default_template) = &self.default_template {
            default_template
        } else {
            tracing::warn!(template=%template_name, "requested template does not exist, using fallback");

            DEFAULT_TEMPLATE_NAME
        };

        self.tera
            .render(name, context)
            .map_err(TemplatesError::RenderTemplate)
    }
}

#[derive(Debug)]
struct FilterError {
    msg: String,
}

impl FilterError {
    fn new<M: ToString>(msg: M) -> Self {
        Self {
            msg: msg.to_string(),
        }
    }
}

impl std::fmt::Display for FilterError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.msg)
    }
}

impl std::error::Error for FilterError {}
