// `moonlm agent --task "..." --workspace  [--model ]`.
//
// The command wires the production pieces together and nothing else: the model
// is the same `PureEngine` as `chat`, the sandbox is the same `Sandbox` the
// tests exercise, and the loop is `run_agent`. What is CLI-specific here is only
// argv parsing and printing.

///|
/// Options for one agent run.
struct AgentCliOptions {
  task : String
  workspace : String
  model : String
  max_steps : Int
  max_tokens : Int
  max_observation_chars : Int
  verbose : Bool
}

///|
/// The default model path, so a demonstration run needs only `--task`.
///
/// Checked in the order listed and the first that exists wins; an explicit
/// `--model` always overrides, and a missing default is an error naming the
/// flag rather than a guess.
fn default_model_candidates() -> Array[String] {
  [
    "D:/llmdata/qwen2.5-0.5b-instruct-q4_k_m.gguf",
    "D:/llmdata/qwen2.5-0.5b-q4_0.gguf",
  ]
}

///|
/// Parse `moonlm agent` arguments.
///
/// Hand-rolled like the other subcommands so the error text points at the flag
/// that was wrong; there is no hidden default for `--task` or `--workspace`
/// because both change what the run does.
fn parse_agent_args(args : Array[String]) -> Result[AgentCliOptions, String] {
  let mut task : String? = None
  let mut workspace : String? = None
  let mut model : String? = None
  let mut max_steps = 6
  let mut max_tokens = 384
  let mut max_obs = 4096
  let mut verbose = false
  let mut i = 2
  while i < args.length() {
    let flag = args[i]
    if flag == "--task" {
      if i + 1 >= args.length() {
        return Err("--task needs a value")
      }
      task = Some(args[i + 1])
      i = i + 2
    } else if flag == "--workspace" {
      if i + 1 >= args.length() {
        return Err("--workspace needs a value")
      }
      workspace = Some(args[i + 1])
      i = i + 2
    } else if flag == "--model" {
      if i + 1 >= args.length() {
        return Err("--model needs a value")
      }
      model = Some(args[i + 1])
      i = i + 2
    } else if flag == "--max-steps" {
      if i + 1 >= args.length() {
        return Err("--max-steps needs a value")
      }
      match parse_int_arg(args[i + 1]) {
        Ok(v) => max_steps = v
        Err(e) => return Err("--max-steps: \{e}")
      }
      i = i + 2
    } else if flag == "--max-tokens" {
      if i + 1 >= args.length() {
        return Err("--max-tokens needs a value")
      }
      match parse_int_arg(args[i + 1]) {
        Ok(v) => max_tokens = v
        Err(e) => return Err("--max-tokens: \{e}")
      }
      i = i + 2
    } else if flag == "--max-observation" {
      if i + 1 >= args.length() {
        return Err("--max-observation needs a value")
      }
      match parse_int_arg(args[i + 1]) {
        Ok(v) => max_obs = v
        Err(e) => return Err("--max-observation: \{e}")
      }
      i = i + 2
    } else if flag == "--verbose" {
      verbose = true
      i = i + 1
    } else {
      return Err("unknown agent option \"\{flag}\"")
    }
  }
  let task = match task {
    Some(t) =>
      if t == "" {
        return Err("--task must not be empty")
      } else {
        t
      }
    None => return Err("agent needs --task \"...\"")
  }
  let workspace = match workspace {
    Some(w) => w
    None => return Err("agent needs --workspace ")
  }
  let model = match model {
    Some(m) => m
    None => {
      let mut picked = ""
      for candidate in default_model_candidates() {
        if picked == "" && fs_ok(candidate) {
          picked = candidate
        }
      }
      if picked == "" {
        return Err(
          "agent needs --model  (no default model was found)",
        )
      }
      picked
    }
  }
  Ok({
    task,
    workspace,
    model,
    max_steps,
    max_tokens,
    max_observation_chars: max_obs,
    verbose,
  })
}

///|
/// Whether a path exists, without pulling the tools package into argv parsing.
#borrow(path)
extern "C" fn ml_path_exists(path : Bytes) -> Int = "ml_path_exists"

