/// Detect the installed `moon` CLI version.
pub fn detect_moon_version() -> String {
  match popen("moon --version 2>/dev/null") {
    Some(v) => trim(v)
    None => "unknown"
  }
}

/// Parse a semver string "x.y.z" (optionally with leading text or pre-release suffix).
pub fn parse_semver(s : String) -> Option[SemVer] {
  let len = s.length()
  if len == 0 { return None }

  // Find first digit
  let mut i = 0
  while i < len {
    let ch = s[i]
    if ch >= zero() && ch <= nine() { break }
    i = i + 1
  }
  if i >= len { return None }

  // Parse major
  let mut major_val = 0
  while i < len {
    let ch = s[i]
    if ch >= zero() && ch <= nine() {
      major_val = major_val * 10 + (ch - zero()).to_int()
      i = i + 1
    } else {
      break
    }
  }
  if i >= len || s[i] != dot() { return None }
  i = i + 1

  // Parse minor
  let mut minor_val = 0
  while i < len {
    let ch = s[i]
    if ch >= zero() && ch <= nine() {
      minor_val = minor_val * 10 + (ch - zero()).to_int()
      i = i + 1
    } else {
      break
    }
  }
  if i >= len || s[i] != dot() { return None }
  i = i + 1

  // Parse patch
  let mut patch_val = 0
  while i < len {
    let ch = s[i]
    if ch >= zero() && ch <= nine() {
      patch_val = patch_val * 10 + (ch - zero()).to_int()
      i = i + 1
    } else {
      break
    }
  }

  Some({ major: major_val, minor: minor_val, patch: patch_val })
}

fn zero() -> UInt16 { '0'.to_int().to_uint16() }
fn nine() -> UInt16 { '9'.to_int().to_uint16() }
fn dot() -> UInt16 { '.'.to_int().to_uint16() }

/// Compare two SemVers. Returns -1 if a < b, 0 if equal, 1 if a > b.
pub fn compare_semver(a : SemVer, b : SemVer) -> Int {
  if a.major != b.major { return if a.major < b.major { -1 } else { 1 } }
  if a.minor != b.minor { return if a.minor < b.minor { -1 } else { 1 } }
  if a.patch != b.patch { return if a.patch < b.patch { -1 } else { 1 } }
  0
}

/// Build a complete version report with optional constraints from env vars.
pub fn make_report(moon_version : String) -> VersionReport {
  let semver_val = parse_semver(moon_version)

  let check_min_str = getenv("MOONBIT_CHECK_MIN")
  let check_max_str = getenv("MOONBIT_CHECK_MAX")

  let check_min = match check_min_str {
    Some(s) => parse_semver(s)
    None => None
  }
  let check_max = match check_max_str {
    Some(s) => parse_semver(s)
    None => None
  }

  let meets_min = match (semver_val, check_min) {
    (Some(ver), Some(min)) => compare_semver(ver, min) >= 0
    _ => true
  }
  let meets_max = match (semver_val, check_max) {
    (Some(ver), Some(max)) => compare_semver(ver, max) <= 0
    _ => true
  }

  {
    moon_version,
    semver: semver_val,
    check_min,
    check_max,
    meets_min,
    meets_max,
    meets_all: meets_min && meets_max,
  }
}

