// Filesystem helpers for the skills store. All cross-target via
// moonbitlang/x/fs; the recursive walk is built on @fs.read_dir +
// @fs.is_dir so no backend-specific FFI remains here.

///| Exclude list matches the pre-existing JS walker (.git etc).
fn sks_excluded(name : String) -> Bool {
  name == ".git" || name == ".github" || name == ".hub"
}

///| Skills fixture dir used by inline tests. Relative to cwd, matching
///| the previous JS resolution (`path.join(process.cwd(), ...)`).
fn ffi_skills_fixtures_path() -> String {
  "./proto/fixtures/skills"
}

///| Recursively collect all `SKILL.md` files under `dir`, skipping
///| excluded directory names. Missing roots return an empty list
///| (no error) so callers can treat "no skills" uniformly.
fn ffi_list_skill_files(dir : String) -> Array[String] {
  let results : Array[String] = []
  sks_walk(dir, results)
  results
}

///| Walker helper — split out to keep ffi_list_skill_files's signature
///| stable and to allow recursion without polluting the top-level API.
fn sks_walk(current : String, out : Array[String]) -> Unit {
  if !@fs.path_exists(current) {
    return
  }
  let entries = try {
    @fs.read_dir(current)
  } catch {
    _ => return
  }
  for name in entries {
    if sks_excluded(name) {
      continue
    }
    let full = current + "/" + name
    let is_d = (try? @fs.is_dir(full)).unwrap_or(false)
    if is_d {
      sks_walk(full, out)
    } else if name == "SKILL.md" {
      out.push(full)
    }
  }
}

///| Read a SKILL.md. Missing / unreadable → "".
fn ffi_read_skill_file(path : String) -> String {
  (try? @fs.read_file_to_string(path)).unwrap_or("")
}

///| Derive a category from a skill file path. Mirrors the previous
///| relative-path heuristic: `skills///SKILL.md` → cat,
///| `skills//SKILL.md` → "".
fn ffi_infer_category(skill_path : String, skills_dir : String) -> String {
  // Strip the skills_dir prefix (plus any trailing slash) to get
  // the skill-relative path, then split by "/".
  let prefix = if skills_dir.has_suffix("/") {
    skills_dir
  } else {
    skills_dir + "/"
  }
  let rel = if skill_path.has_prefix(prefix) {
    skill_path[prefix.length():].to_string()
  } else {
    skill_path
  }
  let parts = rel.split("/").filter(fn(s) { !s.is_empty() }).to_array()
  if parts.length() >= 3 {
    parts[0].to_string()
  } else {
    ""
  }
}

///|
pub struct SkillEntry {
  name : String
  skill_name : String
  description : String
  category : String   // "" = no category = treated as "general" in prompt
  content : String
  frontmatter : SkillFrontmatter
}

///|
pub enum ViewResult {
  ViewOk(String, String)   // name, content
  ViewErr(String)          // error message
}

///|
pub struct SkillsStore {
  skills : Array[SkillEntry]
}

let max_name_len : Int = 64
let max_desc_len : Int = 1024

///|
fn load_from_dir(dir : String, seen_names : Array[String]) -> Array[SkillEntry] {
  let files = ffi_list_skill_files(dir)
  let entries : Array[SkillEntry] = []
  for file_path in files {
    let content = ffi_read_skill_file(file_path)
    if content.length() == 0 { continue }
    let fm = parse_frontmatter(content)
    // Extract parent directory name from path (second to last path component)
    let parts : Array[String] = file_path.split("/").map(fn(sv) { sv.to_string() }).to_array()
    let parts_len = parts.length()
    let skill_dir = if parts_len >= 2 {
      parts[parts_len - 2]
    } else {
      "unknown"
    }
    let raw_name = match fm.name {
      Some(n) => n
      None => skill_dir
    }
    let name = if raw_name.length() > max_name_len {
      raw_name[0:max_name_len].to_string()
    } else {
      raw_name
    }
    if seen_names.contains(name) { continue }
    let raw_desc = match fm.description {
      Some(d) => d
      None => ""
    }
    let description = if raw_desc.length() > max_desc_len {
      raw_desc[0:max_desc_len - 3].to_string() + "..."
    } else {
      raw_desc
    }
    seen_names.push(name)
    let category = ffi_infer_category(file_path, dir)
    entries.push({
      name,
      skill_name: skill_dir,
      description,
      category,
      content,
      frontmatter: fm,
    })
  }
  entries
}

