///|
/// Programmatic builder for users who prefer typed MoonBit code over DSL text.
pub(all) struct TreeBuilder {
  root : String
  mut nodes : Array[BtNode]
  mut notes : Array[String]
} derive(Debug, Eq)

///|
pub fn new_builder(root : String) -> TreeBuilder {
  { root, nodes: Array::new(), notes: Array::new() }
}

///|
pub fn TreeBuilder::add_node(
  self : TreeBuilder,
  n : BtNode,
) -> Result[TreeBuilder, BtError] {
  if self.has_node(n.id) {
    Err(DuplicateNode(n.id))
  } else {
    self.nodes.push(n)
    Ok(self)
  }
}

///|
pub fn TreeBuilder::sequence(
  self : TreeBuilder,
  id : String,
  children : Array[String],
) -> Result[TreeBuilder, BtError] {
  self.add_node(node(id, Sequence, children~))
}

///|
pub fn TreeBuilder::selector(
  self : TreeBuilder,
  id : String,
  children : Array[String],
) -> Result[TreeBuilder, BtError] {
  self.add_node(node(id, Selector, children~))
}

///|
pub fn TreeBuilder::parallel_all(
  self : TreeBuilder,
  id : String,
  children : Array[String],
) -> Result[TreeBuilder, BtError] {
  self.add_node(node(id, ParallelAll, children~))
}

///|
pub fn TreeBuilder::parallel_any(
  self : TreeBuilder,
  id : String,
  children : Array[String],
) -> Result[TreeBuilder, BtError] {
  self.add_node(node(id, ParallelAny, children~))
}

///|
pub fn TreeBuilder::decorator(
  self : TreeBuilder,
  id : String,
  kind : NodeKind,
  child : String,
) -> Result[TreeBuilder, BtError] {
  self.add_node(node(id, kind, children=[child]))
}

///|
pub fn TreeBuilder::condition(
  self : TreeBuilder,
  id : String,
  key : String,
  op : CompareOp,
  expected : BtValue,
) -> Result[TreeBuilder, BtError] {
  self.add_node(condition(id, key, op, expected))
}

///|
pub fn TreeBuilder::set(
  self : TreeBuilder,
  id : String,
  key : String,
  value : BtValue,
) -> Result[TreeBuilder, BtError] {
  self.add_node(set_value(id, key, value))
}

///|
pub fn TreeBuilder::wait(
  self : TreeBuilder,
  id : String,
  ticks : Int,
) -> Result[TreeBuilder, BtError] {
  self.add_node(wait(id, ticks))
}

///|
pub fn TreeBuilder::action(
  self : TreeBuilder,
  id : String,
  name : String,
  statuses : Array[BtStatus],
  writes? : Array[(String, BtValue)],
) -> Result[TreeBuilder, BtError] {
  self.add_node(node(id, ActionPlan(name, statuses, writes.unwrap_or([]))))
}

///|
pub fn TreeBuilder::emit(
  self : TreeBuilder,
  id : String,
  label : String,
) -> Result[TreeBuilder, BtError] {
  self.add_node(node(id, Emit(label)))
}

///|
pub fn TreeBuilder::note(self : TreeBuilder, text : String) -> TreeBuilder {
  self.notes.push(text)
  self
}

///|
pub fn TreeBuilder::finish(self : TreeBuilder) -> Result[BehaviorTree, BtError] {
  let tree = tree_from_nodes(self.root, self.nodes)
  let report = tree.validate()
  if report.ok {
    Ok(tree)
  } else {
    Err(InvalidTree(report.message()))
  }
}

///|
pub fn TreeBuilder::has_node(self : TreeBuilder, id : String) -> Bool {
  let mut i = 0
  while i < self.nodes.length() {
    if self.nodes[i].id == id {
      return true
    }
    i = i + 1
  }
  false
}

///|
pub fn TreeBuilder::node_count(self : TreeBuilder) -> Int {
  self.nodes.length()
}

///|
pub fn TreeBuilder::note_lines(self : TreeBuilder) -> Array[String] {
  let out = Array::new()
  let mut i = 0
  while i < self.notes.length() {
    out.push(self.notes[i])
    i = i + 1
  }
  out
}

///|
pub(all) struct RecipeBundle {
  name : String
  description : String
  tree : BehaviorTree
  blackboard : Blackboard
  expected : BtStatus
  max_ticks : Int
} derive(Debug, Eq)

///|
pub fn RecipeBundle::summary(self : RecipeBundle) -> String {
  self.name +
  ": nodes=" +
  self.tree.node_count().to_string() +
  ", expected=" +
  self.expected.to_text() +
  ", board=" +
  self.blackboard.len().to_string()
}

///|
pub fn RecipeBundle::run(self : RecipeBundle) -> RunSummary {
  smoke_run(self.name, self.tree, self.blackboard, self.expected, max_ticks=self.max_ticks)
}

