pub mod route;
pub mod url;

use crate::errors::*;
use ::url::Url;
use handlebars::{
    Context, Handlebars, Helper, HelperResult, Output, PathAndJson, RenderContext,
    RenderErrorReason,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ArtifactMetadata {
    pub url: Url,
    pub filename: Option<String>,
    pub version: Option<String>,
    pub sha256: Option<String>,
}

impl ArtifactMetadata {
    pub fn from_json(bytes: &[u8]) -> Result<Self> {
        let artifact = serde_json::from_slice(bytes)?;
        Ok(artifact)
    }

    pub fn to_json(&self) -> Result<Vec<u8>> {
        let buf = serde_json::to_vec(self)?;
        Ok(buf)
    }

    pub fn to_values(&self) -> HashMap<&'static str, String> {
        let mut values = HashMap::new();
        values.insert("url", self.url.to_string());

        if let Some(filename) = &self.filename {
            values.insert("filename", filename.to_string());
        }

        if let Some(version) = &self.version {
            values.insert("version", version.to_string());
        }

        if let Some(sha256) = &self.sha256 {
            values.insert("sha256", sha256.to_string());
        }

        values
    }
}

pub fn create_engine(template: &str) -> Result<Handlebars<'_>> {
    let mut handlebars = Handlebars::new();
    handlebars.set_strict_mode(true);
    handlebars
        .register_template_string("t", template)
        .with_context(|| anyhow!("Failed to parse template: {:?}", template))?;
    handlebars.register_helper("slice-until", Box::new(slice_until));
    handlebars.register_helper("slice-after", Box::new(slice_after));

    Ok(handlebars)
}

fn param_to_str<'a>(
    param: Option<&'a PathAndJson>,
) -> std::result::Result<&'a String, RenderErrorReason> {
    if let Some(Value::String(value)) = param.map(|p| p.value()) {
        Ok(value)
    } else {
        Err(RenderErrorReason::Other(
            "Argument is missing or not a string".to_string(),
        ))
    }
}

fn param_to_u64(param: Option<&PathAndJson>) -> std::result::Result<u64, RenderErrorReason> {
    if let Some(Value::Number(num)) = param.map(|p| p.value()) {
        let value = num.as_u64().ok_or_else(|| {
            RenderErrorReason::Other("Argument could not be converted to u64".to_string())
        })?;
        Ok(value)
    } else {
        Err(RenderErrorReason::Other(
            "Argument is missing or not a number".to_string(),
        ))
    }
}

fn param_to_usize(param: Option<&PathAndJson>) -> std::result::Result<usize, RenderErrorReason> {
    let value = param_to_u64(param)?
        .try_into()
        .context("Argument could not be converted to usize")
        .map_err(|_err| {
            RenderErrorReason::Other("Argument could not be converted to usize".to_string())
        })?;
    Ok(value)
}

pub fn slice_until(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _rc: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let value = param_to_str(h.param(0))?;
    let idx = param_to_usize(h.param(1))?;

    out.write(&value[..idx])?;

    Ok(())
}

pub fn slice_after(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _rc: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let value = param_to_str(h.param(0))?;
    let idx = param_to_usize(h.param(1))?;

    out.write(&value[idx..])?;

    Ok(())
}
