Documentation
use std::collections::BTreeMap;
use std::io::{self, Read};
use std::path::PathBuf;

use chrono::Local;
use minijinja::value::Value as TemplateValue;
use minijinja::Environment;
use rand::distr::Alphanumeric;
use rand::Rng;

use diwe::config::{Configuration, NoteTemplate, DEFAULT_KEY_DATE_FORMAT};
use liwe::locale::get_locale;
use liwe::model::{prepend_frontmatter, strip_doc_extension, Frontmatter, Key};

pub const BODY_VARIABLE: &str = "body";
pub const LEGACY_BODY_VARIABLE: &str = "content";
pub const TITLE_VARIABLE: &str = "title";
pub const RESERVED_VARIABLES: [&str; 4] = ["slug", "today", "now", "id"];

pub type Variables = BTreeMap<String, serde_yaml::Value>;

#[derive(Debug, Clone, Default, clap::ValueEnum)]
pub enum IfExists {
    #[default]
    Suffix,
    Override,
    Skip,
    Fail,
}

fn get_default_template() -> NoteTemplate {
    NoteTemplate {
        key_template: "{{slug}}".to_string(),
        document_template: "# {{title}}\n\n{{content}}".to_string(),
    }
}

pub struct DocumentCreator<'a> {
    config: &'a Configuration,
    library_path: PathBuf,
}

pub struct CreateOptions {
    pub template_name: Option<String>,
    pub variables: Variables,
    pub key: Option<String>,
    pub if_exists: IfExists,
    pub frontmatter: Option<Frontmatter>,
    pub empty_key_error: String,
}

pub struct ContentOptions {
    pub key: String,
    pub content: String,
    pub if_exists: IfExists,
}

pub struct PreparedDocument {
    pub key: Key,
    pub path: PathBuf,
    pub content: String,
}

pub struct CreatedDocument {
    pub path: PathBuf,
}

impl<'a> DocumentCreator<'a> {
    pub fn new(config: &'a Configuration, library_path: PathBuf) -> Self {
        Self {
            config,
            library_path,
        }
    }

    fn find_available_key(&self, base_key: &Key) -> Key {
        let mut candidate_key = base_key.clone();
        let mut counter = 1;

        while self
            .library_path
            .join(candidate_key.to_path(self.config.format))
            .exists()
        {
            let suffixed_name = format!("{}-{}", base_key, counter);
            candidate_key = Key::name(&suffixed_name);
            counter += 1;
        }

        candidate_key
    }

    fn locate(
        &self,
        relative_key: &str,
        empty_key_error: &str,
        if_exists: &IfExists,
    ) -> Result<Option<(Key, PathBuf)>, String> {
        let base_key = Key::name(relative_key);
        if base_key.as_str().is_empty() {
            return Err(empty_key_error.to_string());
        }

        let path_str = base_key.to_path(self.config.format);
        let filename_len = std::path::Path::new(&path_str)
            .file_name()
            .map(|name| name.len())
            .unwrap_or(path_str.len());
        if filename_len > 255 {
            return Err(format!(
                "Generated filename is too long ({} bytes, max 255). Use a shorter title.",
                filename_len
            ));
        }

        let file_exists = self.library_path.join(&path_str).exists();
        let final_key = match if_exists {
            IfExists::Skip if file_exists => return Ok(None),
            IfExists::Fail if file_exists => {
                return Err(format!("Document '{}' already exists", base_key))
            }
            IfExists::Suffix => self.find_available_key(&base_key),
            IfExists::Override | IfExists::Skip | IfExists::Fail => base_key,
        };

        let file_path = self
            .library_path
            .join(final_key.to_path(self.config.format));
        Ok(Some((final_key, file_path)))
    }

    pub fn prepare_content(
        &self,
        options: ContentOptions,
    ) -> Result<Option<PreparedDocument>, String> {
        if strip_doc_extension(&options.key) != options.key.as_str() {
            return Err(format!(
                "Key '{}' must not include a file extension",
                options.key
            ));
        }

        match self.locate(&options.key, "Provided key is empty.", &options.if_exists)? {
            Some((key, path)) => Ok(Some(PreparedDocument {
                key,
                path,
                content: options.content,
            })),
            None => Ok(None),
        }
    }