///|
pub fn skills_store_load(skills_dir : String, external_dirs : Array[String]) -> SkillsStore {
  let seen : Array[String] = []
  let all : Array[SkillEntry] = []
  for entry in load_from_dir(skills_dir, seen) {
    all.push(entry)
  }
  for ext_dir in external_dirs {
    for entry in load_from_dir(ext_dir, seen) {
      all.push(entry)
    }
  }
  { skills: all }
}

///| Platform normalization: "mac" | "macos" → "darwin", "linux" → "linux", "windows" → "win32"
fn normalize_platform(hint : String) -> String {
  let lower = hint.to_lower()
  if lower == "mac" || lower == "macos" { "darwin" }
  else if lower == "linux" { "linux" }
  else if lower == "windows" { "win32" }
  else { lower }
}

///|
fn skill_platform_ok(fm : SkillFrontmatter, platform_hint : Option[String]) -> Bool {
  if fm.platforms.length() == 0 { return true }
  match platform_hint {
    None => true
    Some(hint) => {
      let hint_norm = normalize_platform(hint)
      let mut ok = false
      for p in fm.platforms {
        let p_norm = normalize_platform(p)
        if hint_norm.has_prefix(p_norm) || p_norm.has_prefix(hint_norm) {
          ok = true
        }
      }
      ok
    }
  }
}

///|
fn skill_should_show(
  fm : SkillFrontmatter,
  available_tools : Option[Array[String]],
  available_toolsets : Option[Array[String]],
  platform_hint : Option[String]
) -> Bool {
  if !skill_platform_ok(fm, platform_hint) { return false }
  // No tool/toolset filtering when both are absent (backward compat with proto)
  if available_tools is None && available_toolsets is None { return true }
  let at : Array[String] = match available_tools {
    None => []
    Some(arr) => arr
  }
  let ats : Array[String] = match available_toolsets {
    None => []
    Some(arr) => arr
  }
  // fallback_for: hide when primary tool IS available
  for t in fm.fallback_for_tools {
    if at.contains(t) { return false }
  }
  for ts in fm.fallback_for_toolsets {
    if ats.contains(ts) { return false }
  }
  // requires: hide when required tool is NOT available
  for t in fm.requires_tools {
    if !at.contains(t) { return false }
  }
  for ts in fm.requires_toolsets {
    if !ats.contains(ts) { return false }
  }
  true
}

///|
pub fn skills_store_list(
  store : SkillsStore,
  available_tools : Option[Array[String]],
  platform_hint : Option[String],
  available_toolsets~ : Option[Array[String]] = None,
  disabled~ : Array[String] = []
) -> Array[SkillEntry] {
  store.skills.filter(fn(s) {
    if disabled.length() > 0 && (disabled.contains(s.name) || disabled.contains(s.skill_name)) {
      return false
    }
    skill_should_show(s.frontmatter, available_tools, available_toolsets, platform_hint)
  })
}

///| Find a SkillEntry by name field
fn find_skill_by_name(skills : Array[SkillEntry], name : String) -> Option[SkillEntry] {
  let mut found : Option[SkillEntry] = None
  for s in skills {
    if found is None && s.name == name {
      found = Some(s)
    }
  }
  found
}

///| Find a SkillEntry by skill_name field
fn find_skill_by_skill_name(skills : Array[SkillEntry], skill_name : String) -> Option[SkillEntry] {
  let mut found : Option[SkillEntry] = None
  for s in skills {
    if found is None && s.skill_name == skill_name {
      found = Some(s)
    }
  }
  found
}

///|
pub fn skills_store_view(store : SkillsStore, name : String) -> ViewResult {
  match find_skill_by_name(store.skills, name) {
    Some(s) => ViewOk(s.name, s.content)
    None =>
      match find_skill_by_skill_name(store.skills, name) {
        Some(s) => ViewOk(s.name, s.content)
        None => ViewErr("Skill '" + name + "' not found.")
      }
  }
}

///| Find index of category in categories array; returns -1 if not found
fn find_category_index(categories : Array[String], cat : String) -> Int {
  let mut idx = -1
  for i, c in categories {
    if idx == -1 && c == cat {
      idx = i
    }
  }
  idx
}