///|
fn fs_ok(path : String) -> Bool {
  ml_path_exists(@utf8.encode(path) + b"\x00") != 0
}

///|
/// Run the agent end to end and print a transcript.
fn run_agent_cli(args : Array[String]) -> Int {
  let opts = match parse_agent_args(args) {
    Ok(o) => o
    Err(e) => {
      println("error: \{e}\n")
      println(agent_usage())
      return 2
    }
  }
  let sandbox = match @tools.Sandbox::open(opts.workspace) {
    Ok(s) => s
    Err(e) => {
      println("error: \{e}")
      return 1
    }
  }
  let engine = match load_engine(opts.model) {
    Ok(e) => e
    Err(e) => {
      println("error: \{e}")
      return 1
    }
  }
  // The model has to speak the format the parser expects. Read it from the
  // tokenizer's `pre` value rather than assuming: a file that is not Qwen2
  // would otherwise fail with an empty loop rather than a clear message.
  let format = match engine.tokenizer.pre {
    Qwen2 => @agent.Qwen2Xml
    Smollm =>
      return {
        println(
          "error: the agent loop implements the Qwen2 tool-call format; this model uses the \"smollm\" pre-tokenizer",
        )
        2
      }
  }
  let registry = @agent.ToolRegistry::new()
  match @tools.register_fs_tools(registry, sandbox) {
    Ok(_) => ()
    Err(e) => {
      println("error: \{e}")
      return 1
    }
  }
  registry.add(@tools.CalcTool::{}) |> ignore
  let config : @agent.AgentConfig = {
    max_steps: opts.max_steps,
    max_calls_per_step: 4,
    max_tokens_per_step: opts.max_tokens,
    max_observation_chars: opts.max_observation_chars,
    max_history_chars: 6000,
    temperature: 0.0,
    seed: 42UL,
    require_evidence: true,
  }
  println("task:      \{opts.task}")
  println("workspace: \{sandbox.root}")
  println("model:     \{opts.model}")
  println("tools:     \{registry.names().join(", ")}")
  println("")
  let result = @agent.run_agent(
    engine,
    registry,
    format,
    @agent.Qwen2ChatMl,
    opts.task,
    config,
  )
  for call in result.executed {
    let status = if call.ok { "ok" } else { "error" }
    println("[\{status}] \{call.name}(\{call.arguments})")
    if opts.verbose {
      println("  -> \{call.observation}")
    }
  }
  println("")
  match result.stop {
    Answered => {
      println("answer:")
      println(result.answer)
      0
    }
    StepLimit => {
      println("(stopped after \{result.steps} steps without a final answer)")
      if result.executed.length() > 0 {
        println("last observation:")
        println(result.executed[result.executed.length() - 1].observation)
      }
      1
    }
    InvalidArguments(message~) => {
      println("(model produced an invalid tool call after \{result.steps} steps)")
      println("  \{message}")
      1
    }
    EngineError(message~) => {
      println("error: \{message}")
      1
    }
    ContextOverflow => {
      println("error: the prompt does not fit the model's context even after folding the history")
      1
    }
  }
}

///|
fn agent_usage() -> String {
  [
    "Usage: moonlm agent --task \"...\" --workspace  [options]",
    "",
    "Runs the agent loop with the read-only local tools (list_dir, read_file,",
    "grep_text, calc), sandboxed to --workspace.",
    "",
    "Options:",
    "  --task TEXT          What to do (required)",
    "  --workspace DIR      Sandbox root for every file tool (required)",
    "  --model PATH         GGUF model; a Qwen2 file is required",
    "  --max-steps N        Assistant turns before stopping (default 6)",
    "  --max-tokens N       Tokens per generation (default 384)",
    "  --max-observation N  Characters of a tool result kept (default 4096)",
    "  --verbose            Print each tool observation",
    "",
    "Example:",
    "  moonlm agent --task \"总结 docs/ 下的关键结论\" --workspace . --verbose",
  ].join("\n")
}