///|
/// Lightweight view of a MoonBit project directory.
pub(all) struct ProjectSnapshot {
  root : String
  files : Map[String, String]
  dirs : Array[String]
} derive(Debug, Eq)

///|
pub fn ProjectSnapshot::new(
  root : String,
  files : Map[String, String],
  dirs : Array[String],
) -> ProjectSnapshot {
  { root, files, dirs }
}

///|
pub fn ProjectSnapshot::has_file(self : ProjectSnapshot, path : String) -> Bool {
  self.files.contains(path)
}

///|
pub fn ProjectSnapshot::has_dir(self : ProjectSnapshot, path : String) -> Bool {
  self.dirs.contains(path)
}

///|
pub fn ProjectSnapshot::read_file(
  self : ProjectSnapshot,
  path : String,
) -> String? {
  self.files.get(path)
}

///|
pub fn ProjectSnapshot::moon_mod_field(
  self : ProjectSnapshot,
  key : String,
) -> String? {
  match self.read_file("moon.mod") {
    None => None
    Some(content) => extract_assignment(content, key)
  }
}

///|
pub fn ProjectSnapshot::has_any_test_file(self : ProjectSnapshot) -> Bool {
  self.files
  .keys()
  .any(fn(name) {
    name.has_suffix("_test.mbt") ||
    name.has_suffix("_wbtest.mbt") ||
    name.contains("/test/") ||
    name.contains("\\test\\")
  })
}

///|
pub fn ProjectSnapshot::has_workflow(self : ProjectSnapshot) -> Bool {
  self.has_dir(".github/workflows") ||
  self.files
  .keys()
  .any(fn(name) {
    name.has_prefix(".github/workflows/") ||
    name.has_prefix(".github\\workflows\\")
  })
}

///|
pub fn ProjectSnapshot::gitignore_mentions(
  self : ProjectSnapshot,
  expected : String,
) -> Bool {
  match self.read_file(".gitignore") {
    Some(content) => content.contains(expected)
    None => false
  }
}

///|
pub fn ProjectSnapshot::license_mentions(
  self : ProjectSnapshot,
  expected : String,
) -> Bool {
  match self.read_file("LICENSE") {
    Some(content) => content.contains(expected)
    None => false
  }
}

///|
pub fn ProjectSnapshot::has_readme_file(self : ProjectSnapshot) -> Bool {
  self.has_file("README.md") || self.has_file("README.mbt.md")
}

///|
pub fn ProjectSnapshot::has_exact_file(
  self : ProjectSnapshot,
  path : String,
) -> Bool {
  self.has_file(path)
}

///|
pub fn ProjectSnapshot::has_example_content(self : ProjectSnapshot) -> Bool {
  self.files
  .keys()
  .any(fn(name) {
    name.has_prefix("examples/") &&
    !name.has_suffix(".keep") &&
    (
      name.has_suffix(".mbt") ||
      name.has_suffix(".md") ||
      name.has_suffix("moon.pkg")
    )
  })
}

///|
pub fn ProjectSnapshot::workflow_mentions(
  self : ProjectSnapshot,
  needle : String,
) -> Bool {
  self.files
  .keys()
  .any(fn(name) {
    if name.has_prefix(".github/workflows/") {
      match self.read_file(name) {
        Some(content) => content.contains(needle)
        None => false
      }
    } else {
      false
    }
  })
}

///|
pub fn ProjectSnapshot::field_mentions(
  self : ProjectSnapshot,
  key : String,
  needle : String,
) -> Bool {
  match self.read_file("moon.mod") {
    Some(content) => {
      for line in content.split("\n") {
        let text = line.trim().to_owned()
        if !text.has_prefix("//") &&
          text.has_prefix(key) &&
          text.contains(needle) {
          return true
        }
      }
      false
    }
    None => false
  }
}

///|
pub fn ProjectSnapshot::readme_mentions(
  self : ProjectSnapshot,
  needle : String,
) -> Bool {
  match self.read_file("README.md") {
    Some(content) => content.contains(needle)
    None => false
  }
}

///|
fn extract_assignment(content : String, key : String) -> String? {
  for line in content.split("\n") {
    let text = line.trim().to_owned()
    if !text.has_prefix("//") && text.has_prefix(key) && text.contains("=") {
      let pieces = text.split("\"").to_array()
      if pieces.length() >= 3 {
        return Some(pieces[1].to_owned())
      }
    }
  }
  None
}

///|
fn join_path(root : String, rel : String) -> String {
  if root.has_suffix("/") || root.has_suffix("\\") {
    root + rel
  } else {
    root + "/" + rel
  }
}

///|
fn add_file_if_exists(
  files : Map[String, String],
  root : String,
  rel : String,
) -> Unit raise {
  let path = join_path(root, rel)
  if @fs.path_exists(path) && @fs.is_file(path) {
    files[rel] = @fs.read_file_to_string(path)
  }
}

///|
fn add_dir_if_exists(
  dirs : Array[String],
  root : String,
  rel : String,
) -> Unit raise {
  let path = join_path(root, rel)
  if @fs.path_exists(path) && @fs.is_dir(path) {
    dirs.push(rel)
  }
}

///|
fn add_dir_files_if_exists(
  files : Map[String, String],
  root : String,
  rel : String,
) -> Unit raise {
  let dir_path = join_path(root, rel)
  if @fs.path_exists(dir_path) && @fs.is_dir(dir_path) {
    for entry in @fs.read_dir(dir_path) {
      let child_rel = rel + "/" + entry
      let child_path = join_path(root, child_rel)
      if @fs.path_exists(child_path) && @fs.is_file(child_path) {
        add_file_if_exists(files, root, child_rel)
      }
    }
  }
}

///|
/// Load the project files Moon Doctor needs for a first-pass health check.
pub fn load_project(root : String) -> ProjectSnapshot raise {
  let files : Map[String, String] = {}
  let dirs : Array[String] = []
  for
    rel in [
      "moon.mod", "moon.pkg", "README.md", "README.mbt.md", "LICENSE", ".gitignore",
    ] {
    add_file_if_exists(files, root, rel)
  }
  for
    rel in ["examples", ".github", ".github/workflows", "cmd", "test", "tests"] {
    add_dir_if_exists(dirs, root, rel)
  }
  if @fs.path_exists(root) && @fs.is_dir(root) {
    for entry in @fs.read_dir(root) {
      if entry.has_suffix("_test.mbt") || entry.has_suffix("_wbtest.mbt") {
        add_file_if_exists(files, root, entry)
      }
    }
  }
  if dirs.contains(".github/workflows") {
    add_dir_files_if_exists(files, root, ".github/workflows")
  }
  if dirs.contains("examples") {
    add_dir_files_if_exists(files, root, "examples")
  }
  if dirs.contains("cmd") {
    add_dir_files_if_exists(files, root, "cmd")
    let cmd_root = join_path(root, "cmd")
    for entry in @fs.read_dir(cmd_root) {
      let child_rel = "cmd/" + entry
      let child_path = join_path(root, child_rel)
      if @fs.path_exists(child_path) && @fs.is_dir(child_path) {
        add_dir_files_if_exists(files, root, child_rel)
      }
    }
  }
  ProjectSnapshot::new(root, files, dirs)
}