/// Serialize version report to JSON.
pub fn report_to_json(report : VersionReport) -> String {
  let sb = StringBuilder()
  sb.write_string("{\n")
  sb.write_string("  \"moon_version\": ")
  sb.write_string(json_escape(report.moon_version))
  sb.write_string(",\n")

  sb.write_string("  \"semver\": ")
  match report.semver {
    Some(v) => {
      sb.write_string("{\n")
      sb.write_string("    \"major\": " + v.major.to_string() + ",\n")
      sb.write_string("    \"minor\": " + v.minor.to_string() + ",\n")
      sb.write_string("    \"patch\": " + v.patch.to_string() + "\n")
      sb.write_string("  }")
    }
    None => { sb.write_string("null") }
  }
  sb.write_string(",\n")

  // Constraints
  sb.write_string("  \"check_min\": ")
  match report.check_min {
    Some(v) => { sb.write_string(semver_to_json(v)) }
    None => { sb.write_string("null") }
  }
  sb.write_string(",\n")

  sb.write_string("  \"check_max\": ")
  match report.check_max {
    Some(v) => { sb.write_string(semver_to_json(v)) }
    None => { sb.write_string("null") }
  }
  sb.write_string(",\n")

  sb.write_string("  \"meets_min\": ")
  sb.write_string(if report.meets_min { "true" } else { "false" })
  sb.write_string(",\n")

  sb.write_string("  \"meets_max\": ")
  sb.write_string(if report.meets_max { "true" } else { "false" })
  sb.write_string(",\n")

  sb.write_string("  \"meets_all\": ")
  sb.write_string(if report.meets_all { "true" } else { "false" })
  sb.write_string("\n}\n")

  sb.to_string()
}

/// Format a SemVer as JSON object.
fn semver_to_json(v : SemVer) -> String {
  let sb = StringBuilder()
  sb.write_string("{\n")
  sb.write_string("    \"major\": ")
  sb.write_string(v.major.to_string())
  sb.write_string(",\n")
  sb.write_string("    \"minor\": ")
  sb.write_string(v.minor.to_string())
  sb.write_string(",\n")
  sb.write_string("    \"patch\": ")
  sb.write_string(v.patch.to_string())
  sb.write_string("\n  }")
  sb.to_string()
}

/// Trim whitespace from a string.
fn trim(s : String) -> String {
  let len = s.length()
  if len == 0 { return s }

  let mut start = 0
  while start < len && is_whitespace(s[start]) {
    start = start + 1
  }

  let mut end = len
  while end > start && is_whitespace(s[end - 1]) {
    end = end - 1
  }

  if start >= end {
    ""
  } else {
    // Build result without using deprecated substring
    let sb = StringBuilder()
    for i = start; i < end; i = i + 1 {
      sb.write_char(s[i].unsafe_to_char())
    }
    sb.to_string()
  }
}

fn is_whitespace(ch : UInt16) -> Bool {
  let sp = ' '.to_int().to_uint16()
  let nl = '\n'.to_int().to_uint16()
  let cr = '\r'.to_int().to_uint16()
  let tab = '\t'.to_int().to_uint16()
  ch == sp || ch == nl || ch == cr || ch == tab
}

/// JSON string escape.
fn json_escape(s : String) -> String {
  let sb = StringBuilder()
  sb.write_char('"')
  for i = 0; i < s.length(); i = i + 1 {
    let ch = s[i]
    if ch == '"'.to_int().to_uint16() {
      sb.write_string("\\\"")
    } else if ch == '\\'.to_int().to_uint16() {
      sb.write_string("\\\\")
    } else if ch == '\n'.to_int().to_uint16() {
      sb.write_string("\\n")
    } else if ch == '\r'.to_int().to_uint16() {
      sb.write_string("\\r")
    } else if ch == '\t'.to_int().to_uint16() {
      sb.write_string("\\t")
    } else {
      sb.write_char(ch.unsafe_to_char())
    }
  }
  sb.write_char('"')
  sb.to_string()
}

// ── C FFI ──

#borrow(cmd)
extern "c" fn popen(cmd : String) -> Option[String] = "moonwasm_probe_ffi_popen"

#borrow(name)
extern "c" fn getenv(name : String) -> Option[String] = "moonwasm_probe_ffi_getenv"

/// Get an environment variable string.
pub fn get_env(name : String) -> Option[String] {
  getenv(name)
}

/// Exit the process with a code.
pub fn exit_process(code : Int) -> Unit {
  raw_exit(code)
}

extern "c" fn raw_exit(code : Int) = "exit"