use anyhow::{Context, Result, bail};
use serde_json::{Map, Value};

use super::schema::TemplateFile;

/// Parses an HCL template file into the canonical schema struct.
pub fn parse_template_hcl(content: &str) -> Result<TemplateFile> {
    let root_value: Value = hcl::from_str(content).context("invalid HCL")?;

    let mut root = match root_value {
        Value::Object(object) => object,
        _ => bail!("template root must be an object"),
    };

    let commands = normalize_commands(&mut root)?;
    root.insert("commands".to_string(), Value::Object(commands));

    serde_json::from_value(Value::Object(root)).context("template does not match schema")
}

fn normalize_commands(root: &mut Map<String, Value>) -> Result<Map<String, Value>> {
    let commands_attr = root.remove("commands");
    let command_blocks = root.remove("command");

    if commands_attr.is_some() && command_blocks.is_some() {
        bail!("template must define either `commands` or `command`, not both");
    }

    match (commands_attr, command_blocks) {
        (Some(value), None) => normalize_command_map(value, "commands"),
        (None, Some(value)) => normalize_command_map(value, "command"),
        (None, None) => Ok(Map::new()),
        (Some(_), Some(_)) => unreachable!(),
    }
}

fn normalize_command_map(value: Value, field: &str) -> Result<Map<String, Value>> {
    let command_map = expect_object(value, field)?;
    let mut normalized = Map::new();

    for (command_name, command_value) in command_map {
        let command_path = format!("{field}.{command_name}");
        let mut command = expect_object(command_value, &command_path)?;
        normalize_params(&mut command, &command_path)?;
        normalized.insert(command_name, Value::Object(command));
    }

    Ok(normalized)
}

fn normalize_params(command: &mut Map<String, Value>, command_path: &str) -> Result<()> {
    let params_attr = command.remove("params");
    let param_blocks = command.remove("param");

    if params_attr.is_some() && param_blocks.is_some() {
        bail!("{command_path} must define either `params` or `param`, not both");
    }

    if let Some(value) = params_attr {
        command.insert("params".to_string(), value);
        return Ok(());
    }

    let Some(param_blocks) = param_blocks else {
        return Ok(());
    };

    let params_path = format!("{command_path}.param");
    let param_map = expect_object(param_blocks, &params_path)?;

    let mut normalized = Vec::with_capacity(param_map.len());
    for (param_name, param_value) in param_map {
        let param_path = format!("{params_path}.{param_name}");
        let mut param = expect_object(param_value, &param_path)?;

        if let Some(name_value) = param.get("name") {
            match name_value {
                Value::String(existing_name) if existing_name == &param_name => {}
                Value::String(existing_name) => {
                    bail!(
                        "{param_path}.name (`{existing_name}`) must match parameter label `{param_name}`"
                    )
                }
                _ => bail!("{param_path}.name must be a string when provided"),
            }
        } else {
            param.insert("name".to_string(), Value::String(param_name));
        }

        normalized.push(Value::Object(param));
    }

    command.insert("params".to_string(), Value::Array(normalized));
    Ok(())
}

fn expect_object(value: Value, field: &str) -> Result<Map<String, Value>> {
    match value {
        Value::Object(object) => Ok(object),
        _ => bail!("{field} must be an object"),
    }
}

#[cfg(test)]
mod tests {
    use super::parse_template_hcl;

    #[test]
    fn parses_block_style_commands_and_params() {
        let template = r#"
version = 1
provider = "demo"
categories = ["sample"]

command "ping" {
  title = "Ping"
  summary = "Execute a simple ping request"
  description = "Sends a basic ping request and returns the raw response body."

  annotations {
    mode = "read"
    secrets = []
  }

  param "value" {
    type = "string"
    required = false
    default = "hello"
  }

  operation {
    protocol = "http"
    method = "GET"
    url = "https://api.example.com/ping"
    query = {
      q = "{{ args.value }}"
    }
  }

  result {
    decode = "text"
    output = "{{ result }}"
  }
}
"#;

        let parsed = parse_template_hcl(template).unwrap();

        assert_eq!(parsed.provider, "demo");
        let ping = parsed.commands.get("ping").unwrap();
        assert_eq!(ping.params.len(), 1);
        assert_eq!(ping.params[0].name, "value");
    }

    #[test]
    fn rejects_commands_and_command_together() {
        let template = r#"
version = 1
provider = "demo"
commands = {}
command "ping" {}
"#;

        let err = parse_template_hcl(template).unwrap_err();
        assert!(err.to_string().contains("either `commands` or `command`"));
    }

    #[test]
    fn rejects_params_and_param_together() {
        let template = r#"
version = 1
provider = "demo"

command "ping" {
  title = "Ping"
  summary = "Ping"
  description = "Ping"

  annotations {
    mode = "read"
    secrets = []
  }

  params = []

  param "value" {
    type = "string"
  }

  operation {
    protocol = "http"
    method = "GET"
    url = "https://api.example.com/ping"
  }

  result {
    output = "ok"
  }
}
"#;

        let err = parse_template_hcl(template).unwrap_err();
        assert!(err.to_string().contains("either `params` or `param`"));
    }
}
