//! Tera template engine integration
//!
//! Provides template-based rendering for exporting nodes to various formats
//! using the Tera template engine. Supports custom templates for Logseq,
//! JSON, markdown reports, and custom output formats.

use std::path::Path;

use serde_json::json;
use tera::{Context, Tera};
use tracing::{debug, info};

use crate::error::{KbError, Result};
use crate::models::Node;

/// Tera template engine for rendering nodes
///
/// Loads templates from a directory and provides template-based rendering
/// for nodes to various formats (Logseq, JSON, markdown reports, etc).
pub struct TeraEngine {
    tera: Tera,
}

impl TeraEngine {
    /// Create a new Tera engine with templates from the given directory
    ///
    /// Loads all `.html`, `.j2`, `.jinja2`, and `.tera` files from the
    /// templates directory.
    ///
    /// # Arguments
    ///
    /// * `templates_dir` - Path to directory containing template files
    ///
    /// # Errors
    ///
    /// Returns error if templates directory doesn't exist or templates are
    /// invalid
    pub fn new(templates_dir: &Path) -> Result<Self> {
        if !templates_dir.exists() {
            return Err(KbError::Parser(format!(
                "Templates directory not found: {}",
                templates_dir.display()
            )));
        }

        info!("Loading templates from {templates_dir:?}");

        // Build glob pattern for templates
        let pattern = format!("{}/**/*.{{html,j2,jinja2,tera}}", templates_dir.display());
        debug!("Template pattern: {pattern}");

        // Create and compile Tera instance
        let tera = Tera::new(&pattern)
            .map_err(|e| KbError::Parser(format!("Failed to load templates: {e}")))?;

        info!("Loaded {} templates", tera.get_template_names().count());

        Ok(Self { tera })
    }

    /// Create a Tera engine with built-in default templates
    ///
    /// Provides sensible defaults for common export formats.
    ///
    /// # Errors
    ///
    /// Returns error if default templates cannot be registered
    pub fn with_defaults() -> Result<Self> {
        let mut tera = Tera::default();

        // Register default Logseq template
        tera.add_raw_template(
            "logseq",
            r#"title:: {{ node.title }}
tags:: {% for tag in node.tags %}[[{{ tag }}]]{% if not loop.last %}, {% endif %}{% endfor %}
created:: {{ node.created | date(format="%Y-%m-%d") }}
modified:: {{ node.modified | date(format="%Y-%m-%d") }}
type:: {{ node.node_type }}
status:: {{ node.status }}

{{ node.content }}"#,
        )
        .map_err(|e| KbError::Parser(format!("Failed to register template: {e}")))?;

        // Register default JSON template
        tera.add_raw_template(
            "json",
            r#"{
  "id": "{{ node.id }}",
  "title": "{{ node.title }}",
  "type": "{{ node.node_type }}",
  "status": "{{ node.status }}",
  "tags": [{% for tag in node.tags %}"{{ tag }}"{% if not loop.last %},{% endif %}{% endfor %}],
  "content": "{{ node.content | escape }}",
  "created": "{{ node.created }}",
  "modified": "{{ node.modified }}"
}"#,
        )
        .map_err(|e| KbError::Parser(format!("Failed to register template: {e}")))?;

        // Register default Markdown template
        tera.add_raw_template(
            "markdown",
            r#"# {{ node.title }}

**Type:** {{ node.node_type }}
**Status:** {{ node.status }}
**Created:** {{ node.created | date(format="%Y-%m-%d") }}
**Modified:** {{ node.modified | date(format="%Y-%m-%d") }}

{% if node.tags %}
**Tags:** {% for tag in node.tags %}#{{ tag }}{% if not loop.last %} {% endif %}{% endfor %}
{% endif %}

## Content

{{ node.content }}"#,
        )
        .map_err(|e| KbError::Parser(format!("Failed to register template: {e}")))?;

        info!("Initialized Tera engine with default templates");

        Ok(Self { tera })
    }

    /// Render a node using the specified template
    ///
    /// # Arguments
    ///
    /// * `node` - The node to render
    /// * `template_name` - Name of the template to use (e.g., "logseq", "json",
    ///   "markdown")
    ///
    /// # Errors
    ///
    /// Returns error if template not found or rendering fails
    pub fn render(&self, node: &Node, template_name: &str) -> Result<String> {
        debug!(
            "Rendering node {node_id:?} with template: {template_name}",
            node_id = node.id
        );

        // Create context from node
        let mut context = Context::new();
        context.insert(
            "node",
            &json!({
                "id": node.id,
                "title": node.title,
                "content": node.content,
                "node_type": format!("{:?}", node.node_type),
                "status": format!("{:?}", node.status),
                "tags": node.tags,
                "created": node.created.to_rfc3339(),
                "modified": node.modified.to_rfc3339(),
                "relates_to": node.relates_to,
                "depends_on": node.depends_on,
                "implements": node.implements,
                "extends": node.extends,
            }),
        );

        // Render template
        let output = self
            .tera
            .render(template_name, &context)
            .map_err(|e| KbError::Parser(format!("Template rendering failed: {e}")))?;

        debug!("Rendered {bytes} bytes", bytes = output.len());
        Ok(output)
    }

    /// Render a node using the default Logseq template
    ///
    /// # Errors
    ///
    /// Returns error if rendering fails
    pub fn render_logseq(&self, node: &Node) -> Result<String> {
        self.render(node, "logseq")
    }

    /// Render a node using the default JSON template
    ///
    /// # Errors
    ///
    /// Returns error if rendering fails
    pub fn render_json(&self, node: &Node) -> Result<String> {
        self.render(node, "json")
    }

    /// Render a node using the default Markdown template
    ///
    /// # Errors
    ///
    /// Returns error if rendering fails
    pub fn render_markdown(&self, node: &Node) -> Result<String> {
        self.render(node, "markdown")
    }
}