///|
pub fn skills_store_build_prompt(
  store : SkillsStore,
  available_tools : Option[Array[String]],
  platform_hint : Option[String],
  disabled~ : Array[String] = []
) -> String {
  let visible = skills_store_list(
    store, available_tools, platform_hint, disabled=disabled,
  )
  if visible.length() == 0 { return "" }

  // Group by category
  // Use Array of (category, Array of (name, desc)) pairs — no Map available
  let categories : Array[String] = []
  let by_cat : Array[(String, Array[(String, String)])] = []

  for skill in visible {
    let cat = if skill.category.length() == 0 { "general" } else { skill.category }
    let idx = find_category_index(categories, cat)
    if idx >= 0 {
      by_cat[idx].1.push((skill.name, skill.description))
    } else {
      categories.push(cat)
      by_cat.push((cat, [(skill.name, skill.description)]))
    }
  }

  // Sort categories alphabetically
  // Simple insertion sort (no stdlib sort needed for small N)
  let n = categories.length()
  for i in 1..= 0 && categories[j] > key_cat {
      categories[j + 1] = categories[j]
      by_cat[j + 1] = by_cat[j]
      j = j - 1
    }
    categories[j + 1] = key_cat
    by_cat[j + 1] = key_entry
  }

  let lines : Array[String] = []
  for i in 0..= 0 && sorted_skills[sj].0 > key.0 {
        sorted_skills[sj + 1] = sorted_skills[sj]
        sj = sj - 1
      }
      sorted_skills[sj + 1] = key
    }
    for skill_pair in sorted_skills {
      let sname = skill_pair.0
      let sdesc = skill_pair.1
      if sdesc.length() > 0 {
        lines.push("    - " + sname + ": " + sdesc)
      } else {
        lines.push("    - " + sname)
      }
    }
  }

  "## Skills (mandatory)\n" +
  "Before replying, scan the skills below. If a skill matches or is even partially relevant " +
  "to your task, you MUST load it with skill_view(name) and follow its instructions.\n" +
  "\n" +
  "\n" +
  lines.join("\n") +
  "\n\n" +
  "\n" +
  "Only proceed without loading a skill if genuinely none are relevant to the task."
}

///| Tests

test "skills_store: load from fixture dir" {
  let dir = ffi_skills_fixtures_path()
  let store = skills_store_load(dir, [])
  let skills = skills_store_list(store, None, None)
  assert_eq(skills.length(), 3)
}

test "skills_store: filter hides fallback skill when tool available" {
  let dir = ffi_skills_fixtures_path()
  let store = skills_store_load(dir, [])
  let with_tool = skills_store_list(store, Some(["docker_build_tool"]), None)
  let hidden = find_skill_by_name(with_tool, "docker-build")
  match hidden {
    Some(_) => fail("expected docker-build to be hidden")
    None => ()
  }
}

test "skills_store: view returns content for known skill" {
  let dir = ffi_skills_fixtures_path()
  let store = skills_store_load(dir, [])
  let result = skills_store_view(store, "tdd-workflow")
  match result {
    ViewOk(name, content) => {
      assert_eq(name, "tdd-workflow")
      assert_eq(content.contains("Red"), true)
    }
    ViewErr(_) => fail("expected ViewOk")
  }
}

test "skills_store: build_prompt contains skill names" {
  let dir = ffi_skills_fixtures_path()
  let store = skills_store_load(dir, [])
  let prompt = skills_store_build_prompt(store, None, None)
  assert_eq(prompt.contains("tdd-workflow"), true)
  assert_eq(prompt.contains("## Skills (mandatory)"), true)
}

test "skills_store: disabled excludes skill by frontmatter name" {
  let dir = ffi_skills_fixtures_path()
  let store = skills_store_load(dir, [])
  let before = skills_store_list(store, None, None)
  let after = skills_store_list(store, None, None, disabled=["tdd-workflow"])
  assert_eq(after.length(), before.length() - 1)
  match find_skill_by_name(after, "tdd-workflow") {
    Some(_) => fail("expected tdd-workflow to be disabled")
    None => ()
  }
}

test "skills_store: disabled unknown name is a no-op" {
  let dir = ffi_skills_fixtures_path()
  let store = skills_store_load(dir, [])
  let before = skills_store_list(store, None, None)
  let after = skills_store_list(store, None, None, disabled=["nonexistent"])
  assert_eq(after.length(), before.length())
}

test "skills_store: build_prompt respects disabled" {
  let dir = ffi_skills_fixtures_path()
  let store = skills_store_load(dir, [])
  let prompt = skills_store_build_prompt(
    store, None, None, disabled=["tdd-workflow"],
  )
  assert_eq(prompt.contains("tdd-workflow"), false)
}