///|
/// Parse a markdown skill file frontmatter and return `(metadata, body)`.
pub fn parse_skill_frontmatter(
  content : String,
) -> (Map[String, String]?, String) {
  if not(content.has_prefix("---\n")) {
    return (None, content)
  }
  match content.find("\n---\n") {
    Some(end_idx) => {
      let header = content.unsafe_substring(start=4, end=end_idx)
      let body_start = end_idx + 5
      let body = if body_start <= content.length() {
        content.unsafe_substring(start=body_start, end=content.length())
      } else {
        ""
      }
      (Some(parse_skill_frontmatter_map(header)), body)
    }
    None => (None, content)
  }
}

///|
/// Remove frontmatter and return trimmed skill markdown body.
pub fn strip_skill_frontmatter(content : String) -> String {
  parse_skill_frontmatter(content).1.trim().to_string()
}

///|
/// Resolve human-readable skill description from metadata.
pub fn skill_metadata_description(
  metadata : Map[String, String]?,
  fallback : String,
) -> String {
  match metadata {
    Some(meta) =>
      match meta.get("description") {
        Some(desc) => desc
        None => fallback
      }
    None => fallback
  }
}

///|
/// Evaluate whether a skill is runnable in the current environment.
pub fn skill_metadata_is_available(
  metadata : Map[String, String]?,
  bins? : Map[String, Bool] = {},
  env? : Map[String, String] = {},
  scope_key? : String = "nanobot",
) -> Bool {
  match metadata {
    Some(meta) =>
      skill_check_requirements(
        parse_skill_namespace_metadata(meta.get("metadata"), scope_key),
        bins,
        env,
      )
    None => true
  }
}

///|
/// Collect missing requirement text (e.g. `CLI: tmux, ENV: OPENAI_API_KEY`).
pub fn skill_metadata_missing_requirements(
  metadata : Map[String, String]?,
  bins? : Map[String, Bool] = {},
  env? : Map[String, String] = {},
  scope_key? : String = "nanobot",
) -> String {
  match metadata {
    Some(meta) =>
      skill_collect_missing_requirements(
        parse_skill_namespace_metadata(meta.get("metadata"), scope_key),
        bins,
        env,
      )
    None => ""
  }
}

///|
/// `always` may be specified either at root frontmatter or namespaced metadata.
pub fn skill_metadata_is_always(
  metadata : Map[String, String]?,
  scope_key? : String = "nanobot",
) -> Bool {
  match metadata {
    Some(meta) => {
      let always_flag = skill_is_string_true(meta.get("always"))
      let scoped_meta = parse_skill_namespace_metadata(
        meta.get("metadata"),
        scope_key,
      )
      let always_scoped = match
        skill_json_get_bool_field(scoped_meta, "always") {
        Some(v) => v
        None => false
      }
      always_flag || always_scoped
    }
    None => false
  }
}

///|
fn parse_skill_frontmatter_map(header : String) -> Map[String, String] {
  let out : Map[String, String] = {}
  for line_view in header.split("\n") {
    let line = line_view.to_string().trim().to_string()
    if line.is_empty() {
      continue
    }
    match line.find(":") {
      Some(idx) => {
        let key = line.unsafe_substring(start=0, end=idx).trim().to_string()
        let value = line
          .unsafe_substring(start=idx + 1, end=line.length())
          .trim()
          .to_string()
        out[key] = skill_unquote(value)
      }
      None => ()
    }
  }
  out
}

///|
fn parse_skill_namespace_metadata(raw : String?, scope_key : String) -> Json {
  let source = match raw {
    Some(s) => if s.is_empty() { return Json::object({}) } else { s }
    None => return Json::object({})
  }
  let parsed = @json.parse(source) catch { _ => Json::null() }
  match parsed {
    Object(map) =>
      match map.get(scope_key) {
        Some(Object(scoped)) => Json::object(scoped)
        _ => parsed
      }
    _ => Json::object({})
  }
}

///|
fn skill_check_requirements(
  scoped_meta : Json,
  bins : Map[String, Bool],
  env : Map[String, String],
) -> Bool {
  let requires = skill_json_get_object_field(scoped_meta, "requires")
  match requires {
    Some(requires_obj) => {
      for bin in skill_json_get_string_array(requires_obj, "bins") {
        if not(bins.contains(bin)) {
          return false
        }
      }
      for env_key in skill_json_get_string_array(requires_obj, "env") {
        match env.get(env_key) {
          Some(v) => if v.is_empty() { return false }
          None => return false
        }
      }
      true
    }
    None => true
  }
}

///|
fn skill_collect_missing_requirements(
  scoped_meta : Json,
  bins : Map[String, Bool],
  env : Map[String, String],
) -> String {
  let missing : Array[String] = []
  match skill_json_get_object_field(scoped_meta, "requires") {
    Some(requires_obj) => {
      for bin in skill_json_get_string_array(requires_obj, "bins") {
        if not(bins.contains(bin)) {
          missing.push("CLI: " + bin)
        }
      }
      for env_key in skill_json_get_string_array(requires_obj, "env") {
        match env.get(env_key) {
          Some(v) => if v.is_empty() { missing.push("ENV: " + env_key) }
          None => missing.push("ENV: " + env_key)
        }
      }
    }
    None => ()
  }
  missing.join(", ")
}

///|
fn skill_json_get_object_field(obj : Json, key : String) -> Map[String, Json]? {
  match obj {
    Object(map) =>
      match map.get(key) {
        Some(Object(sub)) => Some(sub)
        _ => None
      }
    _ => None
  }
}

///|
fn skill_json_get_string_array(
  obj : Map[String, Json],
  key : String,
) -> Array[String] {
  match obj.get(key) {
    Some(Array(items)) => {
      let out : Array[String] = []
      for item in items {
        match item {
          String(s) => out.push(s)
          _ => ()
        }
      }
      out
    }
    _ => []
  }
}

///|
fn skill_json_get_bool_field(obj : Json, key : String) -> Bool? {
  match obj {
    Object(map) =>
      match map.get(key) {
        Some(True) => Some(true)
        Some(False) => Some(false)
        Some(String(s)) => Some(skill_is_string_true(Some(s)))
        _ => None
      }
    _ => None
  }
}

///|
fn skill_is_string_true(v : String?) -> Bool {
  match v {
    Some(raw) => {
      let s = raw.trim().to_string().to_lower()
      s == "true" || s == "1" || s == "yes"
    }
    None => false
  }
}

///|
fn skill_unquote(s : String) -> String {
  if s.length() >= 2 {
    if (s.has_prefix("\"") && s.has_suffix("\"")) ||
      (s.has_prefix("'") && s.has_suffix("'")) {
      return s.unsafe_substring(start=1, end=s.length() - 1)
    }
  }
  s
}