///|
/// Plan API: parse + lower a workflow YAML and return a structured plan.
/// This is the scheduler component for worker environments.

///|
pub struct PlanResult {
  workflow_name : String
  errors : Array[String]
  jobs : Array[PlanJob]
}

///|
pub struct PlanJob {
  id : String
  needs : Array[String]
  runs_on : Array[String]
  steps : Array[PlanStep]
}

///|
pub struct PlanStep {
  id : String
  kind : String
  name : String
  shell : String
  script : String
  uses : String
  backend : String
  if_condition : String
  env : Map[String, String]
  with_values : Map[String, String]
}

///|
pub fn plan_workflow(
  yaml_text : String,
  workspace_root? : String = "",
) -> PlanResult {
  let parsed = parse_workflow_yaml(yaml_text)
  guard parsed.workflow is Some(workflow) else {
    return { workflow_name: "", errors: parsed.errors, jobs: [] }
  }
  let lowered = lower_push_workflow_in_workspace(workflow, workspace_root)
  let job_steps : Map[String, Array[PlanStep]] = {}
  let job_order : Array[String] = []
  for task in lowered.plan.tasks {
    if task.kind == "barrier" {
      continue
    }
    if job_steps.get(task.job_id) is None {
      job_steps[task.job_id] = []
      job_order.push(task.job_id)
    }
    let step : PlanStep = {
      id: task.id,
      kind: task.kind,
      name: task.name,
      shell: task.shell,
      script: task.script,
      uses: match task.action {
        Some(action) => action.uses
        None => ""
      },
      backend: match task.action {
        Some(action) => action.backend
        None => ""
      },
      if_condition: task.if_condition,
      env: task.env,
      with_values: task.with_values,
    }
    job_steps.get(task.job_id).unwrap().push(step)
  }
  let jobs : Array[PlanJob] = []
  for job_id in job_order {
    let needs = lowered.plan.job_needs.get(job_id).unwrap_or([])
    let runs_on = lowered.plan.tasks
      .iter()
      .find_first(fn(t) { t.job_id == job_id })
      .map(fn(t) { t.runs_on })
      .unwrap_or([])
    jobs.push({
      id: job_id,
      needs,
      runs_on,
      steps: job_steps.get(job_id).unwrap_or([]),
    })
  }
  let errors = lowered.errors
  for err in parsed.errors {
    errors.push(err)
  }
  { workflow_name: workflow.name, errors, jobs }
}

///|
pub fn plan_result_to_json(result : PlanResult) -> String {
  let parts : Array[String] = []
  parts.push("\"workflow\": " + json_escape_string(result.workflow_name))
  if result.errors.length() > 0 {
    let err_items : Array[String] = []
    for err in result.errors {
      err_items.push(json_escape_string(err))
    }
    parts.push("\"errors\": [" + err_items.join(", ") + "]")
  }
  let job_items : Array[String] = []
  for job in result.jobs {
    let jparts : Array[String] = []
    jparts.push("\"id\": " + json_escape_string(job.id))
    if job.needs.length() > 0 {
      let ns : Array[String] = []
      for n in job.needs {
        ns.push(json_escape_string(n))
      }
      jparts.push("\"needs\": [" + ns.join(", ") + "]")
    }
    if job.runs_on.length() > 0 {
      let rs : Array[String] = []
      for r in job.runs_on {
        rs.push(json_escape_string(r))
      }
      jparts.push("\"runs_on\": [" + rs.join(", ") + "]")
    }
    let step_items : Array[String] = []
    for step in job.steps {
      let sparts : Array[String] = []
      sparts.push("\"id\": " + json_escape_string(step.id))
      sparts.push("\"kind\": " + json_escape_string(step.kind))
      if step.name.length() > 0 {
        sparts.push("\"name\": " + json_escape_string(step.name))
      }
      if step.uses.length() > 0 {
        sparts.push("\"uses\": " + json_escape_string(step.uses))
      }
      if step.backend.length() > 0 {
        sparts.push("\"backend\": " + json_escape_string(step.backend))
      }
      if step.shell.length() > 0 {
        sparts.push("\"shell\": " + json_escape_string(step.shell))
      }
      if step.script.length() > 0 {
        sparts.push("\"script\": " + json_escape_string(step.script))
      }
      if step.if_condition.length() > 0 && step.if_condition != "success()" {
        sparts.push("\"if\": " + json_escape_string(step.if_condition))
      }
      if step.env.length() > 0 {
        sparts.push("\"env\": " + plan_json_string_map(step.env))
      }
      if step.with_values.length() > 0 {
        sparts.push("\"with\": " + plan_json_string_map(step.with_values))
      }
      step_items.push("{ " + sparts.join(", ") + " }")
    }
    jparts.push("\"steps\": [" + step_items.join(", ") + "]")
    job_items.push("{ " + jparts.join(", ") + " }")
  }
  parts.push("\"jobs\": [" + job_items.join(", ") + "]")
  "{ " + parts.join(", ") + " }"
}

///|
fn json_escape_string(s : String) -> String {
  let buf = StringBuilder::new()
  buf.write_char('"')
  for ch in s {
    if ch == '"' {
      buf.write_string("\\\"")
    } else if ch == '\\' {
      buf.write_string("\\\\")
    } else if ch == '\n' {
      buf.write_string("\\n")
    } else if ch == '\r' {
      buf.write_string("\\r")
    } else if ch == '\t' {
      buf.write_string("\\t")
    } else {
      buf.write_char(ch)
    }
  }
  buf.write_char('"')
  buf.to_string()
}

///|
fn plan_json_string_map(values : Map[String, String]) -> String {
  let items : Array[String] = []
  for key, value in values {
    items.push(json_escape_string(key) + ": " + json_escape_string(value))
  }
  "{ " + items.join(", ") + " }"
}