///|
pub(all) struct CombatRecipeOptions {
  enemy_visible : Bool
  ammo : Int
  stamina : Int
  aim_ticks : Int
  reload_ticks : Int
} derive(Debug, Eq)

///|
pub fn default_combat_options() -> CombatRecipeOptions {
  { enemy_visible: true, ammo: 2, stamina: 8, aim_ticks: 1, reload_ticks: 2 }
}

///|
pub fn combat_recipe(options? : CombatRecipeOptions) -> Result[RecipeBundle, BtError] {
  let opts = options.unwrap_or(default_combat_options())
  let builder = new_builder("root")
  ignore(builder.selector("root", ["engage", "reload_branch", "patrol"]))
  ignore(builder.sequence("engage", ["can_see", "has_ammo", "has_stamina", "aim", "fire"]))
  ignore(builder.condition("can_see", "enemy_visible", Eq, BoolValue(true)))
  ignore(builder.condition("has_ammo", "ammo", Greater, IntValue(0)))
  ignore(builder.condition("has_stamina", "stamina", GreaterEq, IntValue(5)))
  ignore(
    builder.action(
      "aim",
      "aim_weapon",
      running_then_success(opts.aim_ticks),
      writes=[("mode", TextValue("aiming"))],
    ),
  )
  ignore(builder.action("fire", "fire_weapon", [Success], writes=[("mode", TextValue("fire"))]))
  ignore(builder.sequence("reload_branch", ["empty_ammo", "reload"]))
  ignore(builder.condition("empty_ammo", "ammo", Eq, IntValue(0)))
  ignore(
    builder.action(
      "reload",
      "reload_weapon",
      running_then_success(opts.reload_ticks),
      writes=[("ammo", IntValue(3))],
    ),
  )
  ignore(builder.action("patrol", "patrol_area", [Success], writes=[("mode", TextValue("patrol"))]))
  match builder.finish() {
    Ok(tree) =>
      Ok({
        name: "combat",
        description: "Parameterized game combat behavior tree.",
        tree,
        blackboard: blackboard_from_pairs([
          ("enemy_visible", BoolValue(opts.enemy_visible)),
          ("ammo", IntValue(opts.ammo)),
          ("stamina", IntValue(opts.stamina)),
        ]),
        expected: Success,
        max_ticks: 12,
      })
    Err(err) => Err(err)
  }
}

///|
pub(all) struct RobotRecipeOptions {
  battery : Int
  package_ready : Bool
  charge_threshold : Int
  charge_ticks : Int
  navigate_ticks : Int
} derive(Debug, Eq)

///|
pub fn default_robot_options() -> RobotRecipeOptions {
  {
    battery: 20,
    package_ready: true,
    charge_threshold: 30,
    charge_ticks: 2,
    navigate_ticks: 2,
  }
}

///|
pub fn robot_recipe(options? : RobotRecipeOptions) -> Result[RecipeBundle, BtError] {
  let opts = options.unwrap_or(default_robot_options())
  let builder = new_builder("root")
  ignore(builder.selector("root", ["charge_first", "deliver", "standby"]))
  ignore(builder.sequence("charge_first", ["battery_low", "dock", "charge"]))
  ignore(builder.condition("battery_low", "battery", Less, IntValue(opts.charge_threshold)))
  ignore(builder.action("dock", "dock", [Running, Success], writes=[("docked", BoolValue(true))]))
  ignore(
    builder.action(
      "charge",
      "charge",
      running_then_success(opts.charge_ticks),
      writes=[("battery", IntValue(100)), ("docked", BoolValue(false))],
    ),
  )
  ignore(builder.sequence("deliver", ["package_ready", "pickup", "navigate", "dropoff"]))
  ignore(builder.condition("package_ready", "package_ready", Eq, BoolValue(true)))
  ignore(builder.action("pickup", "pickup", [Success], writes=[("carrying", BoolValue(true))]))
  ignore(
    builder.action(
      "navigate",
      "navigate",
      running_then_success(opts.navigate_ticks),
      writes=[("location", TextValue("target"))],
    ),
  )
  ignore(builder.action("dropoff", "dropoff", [Success], writes=[("delivered", BoolValue(true))]))
  ignore(builder.action("standby", "standby", [Success], writes=[("state", TextValue("standby"))]))
  match builder.finish() {
    Ok(tree) =>
      Ok({
        name: "robot",
        description: "Parameterized warehouse robot task behavior tree.",
        tree,
        blackboard: blackboard_from_pairs([
          ("battery", IntValue(opts.battery)),
          ("package_ready", BoolValue(opts.package_ready)),
        ]),
        expected: Success,
        max_ticks: 16,
      })
    Err(err) => Err(err)
  }
}