    pub fn prepare(&self, options: CreateOptions) -> Result<Option<PreparedDocument>, String> {
        let template_name = options
            .template_name
            .or_else(|| self.config.library.default_template.clone())
            .unwrap_or_else(|| "default".to_string());

        let fallback_template = get_default_template();
        let template = self
            .config
            .templates
            .get(&template_name)
            .or_else(|| {
                if template_name == "default" {
                    Some(&fallback_template)
                } else {
                    None
                }
            })
            .ok_or_else(|| format!("Template '{}' not found in configuration", template_name))?;

        let key_date_format = self
            .config
            .library
            .date_format
            .clone()
            .unwrap_or_else(|| DEFAULT_KEY_DATE_FORMAT.to_string());

        let format_options = self.config.format_options();

        let content_date_format = format_options
            .date_format()
            .unwrap_or("%b %d, %Y")
            .to_string();

        let key_time_format = self
            .config
            .library
            .time_format
            .clone()
            .unwrap_or_else(|| key_date_format.clone());

        let content_time_format = format_options
            .time_format()
            .map(|format| format.to_string())
            .unwrap_or_else(|| content_date_format.clone());

        let key_locale = get_locale(self.config.library.locale.as_deref());
        let content_locale = get_locale(format_options.locale());

        let now = Local::now();
        let key_today = now
            .format_localized(&key_date_format, key_locale)
            .to_string();
        let content_today = now
            .format_localized(&content_date_format, content_locale)
            .to_string();
        let key_now = now
            .format_localized(&key_time_format, key_locale)
            .to_string();
        let content_now = now
            .format_localized(&content_time_format, content_locale)
            .to_string();

        let slug = string_to_slug(&scalar_text(options.variables.get(TITLE_VARIABLE)));
        let id = generate_random_id();

        let relative_key = match &options.key {
            Some(key) => {
                if strip_doc_extension(key) != key.as_str() {
                    return Err(format!("Key '{}' must not include a file extension", key));
                }
                key.clone()
            }
            None => render_template(
                &template.key_template,
                &template_context(&options.variables, &slug, &key_today, &key_now, &id),
            )?,
        };

        let document_content = render_template(
            &template.document_template,
            &template_context(&options.variables, &slug, &content_today, &content_now, &id),
        )?;

        let document_content = prepend_frontmatter(options.frontmatter, &document_content)?;

        let empty_key_error = if options.key.is_some() {
            "Provided key is empty."
        } else {
            &options.empty_key_error
        };

        match self.locate(&relative_key, empty_key_error, &options.if_exists)? {
            Some((key, path)) => Ok(Some(PreparedDocument {
                key,
                path,
                content: document_content,
            })),
            None => Ok(None),
        }
    }
}

pub fn write_document(prepared: &PreparedDocument) -> Result<CreatedDocument, String> {
    if let Some(parent) = prepared.path.parent() {
        if !parent.exists() {
            std::fs::create_dir_all(parent)
                .map_err(|e| format!("Failed to create parent directories: {}", e))?;
        }
    }

    std::fs::write(&prepared.path, &prepared.content)
        .map_err(|e| format!("Failed to write file: {}", e))?;

    Ok(CreatedDocument {
        path: prepared
            .path
            .canonicalize()
            .unwrap_or_else(|_| prepared.path.clone()),
    })
}

pub fn read_stdin_if_available() -> String {
    use std::io::IsTerminal;

    if std::io::stdin().is_terminal() {
        return String::new();
    }

    read_stdin()
}

pub fn read_stdin() -> String {
    let mut buffer = String::new();
    io::stdin().read_to_string(&mut buffer).unwrap_or_default();
    buffer
}

fn template_context(
    variables: &Variables,
    slug: &str,
    today: &str,
    now: &str,
    id: &str,
) -> BTreeMap<String, TemplateValue> {
    let mut context: BTreeMap<String, TemplateValue> = variables
        .iter()
        .map(|(name, value)| (name.clone(), TemplateValue::from_serialize(value)))
        .collect();

    if let Some(body) = context.get(BODY_VARIABLE).cloned() {
        context.insert(LEGACY_BODY_VARIABLE.to_string(), body);
    }

    context.insert("slug".to_string(), TemplateValue::from(slug));
    context.insert("today".to_string(), TemplateValue::from(today));
    context.insert("now".to_string(), TemplateValue::from(now));
    context.insert("id".to_string(), TemplateValue::from(id));
    context
}

fn render_template(
    template_str: &str,
    context: &BTreeMap<String, TemplateValue>,
) -> Result<String, String> {
    Environment::new()
        .template_from_str(template_str)
        .map_err(|e| format!("Invalid template syntax: {}", e))?
        .render(context)
        .map_err(|e| format!("Template rendering failed: {}", e))
}

fn generate_random_id() -> String {
    rand::rng()
        .sample_iter(&Alphanumeric)
        .take(8)
        .map(char::from)
        .collect::<String>()
        .to_lowercase()
}

fn scalar_text(value: Option<&serde_yaml::Value>) -> String {
    match value {
        Some(serde_yaml::Value::String(text)) => text.clone(),
        Some(serde_yaml::Value::Number(number)) => number.to_string(),
        Some(serde_yaml::Value::Bool(flag)) => flag.to_string(),
        _ => String::new(),
    }
}

fn string_to_slug(s: &str) -> String {
    s.to_lowercase()
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { '-' })
        .collect::<String>()
        .split('-')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join("-")
}