use itertools::Itertools;
use regex::Regex;
use std::sync::LazyLock;
use tera::Tera;

/// The `blob/<ref>/` part of a GitHub or GitLab source URL, which
/// `source_code_link` rewrites per command.
static BLOB_PATH: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/").unwrap()
});

pub(crate) static TERA: LazyLock<Tera> = LazyLock::new(|| {
    let mut tera = Tera::default();

    tera.register_filter(
        "repeat",
        move |value: &tera::Value,
              args: tera::Kwargs,
              _: &tera::State|
              -> tera::TeraResult<String> {
            let value = value.as_str().unwrap();
            let count = args.must_get::<u64>("count")?;
            Ok(value.repeat(count as usize))
        },
    );
    tera.register_filter(
        "default",
        |value: &tera::Value,
         kwargs: tera::Kwargs,
         _: &tera::State|
         -> tera::TeraResult<tera::Value> {
            let default_val = kwargs.must_get::<tera::Value>("value")?;
            let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
            if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
                Ok(default_val)
            } else {
                Ok(value.clone())
            }
        },
    );
    tera.register_filter(
        "filter",
        |value: &[tera::Value],
         args: tera::Kwargs,
         _: &tera::State|
         -> tera::TeraResult<tera::Value> {
            let attr = args.must_get::<String>("attribute")?;
            let expected = args
                .get::<tera::Value>("value")?
                .unwrap_or_else(tera::Value::none);
            let values = value
                .iter()
                .filter(|item| {
                    let Some(actual) = item.get_from_path(&attr) else {
                        return false;
                    };
                    if expected.is_none() {
                        !actual.is_none()
                    } else {
                        actual == &expected
                    }
                })
                .cloned()
                .collect_vec();
            Ok(tera::Value::from(values))
        },
    );
    tera.register_filter(
        "slice",
        |value: &[tera::Value],
         args: tera::Kwargs,
         _: &tera::State|
         -> tera::TeraResult<tera::Value> {
            fn index(idx: i64, len: usize) -> usize {
                if idx < 0 {
                    len.saturating_sub(idx.unsigned_abs() as usize)
                } else {
                    idx as usize
                }
            }

            let start = args
                .get::<i64>("start")?
                .map(|idx| index(idx, value.len()))
                .unwrap_or_default();
            let end = args
                .get::<i64>("end")?
                .map(|idx| index(idx, value.len()))
                .unwrap_or(value.len())
                .min(value.len());
            if start >= end {
                return Ok(tera::Value::from(Vec::<tera::Value>::new()));
            }
            Ok(tera::Value::from(value[start..end].to_vec()))
        },
    );
    tera.register_filter(
        "concat",
        |value: &[tera::Value],
         args: tera::Kwargs,
         _: &tera::State|
         -> tera::TeraResult<tera::Value> {
            let extra = args.must_get::<tera::Value>("with")?;
            let mut values = value.to_vec();
            if let Some(extra_values) = extra.as_array() {
                values.extend(extra_values.iter().cloned());
            } else {
                values.push(extra);
            }
            Ok(tera::Value::from(values))
        },
    );
    tera.register_filter(
        "escape_md",
        |value: &tera::Value, _: tera::Kwargs, _: &tera::State| -> tera::TeraResult<tera::Value> {
            Ok(value.clone())
        },
    );
    tera.register_function(
        "source_code_link",
        move |args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
            let spec = args.must_get::<&tera::Value>("spec")?;
            let cmd = args.must_get::<&tera::Value>("cmd")?;
            let full_cmd = cmd.get_from_path("full_cmd").and_then(|v| v.as_array());
            let source_code_link_template = spec
                .get_from_path("source_code_link_template")
                .and_then(|v| v.as_str());
            if let (Some(full_cmd), Some(source_code_link_template)) =
                (full_cmd, source_code_link_template)
            {
                if full_cmd.is_empty() {
                    return Ok(String::new());
                }
                let mut ctx = tera::Context::new();
                let path = full_cmd.iter().map(|v| v.as_str().unwrap()).join("/");
                ctx.insert_value("spec", spec.clone());
                ctx.insert_value("cmd", cmd.clone());
                ctx.insert("path", &path);
                let href = TERA
                    .clone()
                    .render_str(source_code_link_template, &ctx, false)?;
                let friendly = BLOB_PATH.replace_all(&href, "").to_string();
                let link = if BLOB_PATH.is_match(&href) {
                    format!("[`{friendly}`]({href})")
                } else {
                    format!("[{friendly}]({href})")
                };
                Ok(link)
            } else {
                Ok(String::new())
            }
        },
    );

    #[rustfmt::skip]
    tera.add_raw_templates([
        ("arg_template.md.tera", include_str!("templates/arg_template.md.tera")),
        ("cmd_template.md.tera", include_str!("templates/cmd_template.md.tera")),
        (
            "config_template.md.tera",
            include_str!("templates/config_template.md.tera"),
        ),
        ("flag_template.md.tera", include_str!("templates/flag_template.md.tera")),
        ("spec_template.md.tera", include_str!("templates/spec_template.md.tera")),
        ("index_template.md.tera", include_str!("templates/index_template.md.tera")),
    ]).unwrap();

    tera
});
