// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// A skill document: the frontmatter the specification defines, the Markdown
/// an agent reads when the skill is activated, and the directory it came from.
///
/// The specification loads a skill in three tiers, and the fields follow them:
/// [`Skill::summary`] is the tier a client reads for every skill it knows
/// about, `body` is the tier it reads once the skill is chosen, and the files
/// the body points at are read one at a time through [`Skill::resolve`] when the
/// task reaches them.
pub(all) struct Skill {
  /// The `name` field, trimmed. Empty when the frontmatter has none, or when
  /// it is not text.
  name : String
  /// The `description` field, trimmed. Empty when the frontmatter has none.
  description : String
  /// The Markdown after the frontmatter, trimmed at both ends.
  body : String
  /// The whole frontmatter, so a client reads fields this library does not
  /// name — `license`, `metadata`, `allowed-tools` — and so [`validate`] can
  /// judge the text as written rather than as trimmed.
  meta : Map[String, Json]
  /// The skill directory as given to [`parse`], or as returned by a
  /// [`Source`]. Empty means there is no directory to check the name against.
  root : String
}

///|
/// The metadata tier: what a client puts in front of a model before the model
/// has chosen anything, which the specification budgets at about a hundred
/// tokens per skill. One line, so a list of them stays a list.
pub fn Skill::summary(self : Skill) -> String {
  if self.description == "" {
    self.name
  } else if self.name == "" {
    self.description
  } else {
    "\{self.name}: \{self.description}"
  }
}

///|
/// The path of the document, which is what a client hands back to the model so
/// it can open the skill if it wants the body.
pub fn Skill::location(self : Skill) -> String {
  if self.root == "" {
    "SKILL.md"
  } else {
    join_path(self.root, "SKILL.md")
  }
}

///|
/// The `license` field, or `None` when it is absent or not text.
pub fn Skill::license(self : Skill) -> String? {
  self.text_of("license")
}

///|
/// The `compatibility` field, or `None` when it is absent or not text.
pub fn Skill::compatibility(self : Skill) -> String? {
  self.text_of("compatibility")
}

///|
/// The experimental `allowed-tools` field, or `None`.
pub fn Skill::allowed_tools(self : Skill) -> String? {
  self.text_of("allowed-tools")
}

///|
/// The `metadata` mapping as text, or `None` when it is absent. The
/// specification defines it as string keys to string values; a value that is
/// not text is skipped rather than turned into JSON spelling.
pub fn Skill::metadata(self : Skill) -> Map[String, String]? {
  guard self.meta.get("metadata") is Some(Object(fields)) else { return None }
  let out = Map([])
  for key, value in fields {
    if value is String(text) {
      out[key] = text
    }
  }
  Some(out)
}

///|
fn Skill::text_of(self : Skill, key : String) -> String? {
  match self.meta.get(key) {
    Some(String(text)) => Some(text)
    _ => None
  }
}

///|
/// The path of a file the body refers to, resolved against the skill root, or
/// `None` when the reference is not a file inside this skill.
///
/// This is how the third tier is read: the body says `references/REFERENCE.md`,
/// the model asks for that file, and the client fetches it from the same
/// [`Source`] the skill came from. A reference that climbs out of the directory
/// — `../other-skill/SKILL.md`, an absolute path, a URL — is `None` rather than
/// a path: a skill may point at its own files and nowhere else.
pub fn Skill::resolve(self : Skill, path : StringView) -> String? {
  let raw = path.trim()
  if raw == "" || raw.has_prefix("/") || raw.has_prefix("\\") {
    return None
  }
  let segments = []
  for segment in raw.replace_all(old="\\", new="/").split("/") {
    if segment == "" || segment == "." {
      continue
    }
    if segment == ".." || segment.contains(":") {
      return None
    }
    segments.push(segment)
  }
  if segments.length() == 0 {
    return None
  }
  Some(join_path(self.root, segments.join("/")))
}

///|
/// `a` and `b` with exactly one `/` between them, whichever slashes either
/// already carries.
///
/// Paths are joined here rather than with the platform's separator: a skill
/// root is a key in a [`Source`], and a key that reads the same on every system
/// is the one a report can be compared against.
pub fn join_path(a : StringView, b : StringView) -> String {
  let head = a.to_owned()
  let mut end = head.length()
  while end > 0 && (head[end - 1] == '/' || head[end - 1] == '\\') {
    end = end - 1
  }
  if end == 0 {
    return b.to_owned()
  }
  "\{head[:end]}/\{b}"
}

///|
/// The last segment of a path, which is the directory name the specification
/// compares a skill's `name` against.
pub fn base_name(path : StringView) -> String {
  let text = path.to_owned()
  let mut end = text.length()
  while end > 0 && (text[end - 1] == '/' || text[end - 1] == '\\') {
    end = end - 1
  }
  let mut start = end
  while start > 0 && text[start - 1] != '/' && text[start - 1] != '\\' {
    start = start - 1
  }
  text[start:end].to_owned()
}