///|
pub struct CliRunSpec {
  command : String
  show_help : Bool
  workflow_path : String
  cache_store_path : String
  signatures : Map[String, String]
  success_task_ids : Array[String]
  errors : Array[String]
}

///|
fn drop_program_name(raw_args : Array[String]) -> Array[String] {
  if raw_args.length() <= 1 {
    return []
  }
  let args : Array[String] = []
  for i in 1.. CliRunSpec {
  let args = drop_program_name(raw_args)
  let errors : Array[String] = []
  let signature_result = @wf.parse_external_inputs(
    args,
    {},
    cli_flag="--signature",
    env_prefix="BITFLOW_SIGNATURE_",
  )
  for _err in signature_result.errors {
    errors.push("option --signature expects key=value")
  }
  let mut command = "check"
  let mut show_help = false
  let mut workflow_path = ""
  let mut cache_store_path = ""
  let success_task_ids : Array[String] = []
  let mut i = 0
  if args.length() > 0 &&
    (
      args[0] == "check" ||
      args[0] == "cache-plan" ||
      args[0] == "cache-writeback"
    ) {
    command = args[0]
    i += 1
  }
  while i < args.length() {
    let arg = args[i]
    if arg == "-h" || arg == "--help" {
      show_help = true
      i += 1
      continue
    }
    if arg == "--var" {
      if i + 1 >= args.length() {
        errors.push("option --var expects key=value")
        i += 1
      } else {
        i += 2
      }
      continue
    }
    if arg.has_prefix("--var=") {
      i += 1
      continue
    }
    if arg == "--signature" {
      if i + 1 >= args.length() {
        errors.push("option --signature expects key=value")
        i += 1
      } else {
        i += 2
      }
      continue
    }
    if arg.has_prefix("--signature=") {
      i += 1
      continue
    }
    if arg == "--cache-store" {
      if i + 1 >= args.length() {
        errors.push("option --cache-store expects path")
        i += 1
      } else {
        cache_store_path = args[i + 1]
        i += 2
      }
      continue
    }
    if arg.has_prefix("--cache-store=") {
      cache_store_path = String::unsafe_substring(
        arg,
        start="--cache-store=".length(),
        end=arg.length(),
      )
      if cache_store_path.length() == 0 {
        errors.push("option --cache-store expects path")
      }
      i += 1
      continue
    }
    if arg == "--success" {
      if i + 1 >= args.length() {
        errors.push("option --success expects task id")
        i += 1
      } else {
        success_task_ids.push(args[i + 1])
        i += 2
      }
      continue
    }
    if arg.has_prefix("--success=") {
      let task_id = String::unsafe_substring(
        arg,
        start="--success=".length(),
        end=arg.length(),
      )
      if task_id.length() == 0 {
        errors.push("option --success expects task id")
      } else {
        success_task_ids.push(task_id)
      }
      i += 1
      continue
    }
    if arg.has_prefix("-") {
      errors.push("unknown option: " + arg)
      i += 1
      continue
    }
    if workflow_path.length() == 0 {
      workflow_path = arg
    } else {
      errors.push("multiple workflow paths: '\{workflow_path}' and '\{arg}'")
    }
    i += 1
  }
  if !show_help && workflow_path.length() == 0 {
    errors.push("workflow path is required")
  }
  if !show_help &&
    (command == "cache-plan" || command == "cache-writeback") &&
    cache_store_path.length() == 0 {
    errors.push("option --cache-store is required")
  }
  {
    command,
    show_help,
    workflow_path,
    cache_store_path,
    signatures: signature_result.values,
    success_task_ids,
    errors,
  }
}

///|
fn host_fs_adapter() -> @wf.FsAdapter {
  @wf.FsAdapter::new(
    fn(path : String) {
      try @xfs.read_file_to_string(path) catch {
        _ => None
      } noraise {
        text => Some(text)
      }
    },
    fn(path : String, content : String) {
      try @xfs.write_string_to_file(path, content) catch {
        _ => false
      } noraise {
        _ => true
      }
    },
    fn(path : String) { @xfs.path_exists(path) },
    fn() -> Array[String] { [] },
  )
}

///|
fn print_usage() -> Unit {
  println("Usage: bitflow [check]  [--var KEY=VALUE]...")
  println(
    "       bitflow cache-plan  --cache-store  [--signature KEY=VALUE]...",
  )
  println(
    "       bitflow cache-writeback  --cache-store  [--signature KEY=VALUE] [--success TASK_ID]...",
  )
  println("Env override: BITFLOW_VAR_=")
}

///|
fn print_parse_errors(errors : Array[String]) -> Unit {
  for err in errors {
    println("error: " + err)
  }
}

///|
fn main {
  let raw_args = cli_args()
  let spec = parse_main_cli_args(raw_args)
  if spec.show_help {
    print_usage()
    return
  }
  if spec.errors.length() > 0 {
    print_parse_errors(spec.errors)
    print_usage()
    @xsys.exit(2)
  }
  let adapter = @wf.WorkflowAdapter::new(
    host_fs_adapter(),
    @wf.CommandAdapter::none(),
  )
  if spec.command == "cache-plan" {
    let cache_store = @wf.read_flow_cache_store(spec.cache_store_path, adapter)
    let planned : @wf.FlowTaskCachePlanResult = if cache_store.issues.length() >
      0 {
      @wf.new_task_cache_plan_result([], cache_store.issues)
    } else {
      @wf.plan_task_cache_from_fs_with_cli_env(
        spec.workflow_path,
        adapter,
        spec.signatures,
        cache_store.entries,
        drop_program_name(raw_args),
        @env.get_env_vars(),
      )
    }
    println(@wf.render_task_cache_plan_json(planned))
    if planned.issues.length() > 0 {
      @xsys.exit(1)
    }
    return
  }
  if spec.command == "cache-writeback" {
    let cache_store = @wf.read_flow_cache_store(spec.cache_store_path, adapter)
    let writeback : @wf.FlowTaskCacheWritebackResult = if cache_store.issues.length() >
      0 {
      @wf.new_task_cache_writeback_result({}, [], cache_store.issues)
    } else {
      @wf.writeback_task_cache_from_fs_with_cli_env(
        spec.workflow_path,
        adapter,
        spec.signatures,
        cache_store.entries,
        spec.success_task_ids,
        drop_program_name(raw_args),
        @env.get_env_vars(),
      )
    }
    let persisted = if writeback.issues.length() == 0 {
      @wf.write_flow_cache_store(
        spec.cache_store_path,
        writeback.entries,
        adapter,
      )
    } else {
      true
    }
    let final_writeback = if !persisted {
      @wf.new_task_cache_writeback_result(
        writeback.entries,
        writeback.updated,
        ["failed to write cache store '\{spec.cache_store_path}'"],
      )
    } else {
      writeback
    }
    println(@wf.render_task_cache_writeback_json(final_writeback))
    if final_writeback.issues.length() > 0 {
      @xsys.exit(1)
    }
    return
  }
  let parsed = @wf.parse_from_fs_with_cli_env(
    spec.workflow_path,
    adapter,
    drop_program_name(raw_args),
    @env.get_env_vars(),
  )
  if parsed.errors.length() > 0 {
    print_parse_errors(parsed.errors)
    @xsys.exit(1)
  }
  println("workflow=\{parsed.ir.name}")
  println("nodes=\{parsed.ir.nodes.length().to_string()}")
  println("tasks=\{parsed.ir.tasks.length().to_string()}")
}

///|
test "main cli: parses check command with workflow path and var flags" {
  let spec = parse_main_cli_args([
    "bitflow", "check", "workflow.star", "--var", "profile=prod", "--var=retries=3",
  ])
  assert_eq(spec.command, "check")
  assert_eq(spec.show_help, false)
  assert_eq(spec.workflow_path, "workflow.star")
  assert_eq(spec.cache_store_path, "")
  assert_eq(spec.signatures.length(), 0)
  assert_eq(spec.success_task_ids.length(), 0)
  assert_eq(spec.errors.length(), 0)
}

///|
test "main cli: reports missing workflow path and malformed var option" {
  let spec = parse_main_cli_args(["bitflow", "--var"])
  let text = spec.errors.join("\n")
  assert_true(text.contains("option --var expects key=value"))
  assert_true(text.contains("workflow path is required"))
}

///|
test "main cli: accepts help flag without workflow path" {
  let spec = parse_main_cli_args(["bitflow", "--help"])
  assert_eq(spec.show_help, true)
  assert_eq(spec.errors.length(), 0)
}

///|
test "main cli: reports unknown option and multiple workflow paths" {
  let spec = parse_main_cli_args([
    "bitflow", "--unknown", "workflow.star", "other.star",
  ])
  let text = spec.errors.join("\n")
  assert_true(text.contains("unknown option"))
  assert_true(text.contains("multiple workflow paths"))
}

///|
test "main cli: parses cache-plan command with cache store and signatures" {
  let spec = parse_main_cli_args([
    "bitflow", "cache-plan", "workflow.star", "--cache-store", "cache.json", "--signature",
    "root=sig-root", "--signature=dep=sig-dep",
  ])
  assert_eq(spec.command, "cache-plan")
  assert_eq(spec.workflow_path, "workflow.star")
  assert_eq(spec.cache_store_path, "cache.json")
  assert_eq(spec.signatures.get("root"), Some("sig-root"))
  assert_eq(spec.signatures.get("dep"), Some("sig-dep"))
  assert_eq(spec.errors.length(), 0)
}

///|
test "main cli: cache-plan requires cache store and well-formed signatures" {
  let spec = parse_main_cli_args([
    "bitflow", "cache-plan", "workflow.star", "--signature", "broken",
  ])
  let text = spec.errors.join("\n")
  assert_true(text.contains("option --signature expects key=value"))
  assert_true(text.contains("option --cache-store is required"))
}

///|
test "main cli: parses cache-writeback command with success tasks" {
  let spec = parse_main_cli_args([
    "bitflow", "cache-writeback", "workflow.star", "--cache-store=cache.json", "--signature=root=sig-root",
    "--success", "root:build", "--success=dep:test",
  ])
  assert_eq(spec.command, "cache-writeback")
  assert_eq(spec.workflow_path, "workflow.star")
  assert_eq(spec.cache_store_path, "cache.json")
  assert_eq(spec.signatures.get("root"), Some("sig-root"))
  assert_eq(spec.success_task_ids.length(), 2)
  assert_eq(spec.success_task_ids[0], "root:build")
  assert_eq(spec.success_task_ids[1], "dep:test")
  assert_eq(spec.errors.length(), 0)
}

///|
test "main cli: cache-writeback validates success option arity" {
  let spec = parse_main_cli_args([
    "bitflow", "cache-writeback", "workflow.star", "--cache-store", "cache.json",
    "--success",
  ])
  let text = spec.errors.join("\n")
  assert_true(text.contains("option --success expects task id"))
}

///|
#cfg(target="js")
fn cli_args() -> Array[String] {
  // core/env includes the JS runtime; retain the existing program-name contract.
  @env.args()[1:].to_owned()
}

///|
#cfg(not(target="js"))
fn cli_args() -> Array[String] {
  @env.args()
}