///|
pub(all) struct AgentRecipeOptions {
  needs_current_info : Bool
  tool_allowed : Bool
  ambiguity : Int
} derive(Debug, Eq)

///|
pub fn default_agent_options() -> AgentRecipeOptions {
  { needs_current_info: true, tool_allowed: true, ambiguity: 1 }
}

///|
pub fn agent_recipe(options? : AgentRecipeOptions) -> Result[RecipeBundle, BtError] {
  let opts = options.unwrap_or(default_agent_options())
  let builder = new_builder("root")
  ignore(builder.selector("root", ["research", "direct_answer", "clarify"]))
  ignore(builder.sequence("research", ["needs_info", "tool_ok", "search", "compose"]))
  ignore(builder.condition("needs_info", "needs_current_info", Eq, BoolValue(true)))
  ignore(builder.condition("tool_ok", "tool_allowed", Eq, BoolValue(true)))
  ignore(builder.action("search", "search", [Running, Success], writes=[("evidence", TextValue("ready"))]))
  ignore(builder.action("compose", "compose", [Success], writes=[("answer", TextValue("ready"))]))
  ignore(builder.sequence("direct_answer", ["low_ambiguity", "draft"]))
  ignore(builder.condition("low_ambiguity", "ambiguity", LessEq, IntValue(2)))
  ignore(builder.action("draft", "draft", [Success], writes=[("answer", TextValue("direct"))]))
  ignore(builder.action("clarify", "clarify", [Success], writes=[("answer", TextValue("needs_input"))]))
  match builder.finish() {
    Ok(tree) =>
      Ok({
        name: "agent",
        description: "AI-agent style decision tree with research, direct answer, and clarification paths.",
        tree,
        blackboard: blackboard_from_pairs([
          ("needs_current_info", BoolValue(opts.needs_current_info)),
          ("tool_allowed", BoolValue(opts.tool_allowed)),
          ("ambiguity", IntValue(opts.ambiguity)),
        ]),
        expected: Success,
        max_ticks: 10,
      })
    Err(err) => Err(err)
  }
}

///|
pub fn recipe_catalog() -> Array[RecipeBundle] {
  let out = Array::new()
  match combat_recipe() {
    Ok(bundle) => out.push(bundle)
    Err(_) => ()
  }
  match robot_recipe() {
    Ok(bundle) => out.push(bundle)
    Err(_) => ()
  }
  match agent_recipe() {
    Ok(bundle) => out.push(bundle)
    Err(_) => ()
  }
  out
}

///|
pub fn run_recipe_catalog() -> Array[RunSummary] {
  let bundles = recipe_catalog()
  let out = Array::new()
  let mut i = 0
  while i < bundles.length() {
    out.push(bundles[i].run())
    i = i + 1
  }
  out
}

///|
pub fn recipe_report_lines() -> Array[String] {
  let bundles = recipe_catalog()
  let lines = Array::new()
  lines.push("MoonBTKit recipe report")
  let mut i = 0
  while i < bundles.length() {
    lines.push(bundles[i].summary())
    lines.push(bundles[i].run().to_line())
    i = i + 1
  }
  lines
}

///|
pub fn recipe_to_case(bundle : RecipeBundle) -> CaseSpec {
  {
    name: "recipe_" + bundle.name,
    dsl: serialize_tree(bundle.tree).join("\n") +
      "\n" +
      serialize_blackboard(bundle.blackboard).join("\n"),
    expectation: {
      final_status: bundle.expected,
      required_board: [],
      max_ticks: bundle.max_ticks,
    },
  }
}

///|
pub fn recipe_cases() -> Array[CaseSpec] {
  let bundles = recipe_catalog()
  let cases = Array::new()
  let mut i = 0
  while i < bundles.length() {
    cases.push(recipe_to_case(bundles[i]))
    i = i + 1
  }
  cases
}

///|
pub fn running_then_success(running_ticks : Int) -> Array[BtStatus] {
  let out = Array::new()
  let mut i = 0
  while i < running_ticks {
    out.push(Running)
    i = i + 1
  }
  out.push(Success)
  out
}

///|
pub fn running_then_failure(running_ticks : Int) -> Array[BtStatus] {
  let out = Array::new()
  let mut i = 0
  while i < running_ticks {
    out.push(Running)
    i = i + 1
  }
  out.push(Failure)
  out
}

///|
pub fn repeat_status(status : BtStatus, count : Int) -> Array[BtStatus] {
  let out = Array::new()
  let mut i = 0
  while i < count {
    out.push(status)
    i = i + 1
  }
  out
}

///|
pub fn append_status(
  base : Array[BtStatus],
  tail : BtStatus,
) -> Array[BtStatus] {
  let out = Array::new()
  let mut i = 0
  while i < base.length() {
    out.push(base[i])
    i = i + 1
  }
  out.push(tail)
  out
}