// A runtime template engine — moonctl's faithful equivalent of goctl's
// `text/template`. It parses a template into a node tree once, then renders it
// against a dynamic `Value` (MoonBit has no reflection, so template data is an
// explicit tagged value the way `encoding/json` models it). Supports `{{.Field}}`
// interpolation, `{{if}}/{{else}}/{{range}}/{{with}}/{{end}}`, `$variable`
// assignment, `|` pipelines, parenthesised sub-pipelines, `{{- -}}` whitespace
// trimming, `{{/* comments */}}`, and a library of pipeline functions. Pure and
// all-backend.

///|
/// Dynamic template data. `.Field` lookups resolve against `Dict`; `range`
/// iterates `List` (index/element) or `Dict` (sorted key/value). This is the
/// explicit stand-in for the arbitrary Go values goctl reflects over.
pub(all) enum Value {
  Null
  Bool(Bool)
  Int(Int64)
  Float(Double)
  Str(String)
  List(Array[Value])
  Dict(Map[String, Value])
}

///|
/// A template parse or execution failure, carrying a human-readable message.
pub suberror TemplateError {
  ParseError(String)
  ExecError(String)
}

///|
impl Show for TemplateError with fn output(self, logger) {
  match self {
    ParseError(m) => logger.write_string("template parse error: " + m)
    ExecError(m) => logger.write_string("template exec error: " + m)
  }
}

///|
/// A pipeline function: it receives the already-evaluated argument list (with the
/// piped-in value, if any, appended last, mirroring Go) and returns a `Value`.
pub type FuncFn = (Array[Value]) -> Value raise TemplateError

// ── AST ──────────────────────────────────────────────────────────────────────

///|
priv enum Arg {
  Dot
  Field(Array[String])
  Var(String, Array[String])
  StrLit(String)
  IntLit(Int64)
  FloatLit(Double)
  BoolLit(Bool)
  NilLit
  Sub(Array[Command])
}

///|
priv struct Command {
  fn_ : String?
  args : Array[Arg]
}

///|
priv enum Node {
  Text(String)
  Action(Array[Command])
  Assign(String, Array[Command])
  IfNode(Array[Command], Array[Node], Array[Node])
  RangeNode(Array[Command], String?, String?, Array[Node], Array[Node])
  WithNode(Array[Command], Array[Node], Array[Node])
}

// ── Lexing: raw source → text / action pieces ────────────────────────────────

///|
priv enum Piece {
  PText(String)
  PAction(String)
}

///|
/// Index of the `}}` that closes an action started at `from`, skipping `"…"` and
/// `` `…` `` string literals so a `}}` inside a string is not mistaken for the
/// delimiter. Returns `-1` if unterminated.
fn find_close(src : String, from : Int) -> Int {
  let n = src.length()
  let mut i = from
  while i < n {
    let c = src[i]
    if c == '"' {
      i += 1
      while i < n && src[i] != '"' {
        if src[i] == '\\' {
          i += 2
        } else {
          i += 1
        }
      }
      i += 1
    } else if c == '`' {
      i += 1
      while i < n && src[i] != '`' {
        i += 1
      }
      i += 1
    } else if c == '}' && i + 1 < n && src[i + 1] == '}' {
      return i
    } else {
      i += 1
    }
  }
  -1
}

///|
fn trim_left_ws(s : String) -> String {
  let n = s.length()
  let mut a = 0
  while a < n && is_ws(s[a]) {
    a += 1
  }
  s[a:n].to_owned()
}

///|
fn trim_right_ws(s : String) -> String {
  let mut b = s.length()
  while b > 0 && is_ws(s[b - 1]) {
    b -= 1
  }
  s[0:b].to_owned()
}

///|
/// Split `src` into literal-text and action pieces, honouring `{{- -}}`
/// whitespace-trim markers and dropping `{{/* comments */}}`.
fn scan(src : String) -> Array[Piece] raise TemplateError {
  let pieces : Array[Piece] = []
  let n = src.length()
  let mut i = 0
  let mut text_start = 0
  let mut trim_next_left = false
  while i < n {
    if src[i] == '{' && i + 1 < n && src[i + 1] == '{' {
      let mut body_start = i + 2
      let mut left_trim_prev = false
      if body_start < n &&
        src[body_start] == '-' &&
        body_start + 1 < n &&
        is_ws(src[body_start + 1]) {
        left_trim_prev = true
        body_start += 1
      }
      let mut t = src[text_start:i].to_owned()
      if trim_next_left {
        t = trim_left_ws(t)
      }
      if left_trim_prev {
        t = trim_right_ws(t)
      }
      if t != "" {
        pieces.push(PText(t))
      }
      trim_next_left = false
      let close = find_close(src, body_start)
      if close < 0 {
        raise ParseError("unclosed action, missing '}}'")
      }
      let mut body_end = close
      let mut right_trim_next = false
      if body_end - 1 >= body_start &&
        src[body_end - 1] == '-' &&
        body_end - 2 >= body_start &&
        is_ws(src[body_end - 2]) {
        right_trim_next = true
        body_end -= 1
      }
      let body = trim(src[body_start:body_end].to_owned())
      if starts_with(body, "/*") {
        // comment — emit nothing
        ()
      } else {
        pieces.push(PAction(body))
      }
      trim_next_left = right_trim_next
      i = close + 2
      text_start = i
    } else {
      i += 1
    }
  }
  let mut t = src[text_start:n].to_owned()
  if trim_next_left {
    t = trim_left_ws(t)
  }
  if t != "" {
    pieces.push(PText(t))
  }
  pieces
}

// ── Lexing: action body → tokens ─────────────────────────────────────────────

///|
priv enum Tok {
  TWord(String)
  TStr(String)
  TPipe
  TLparen
  TRparen
  TAssign
  TEq
  TComma
}

///|
fn is_digit(c : UInt16) -> Bool {
  c >= '0' && c <= '9'
}

///|
/// Tokenise one action body: identifiers/fields/variables/numbers become
/// `TWord`, quoted and raw strings become `TStr`, and `|`, `(`, `)`, `,`, `:=`,
/// `=` become their own tokens.
fn lex_action(body : String) -> Array[Tok] raise TemplateError {
  let toks : Array[Tok] = []
  let n = body.length()
  let mut i = 0
  while i < n {
    let c = body[i]
    if is_ws(c) {
      i += 1
    } else if c == '|' {
      toks.push(TPipe)
      i += 1
    } else if c == '(' {
      toks.push(TLparen)
      i += 1
    } else if c == ')' {
      toks.push(TRparen)
      i += 1
    } else if c == ',' {
      toks.push(TComma)
      i += 1
    } else if c == ':' && i + 1 < n && body[i + 1] == '=' {
      toks.push(TAssign)
      i += 2
    } else if c == '=' {
      toks.push(TEq)
      i += 1
    } else if c == '"' {
      i += 1
      let sb = StringBuilder::new()
      let mut done = false
      while !done {
        if i >= n {
          raise ParseError("unterminated string literal")
        }
        let ch = body[i]
        if ch == '"' {
          i += 1
          done = true
        } else if ch == '\\' && i + 1 < n {
          let e = body[i + 1]
          let code = if e == 'n' {
            10
          } else if e == 't' {
            9
          } else if e == 'r' {
            13
          } else {
            e.to_int()
          }
          sb.write_char(code.unsafe_to_char())
          i += 2
        } else {
          sb.write_char(ch.to_int().unsafe_to_char())
          i += 1
        }
      }
      toks.push(TStr(sb.to_string()))
    } else if c == '`' {
      i += 1
      let start = i
      while i < n && body[i] != '`' {
        i += 1
      }
      if i >= n {
        raise ParseError("unterminated raw string literal")
      }
      toks.push(TStr(body[start:i].to_owned()))
      i += 1
    } else {
      let start = i
      while i < n {
        let d = body[i]
        if is_ws(d) ||
          d == '|' ||
          d == '(' ||
          d == ')' ||
          d == ',' ||
          d == '"' ||
          d == '`' ||
          d == '=' ||
          (d == ':' && i + 1 < n && body[i + 1] == '=') {
          break
        }
        i += 1
      }
      toks.push(TWord(body[start:i].to_owned()))
    }
  }
  toks
}

// ── Parsing: tokens → commands ───────────────────────────────────────────────

///|
/// Split `s` on `ch` (no quote handling; for `.a.b` field chains and `$var`
/// names).
fn split_char(s : String, ch : UInt16) -> Array[String] {
  let out : Array[String] = []
  let n = s.length()
  let mut start = 0
  for i = 0; i <= n; i = i + 1 {
    if i == n || s[i] == ch {
      out.push(s[start:i].to_owned())
      start = i + 1
    }
  }
  out
}

///|
/// True when `w` is a value token (field, variable, literal, number) rather than
/// a bare function-name identifier.
fn is_value_word(w : String) -> Bool {
  if w.length() == 0 {
    return false
  }
  let c0 = w[0]
  if c0 == '.' || c0 == '$' {
    return true
  }
  if w == "true" || w == "false" || w == "nil" {
    return true
  }
  if is_digit(c0) {
    return true
  }
  if (c0 == '-' || c0 == '+') && w.length() > 1 && is_digit(w[1]) {
    return true
  }
  false
}

///|
fn parse_int_manual(w : String) -> Int64 raise TemplateError {
  let n = w.length()
  let mut i = 0
  let mut neg = false
  if n > 0 && (w[0] == '-' || w[0] == '+') {
    neg = w[0] == '-'
    i = 1
  }
  if i >= n {
    raise ParseError("malformed number '" + w + "'")
  }
  let mut v : Int64 = 0
  while i < n {
    let c = w[i]
    if !is_digit(c) {
      raise ParseError("malformed integer '" + w + "'")
    }
    v = v * 10L + (c.to_int() - 48).to_int64()
    i += 1
  }
  if neg {
    -v
  } else {
    v
  }
}

///|
fn parse_double_manual(w : String) -> Double raise TemplateError {
  let n = w.length()
  let mut i = 0
  let mut neg = false
  if n > 0 && (w[0] == '-' || w[0] == '+') {
    neg = w[0] == '-'
    i = 1
  }
  let mut int_part = 0.0
  let mut saw_digit = false
  while i < n && is_digit(w[i]) {
    int_part = int_part * 10.0 + (w[i].to_int() - 48).to_double()
    saw_digit = true
    i += 1
  }
  let mut frac = 0.0
  let mut scale = 1.0
  if i < n && w[i] == '.' {
    i += 1
    while i < n && is_digit(w[i]) {
      scale = scale / 10.0
      frac = frac + (w[i].to_int() - 48).to_double() * scale
      saw_digit = true
      i += 1
    }
  }
  let mut value = int_part + frac
  if i < n && (w[i] == 'e' || w[i] == 'E') {
    i += 1
    let mut exp_neg = false
    if i < n && (w[i] == '-' || w[i] == '+') {
      exp_neg = w[i] == '-'
      i += 1
    }
    let mut exp = 0
    while i < n && is_digit(w[i]) {
      exp = exp * 10 + (w[i].to_int() - 48)
      i += 1
    }
    let mut factor = 1.0
    for _k = 0; _k < exp; _k = _k + 1 {
      factor = factor * 10.0
    }
    value = if exp_neg { value / factor } else { value * factor }
  }
  if i != n || !saw_digit {
    raise ParseError("malformed number '" + w + "'")
  }
  if neg {
    -value
  } else {
    value
  }
}

///|
fn parse_number(w : String) -> Arg raise TemplateError {
  let mut is_float = false
  for i = 0; i < w.length(); i = i + 1 {
    let c = w[i]
    if c == '.' || c == 'e' || c == 'E' {
      is_float = true
    }
  }
  if is_float {
    FloatLit(parse_double_manual(w))
  } else {
    IntLit(parse_int_manual(w))
  }
}

///|
/// Classify a value word into an `Arg`; raises when it is actually a bare
/// function name (only valid as the first word of a command).
fn classify_value(w : String) -> Arg raise TemplateError {
  if w.length() == 0 {
    raise ParseError("empty token")
  }
  if w == "." {
    return Dot
  }
  let c0 = w[0]
  if c0 == '.' {
    let parts = split_char(w[1:].to_owned(), '.')
    let chain : Array[String] = []
    for p in parts {
      if p != "" {
        chain.push(p)
      }
    }
    return Field(chain)
  }
  if w == "$" {
    return Var("", [])
  }
  if c0 == '$' {
    let parts = split_char(w[1:].to_owned(), '.')
    let chain : Array[String] = []
    for i = 1; i < parts.length(); i = i + 1 {
      if parts[i] != "" {
        chain.push(parts[i])
      }
    }
    return Var(parts[0], chain)
  }
  if w == "true" {
    return BoolLit(true)
  }
  if w == "false" {
    return BoolLit(false)
  }
  if w == "nil" {
    return NilLit
  }
  if is_digit(c0) ||
    ((c0 == '-' || c0 == '+') && w.length() > 1 && is_digit(w[1])) {
    return parse_number(w)
  }
  raise ParseError(
    "unexpected identifier '" +
    w +
    "'; a function name is only valid as the first word of a command",
  )
}

///|
priv struct TokCursor {
  toks : Array[Tok]
  mut pos : Int
}

///|
fn TokCursor::expect_rparen(self : TokCursor) -> Unit raise TemplateError {
  if self.pos < self.toks.length() {
    match self.toks[self.pos] {
      TRparen => self.pos += 1
      _ => raise ParseError("expected ')'")
    }
  } else {
    raise ParseError("expected ')'")
  }
}

///|
fn TokCursor::parse_command(self : TokCursor) -> Command raise TemplateError {
  if self.pos >= self.toks.length() {
    raise ParseError("expected a command")
  }
  match self.toks[self.pos] {
    TStr(s) => {
      self.pos += 1
      { fn_: None, args: [StrLit(s)] }
    }
    TLparen => {
      self.pos += 1
      let sub = self.parse_pipeline()
      self.expect_rparen()
      { fn_: None, args: [Sub(sub)] }
    }
    TWord(w) => {
      self.pos += 1
      if is_value_word(w) {
        { fn_: None, args: [classify_value(w)] }
      } else {
        let args : Array[Arg] = []
        let mut stop = false
        while !stop {
          if self.pos >= self.toks.length() {
            break
          }
          match self.toks[self.pos] {
            TPipe => stop = true
            TRparen => stop = true
            TStr(s) => {
              args.push(StrLit(s))
              self.pos += 1
            }
            TLparen => {
              self.pos += 1
              let sub = self.parse_pipeline()
              self.expect_rparen()
              args.push(Sub(sub))
            }
            TWord(aw) => {
              args.push(classify_value(aw))
              self.pos += 1
            }
            TComma => raise ParseError("unexpected ',' in arguments")
            TAssign => raise ParseError("unexpected ':=' in arguments")
            TEq => raise ParseError("unexpected '=' in arguments")
          }
        }
        { fn_: Some(w), args }
      }
    }
    _ => raise ParseError("unexpected token; expected a command")
  }
}

///|
fn TokCursor::parse_pipeline(
  self : TokCursor,
) -> Array[Command] raise TemplateError {
  let cmds : Array[Command] = []
  for ;; {
    cmds.push(self.parse_command())
    if self.pos < self.toks.length() {
      match self.toks[self.pos] {
        TPipe => self.pos += 1
        _ => break
      }
    } else {
      break
    }
  }
  cmds
}

///|
/// Parse the pipeline that starts at token `start`, requiring every token to be
/// consumed.
fn pipeline_from(
  toks : Array[Tok],
  start : Int,
) -> Array[Command] raise TemplateError {
  let tc = { toks, pos: start }
  let cmds = tc.parse_pipeline()
  if tc.pos != toks.length() {
    raise ParseError("unexpected trailing tokens in action")
  }
  cmds
}

// ── Parsing: pieces → node tree ──────────────────────────────────────────────

///|
priv struct Parser {
  pieces : Array[Piece]
  mut pos : Int
}

///|
fn first_word(toks : Array[Tok]) -> String {
  if toks.length() == 0 {
    return ""
  }
  match toks[0] {
    TWord(w) => w
    _ => ""
  }
}

///|
fn is_word(t : Tok, s : String) -> Bool {
  match t {
    TWord(w) => w == s
    _ => false
  }
}

///|
fn find_tassign(toks : Array[Tok]) -> Int {
  for i = 0; i < toks.length(); i = i + 1 {
    match toks[i] {
      TAssign => return i
      _ => ()
    }
  }
  -1
}

///|
fn is_assignment(toks : Array[Tok]) -> Bool {
  if toks.length() < 2 {
    return false
  }
  let starts_var = match toks[0] {
    TWord(w) => w.length() > 0 && w[0] == '$'
    _ => false
  }
  if !starts_var {
    return false
  }
  match toks[1] {
    TAssign => true
    TEq => true
    _ => false
  }
}

///|
fn build_assign(toks : Array[Tok]) -> Node raise TemplateError {
  let name = match toks[0] {
    TWord(w) => w[1:].to_owned()
    _ => raise ParseError("malformed variable assignment")
  }
  Assign(name, pipeline_from(toks, 2))
}

///|
fn drop_first(toks : Array[Tok]) -> Array[Tok] {
  let out : Array[Tok] = []
  for i = 1; i < toks.length(); i = i + 1 {
    out.push(toks[i])
  }
  out
}

///|
fn Parser::parse_if(
  self : Parser,
  toks : Array[Tok],
) -> Node raise TemplateError {
  let pipe = pipeline_from(toks, 1)
  let (then_nodes, term) = self.parse_list()
  let tterm = lex_action(term)
  let kw = first_word(tterm)
  let else_nodes : Array[Node] = if kw == "end" {
    []
  } else if kw == "else" {
    if tterm.length() >= 2 && is_word(tterm[1], "if") {
      [self.parse_if(drop_first(tterm))]
    } else {
      let (e, term2) = self.parse_list()
      if first_word(lex_action(term2)) != "end" {
        raise ParseError("expected {{end}} to close {{if}}")
      }
      e
    }
  } else {
    raise ParseError("expected {{else}} or {{end}} to close {{if}}")
  }
  IfNode(pipe, then_nodes, else_nodes)
}

///|
fn Parser::parse_range(
  self : Parser,
  toks : Array[Tok],
) -> Node raise TemplateError {
  let assign_idx = find_tassign(toks)
  let mut key_var : String? = None
  let mut val_var : String? = None
  let pipe = if assign_idx >= 0 {
    let names : Array[String] = []
    for i = 1; i < assign_idx; i = i + 1 {
      match toks[i] {
        TWord(w) =>
          if w.length() > 0 && w[0] == '$' {
            names.push(w[1:].to_owned())
          } else {
            raise ParseError("range: expected a $variable")
          }
        TComma => ()
        _ => raise ParseError("range: malformed variable list")
      }
    }
    if names.length() == 1 {
      val_var = Some(names[0])
    } else if names.length() == 2 {
      key_var = Some(names[0])
      val_var = Some(names[1])
    } else {
      raise ParseError("range: expected 1 or 2 variables before ':='")
    }
    pipeline_from(toks, assign_idx + 1)
  } else {
    pipeline_from(toks, 1)
  }
  let (body, term) = self.parse_list()
  let kw = first_word(lex_action(term))
  let els : Array[Node] = if kw == "end" {
    []
  } else if kw == "else" {
    let (e, term2) = self.parse_list()
    if first_word(lex_action(term2)) != "end" {
      raise ParseError("range: expected {{end}}")
    }
    e
  } else {
    raise ParseError("range: expected {{else}} or {{end}}")
  }
  RangeNode(pipe, key_var, val_var, body, els)
}

///|
fn Parser::parse_with(
  self : Parser,
  toks : Array[Tok],
) -> Node raise TemplateError {
  let pipe = pipeline_from(toks, 1)
  let (body, term) = self.parse_list()
  let kw = first_word(lex_action(term))
  let els : Array[Node] = if kw == "end" {
    []
  } else if kw == "else" {
    let (e, term2) = self.parse_list()
    if first_word(lex_action(term2)) != "end" {
      raise ParseError("with: expected {{end}}")
    }
    e
  } else {
    raise ParseError("with: expected {{else}} or {{end}}")
  }
  WithNode(pipe, body, els)
}

///|
/// Parse a node list until a `{{end}}` or `{{else}}` (returned as the terminator
/// body) or end of input (empty terminator).
fn Parser::parse_list(
  self : Parser,
) -> (Array[Node], String) raise TemplateError {
  let nodes : Array[Node] = []
  for ;; {
    if self.pos >= self.pieces.length() {
      return (nodes, "")
    }
    match self.pieces[self.pos] {
      PText(t) => {
        nodes.push(Text(t))
        self.pos += 1
      }
      PAction(body) => {
        let toks = lex_action(body)
        let kw = first_word(toks)
        if kw == "end" || kw == "else" {
          self.pos += 1
          return (nodes, body)
        } else if kw == "if" {
          self.pos += 1
          nodes.push(self.parse_if(toks))
        } else if kw == "range" {
          self.pos += 1
          nodes.push(self.parse_range(toks))
        } else if kw == "with" {
          self.pos += 1
          nodes.push(self.parse_with(toks))
        } else if is_assignment(toks) {
          self.pos += 1
          nodes.push(build_assign(toks))
        } else {
          self.pos += 1
          nodes.push(Action(pipeline_from(toks, 0)))
        }
      }
    }
  }
}

// ── Execution ────────────────────────────────────────────────────────────────

///|
priv struct Env {
  root : Value
  funcs : Map[String, FuncFn]
}

///|
/// Go's notion of truth: false, zero, empty string/list/dict and nil are falsy.
fn truthy(v : Value) -> Bool {
  match v {
    Null => false
    Bool(b) => b
    Int(n) => n != 0L
    Float(f) => f != 0.0
    Str(s) => s.length() != 0
    List(xs) => xs.length() != 0
    Dict(m) => m.length() != 0
  }
}

///|
fn str_cmp(a : String, b : String) -> Int {
  let la = a.length()
  let lb = b.length()
  let mut i = 0
  while i < la && i < lb {
    let ca = a[i].to_int()
    let cb = b[i].to_int()
    if ca != cb {
      return if ca < cb { -1 } else { 1 }
    }
    i += 1
  }
  if la == lb {
    0
  } else if la < lb {
    -1
  } else {
    1
  }
}

///|
fn sorted_keys(m : Map[String, Value]) -> Array[String] {
  let keys : Array[String] = []
  for k, _ in m {
    keys.push(k)
  }
  for i = 1; i < keys.length(); i = i + 1 {
    let key = keys[i]
    let mut j = i - 1
    while j >= 0 && str_cmp(keys[j], key) > 0 {
      keys[j + 1] = keys[j]
      j -= 1
    }
    keys[j + 1] = key
  }
  keys
}

///|
/// Render a `Value` to its output string, as Go's template does when a pipeline
/// value is printed. A `Null` (including a missing key) prints as the empty
/// string.
fn to_output(v : Value) -> String {
  match v {
    Null => ""
    Bool(b) => if b { "true" } else { "false" }
    Int(n) => n.to_string()
    Float(f) => f.to_string()
    Str(s) => s
    List(xs) => {
      let sb = StringBuilder::new()
      sb.write_string("[")
      for i = 0; i < xs.length(); i = i + 1 {
        if i > 0 {
          sb.write_string(" ")
        }
        sb.write_string(to_output(xs[i]))
      }
      sb.write_string("]")
      sb.to_string()
    }
    Dict(m) => {
      let sb = StringBuilder::new()
      sb.write_string("map[")
      let keys = sorted_keys(m)
      for i = 0; i < keys.length(); i = i + 1 {
        if i > 0 {
          sb.write_string(" ")
        }
        sb.write_string(keys[i])
        sb.write_string(":")
        sb.write_string(to_output(m.get(keys[i]).unwrap()))
      }
      sb.write_string("]")
      sb.to_string()
    }
  }
}

///|
fn follow(v : Value, chain : Array[String]) -> Value raise TemplateError {
  let mut cur = v
  for f in chain {
    cur = match cur {
      Dict(m) =>
        match m.get(f) {
          Some(x) => x
          None => Null
        }
      Null => Null
      _ =>
        raise ExecError("cannot access field '" + f + "' of a non-object value")
    }
  }
  cur
}

///|
fn eval_arg(
  env : Env,
  a : Arg,
  dot : Value,
  vars : Map[String, Value],
) -> Value raise TemplateError {
  match a {
    Dot => dot
    Field(chain) => follow(dot, chain)
    Var(name, chain) => {
      let base = if name == "" {
        env.root
      } else {
        match vars.get(name) {
          Some(v) => v
          None => raise ExecError("undefined variable $" + name)
        }
      }
      follow(base, chain)
    }
    StrLit(s) => Str(s)
    IntLit(n) => Int(n)
    FloatLit(f) => Float(f)
    BoolLit(b) => Bool(b)
    NilLit => Null
    Sub(cmds) => eval_pipeline(env, cmds, dot, vars)
  }
}

///|
fn eval_command(
  env : Env,
  cmd : Command,
  dot : Value,
  vars : Map[String, Value],
  piped : Value?,
) -> Value raise TemplateError {
  let args : Array[Value] = []
  for a in cmd.args {
    args.push(eval_arg(env, a, dot, vars))
  }
  match cmd.fn_ {
    Some(name) => {
      match piped {
        Some(p) => args.push(p)
        None => ()
      }
      match env.funcs.get(name) {
        Some(f) => f(args)
        None => raise ExecError("function '" + name + "' is not defined")
      }
    }
    None => {
      match piped {
        Some(_) => raise ExecError("cannot pipe a value into a non-function")
        None => ()
      }
      if args.length() != 1 {
        raise ExecError("value command must have exactly one argument")
      }
      args[0]
    }
  }
}

///|
fn eval_pipeline(
  env : Env,
  cmds : Array[Command],
  dot : Value,
  vars : Map[String, Value],
) -> Value raise TemplateError {
  let mut acc : Value? = None
  for cmd in cmds {
    acc = Some(eval_command(env, cmd, dot, vars, acc))
  }
  match acc {
    Some(v) => v
    None => raise ExecError("empty pipeline")
  }
}

///|
fn render_nodes(
  env : Env,
  nodes : Array[Node],
  dot : Value,
  vars : Map[String, Value],
  sb : StringBuilder,
) -> Unit raise TemplateError {
  for node in nodes {
    match node {
      Text(t) => sb.write_string(t)
      Action(cmds) =>
        sb.write_string(to_output(eval_pipeline(env, cmds, dot, vars)))
      Assign(name, cmds) => vars[name] = eval_pipeline(env, cmds, dot, vars)
      IfNode(cmds, then_nodes, else_nodes) => {
        let v = eval_pipeline(env, cmds, dot, vars)
        let child = vars.copy()
        if truthy(v) {
          render_nodes(env, then_nodes, dot, child, sb)
        } else {
          render_nodes(env, else_nodes, dot, child, sb)
        }
      }
      WithNode(cmds, body, else_nodes) => {
        let v = eval_pipeline(env, cmds, dot, vars)
        let child = vars.copy()
        if truthy(v) {
          render_nodes(env, body, v, child, sb)
        } else {
          render_nodes(env, else_nodes, dot, child, sb)
        }
      }
      RangeNode(cmds, key_var, val_var, body, else_nodes) => {
        let v = eval_pipeline(env, cmds, dot, vars)
        match v {
          List(xs) =>
            if xs.length() == 0 {
              render_nodes(env, else_nodes, dot, vars.copy(), sb)
            } else {
              for i = 0; i < xs.length(); i = i + 1 {
                let child = vars.copy()
                match key_var {
                  Some(k) => child[k] = Int(i.to_int64())
                  None => ()
                }
                match val_var {
                  Some(nm) => child[nm] = xs[i]
                  None => ()
                }
                render_nodes(env, body, xs[i], child, sb)
              }
            }
          Dict(m) => {
            let keys = sorted_keys(m)
            if keys.length() == 0 {
              render_nodes(env, else_nodes, dot, vars.copy(), sb)
            } else {
              for key in keys {
                let value = m.get(key).unwrap()
                let child = vars.copy()
                match key_var {
                  Some(k) => child[k] = Str(key)
                  None => ()
                }
                match val_var {
                  Some(nm) => child[nm] = value
                  None => ()
                }
                render_nodes(env, body, value, child, sb)
              }
            }
          }
          Null => render_nodes(env, else_nodes, dot, vars.copy(), sb)
          _ =>
            raise ExecError("range: cannot iterate over a non-list/dict value")
        }
      }
    }
  }
}

// ── Pipeline function library ────────────────────────────────────────────────

///|
fn arg_at(
  xs : Array[Value],
  i : Int,
  name : String,
) -> Value raise TemplateError {
  if i >= xs.length() {
    raise ExecError(name + ": missing argument")
  }
  xs[i]
}

///|
fn ends_with(s : String, suf : String) -> Bool {
  let sn = s.length()
  let fn_ = suf.length()
  if sn < fn_ {
    return false
  }
  for i = 0; i < fn_; i = i + 1 {
    if s[sn - fn_ + i] != suf[i] {
      return false
    }
  }
  true
}

///|
fn str_contains(hay : String, needle : String) -> Bool {
  let hn = hay.length()
  let nn = needle.length()
  if nn == 0 {
    return true
  }
  for i = 0; i + nn <= hn; i = i + 1 {
    let mut ok = true
    for j = 0; j < nn; j = j + 1 {
      if hay[i + j] != needle[j] {
        ok = false
        break
      }
    }
    if ok {
      return true
    }
  }
  false
}

///|
fn str_replace(s : String, old : String, new_ : String) -> String {
  if old.length() == 0 {
    return s
  }
  let sb = StringBuilder::new()
  let sn = s.length()
  let on = old.length()
  let mut i = 0
  while i < sn {
    if i + on <= sn {
      let mut ok = true
      for j = 0; j < on; j = j + 1 {
        if s[i + j] != old[j] {
          ok = false
          break
        }
      }
      if ok {
        sb.write_string(new_)
        i += on
        continue
      }
    }
    sb.write_char(s[i].to_int().unsafe_to_char())
    i += 1
  }
  sb.to_string()
}

///|
fn join_strings(parts : Array[String], sep : String) -> String {
  let sb = StringBuilder::new()
  for i = 0; i < parts.length(); i = i + 1 {
    if i > 0 {
      sb.write_string(sep)
    }
    sb.write_string(parts[i])
  }
  sb.to_string()
}

///|
fn title_case(s : String) -> String {
  let words = split_char(s, ' ')
  let out : Array[String] = []
  for w in words {
    if w.length() == 0 {
      out.push(w)
    } else {
      out.push(w[0:1].to_owned().to_upper() + w[1:].to_owned().to_lower())
    }
  }
  join_strings(out, " ")
}

///|
fn val_num(v : Value) -> Double? {
  match v {
    Int(n) => Some(n.to_double())
    Float(f) => Some(f)
    _ => None
  }
}

///|
fn val_eq(a : Value, b : Value) -> Bool {
  match (a, b) {
    (Null, Null) => true
    (Bool(x), Bool(y)) => x == y
    (Str(x), Str(y)) => x == y
    (Int(x), Int(y)) => x == y
    (Float(x), Float(y)) => x == y
    (Int(x), Float(y)) => x.to_double() == y
    (Float(x), Int(y)) => x == y.to_double()
    _ => false
  }
}

///|
fn val_cmp(a : Value, b : Value) -> Int raise TemplateError {
  match (a, b) {
    (Str(x), Str(y)) => str_cmp(x, y)
    _ =>
      match (val_num(a), val_num(b)) {
        (Some(x), Some(y)) => if x < y { -1 } else if x > y { 1 } else { 0 }
        _ =>
          raise ExecError(
            "comparison requires two numbers or two strings of the same kind",
          )
      }
  }
}

///|
fn sprintf(fmt : String, args : Array[Value], start : Int) -> String {
  let sb = StringBuilder::new()
  let mut ai = start
  let n = fmt.length()
  let mut i = 0
  while i < n {
    let c = fmt[i]
    if c == '%' && i + 1 < n {
      let v = fmt[i + 1]
      if v == '%' {
        sb.write_char('%')
        i += 2
      } else {
        let a = if ai < args.length() { args[ai] } else { Null }
        match v {
          's' | 'v' | 'd' => sb.write_string(to_output(a))
          't' =>
            sb.write_string(
              match a {
                Bool(b) => if b { "true" } else { "false" }
                _ => to_output(a)
              },
            )
          'q' => sb.write_string("\"" + to_output(a) + "\"")
          _ => {
            sb.write_char('%')
            sb.write_char(v.to_int().unsafe_to_char())
            ai -= 1
          }
        }
        ai += 1
        i += 2
      }
    } else {
      sb.write_char(c.to_int().unsafe_to_char())
      i += 1
    }
  }
  sb.to_string()
}

///|
/// The default function library: string helpers (`upper`, `lower`, `title`,
/// `trim`, `trimPrefix`, `trimSuffix`, `hasPrefix`, `hasSuffix`, `contains`,
/// `replace`), comparisons (`eq`, `ne`, `lt`, `le`, `gt`, `ge`), logic (`and`,
/// `or`, `not`), and `len`, `index`, `default`, `printf`, `print`, `println`.
/// String helpers put the operated-on value last so `{{.Name | trimPrefix "Get"}}`
/// pipelines read naturally.
fn default_funcs() -> Map[String, FuncFn] {
  let m : Map[String, FuncFn] = Map([])
  m["upper"] = xs => Str(to_output(arg_at(xs, 0, "upper")).to_upper())
  m["lower"] = xs => Str(to_output(arg_at(xs, 0, "lower")).to_lower())
  m["title"] = xs => Str(title_case(to_output(arg_at(xs, 0, "title"))))
  m["trim"] = xs => Str(trim(to_output(arg_at(xs, 0, "trim"))))
  m["trimPrefix"] = xs => {
    let prefix = to_output(arg_at(xs, 0, "trimPrefix"))
    let s = to_output(arg_at(xs, 1, "trimPrefix"))
    Str(if starts_with(s, prefix) { s[prefix.length():].to_owned() } else { s })
  }
  m["trimSuffix"] = xs => {
    let suffix = to_output(arg_at(xs, 0, "trimSuffix"))
    let s = to_output(arg_at(xs, 1, "trimSuffix"))
    Str(
      if ends_with(s, suffix) {
        s[0:s.length() - suffix.length()].to_owned()
      } else {
        s
      },
    )
  }
  m["hasPrefix"] = xs => {
    Bool(
      starts_with(
        to_output(arg_at(xs, 1, "hasPrefix")),
        to_output(arg_at(xs, 0, "hasPrefix")),
      ),
    )
  }
  m["hasSuffix"] = xs => {
    Bool(
      ends_with(
        to_output(arg_at(xs, 1, "hasSuffix")),
        to_output(arg_at(xs, 0, "hasSuffix")),
      ),
    )
  }
  m["contains"] = xs => {
    Bool(
      str_contains(
        to_output(arg_at(xs, 1, "contains")),
        to_output(arg_at(xs, 0, "contains")),
      ),
    )
  }
  m["replace"] = xs => {
    Str(
      str_replace(
        to_output(arg_at(xs, 2, "replace")),
        to_output(arg_at(xs, 0, "replace")),
        to_output(arg_at(xs, 1, "replace")),
      ),
    )
  }
  m["eq"] = xs => {
    if xs.length() < 2 {
      raise ExecError("eq: needs at least two arguments")
    }
    let a = xs[0]
    let mut found = false
    for i = 1; i < xs.length(); i = i + 1 {
      if val_eq(a, xs[i]) {
        found = true
      }
    }
    Bool(found)
  }
  m["ne"] = xs => Bool(!val_eq(arg_at(xs, 0, "ne"), arg_at(xs, 1, "ne")))
  m["lt"] = xs => Bool(val_cmp(arg_at(xs, 0, "lt"), arg_at(xs, 1, "lt")) < 0)
  m["le"] = xs => Bool(val_cmp(arg_at(xs, 0, "le"), arg_at(xs, 1, "le")) <= 0)
  m["gt"] = xs => Bool(val_cmp(arg_at(xs, 0, "gt"), arg_at(xs, 1, "gt")) > 0)
  m["ge"] = xs => Bool(val_cmp(arg_at(xs, 0, "ge"), arg_at(xs, 1, "ge")) >= 0)
  m["and"] = xs => {
    if xs.length() == 0 {
      raise ExecError("and: needs at least one argument")
    }
    let mut result = xs[0]
    let mut done = false
    for x in xs {
      if !done {
        result = x
        if !truthy(x) {
          done = true
        }
      }
    }
    result
  }
  m["or"] = xs => {
    if xs.length() == 0 {
      raise ExecError("or: needs at least one argument")
    }
    let mut result = xs[0]
    let mut done = false
    for x in xs {
      if !done {
        result = x
        if truthy(x) {
          done = true
        }
      }
    }
    result
  }
  m["not"] = xs => Bool(!truthy(arg_at(xs, 0, "not")))
  m["len"] = xs => {
    let v = arg_at(xs, 0, "len")
    let n = match v {
      Str(s) => s.length()
      List(l) => l.length()
      Dict(d) => d.length()
      Null => 0
      _ => raise ExecError("len: value has no length")
    }
    Int(n.to_int64())
  }
  m["index"] = xs => {
    if xs.length() < 1 {
      raise ExecError("index: needs at least one argument")
    }
    let mut cur = xs[0]
    for i = 1; i < xs.length(); i = i + 1 {
      let k = xs[i]
      cur = match cur {
        List(l) =>
          match k {
            Int(n) =>
              if n >= 0L && n < l.length().to_int64() {
                l[n.to_int()]
              } else {
                Null
              }
            _ => raise ExecError("index: list index must be an integer")
          }
        Dict(d) =>
          match k {
            Str(s) =>
              match d.get(s) {
                Some(x) => x
                None => Null
              }
            _ => raise ExecError("index: map key must be a string")
          }
        _ => raise ExecError("index: value cannot be indexed")
      }
    }
    cur
  }
  m["default"] = xs => {
    let d = arg_at(xs, 0, "default")
    let v = arg_at(xs, 1, "default")
    if truthy(v) {
      v
    } else {
      d
    }
  }
  m["printf"] = xs => Str(sprintf(to_output(arg_at(xs, 0, "printf")), xs, 1))
  m["print"] = xs => {
    let sb = StringBuilder::new()
    for x in xs {
      sb.write_string(to_output(x))
    }
    Str(sb.to_string())
  }
  m["println"] = xs => {
    let sb = StringBuilder::new()
    for x in xs {
      sb.write_string(to_output(x))
    }
    sb.write_string("\n")
    Str(sb.to_string())
  }
  m
}

// ── Public API ───────────────────────────────────────────────────────────────

///|
/// A parsed, reusable template — moonctl's `text/template` equivalent. Build it
/// with `Template::new`, register extra functions with `func`, parse a source
/// with `parse`, then `render` it against a `Value` any number of times. The
/// parsed node tree stays private, captured inside `renderer`.
pub struct Template {
  mut renderer : (Value) -> String raise TemplateError
  funcs : Map[String, FuncFn]
}

///|
/// A fresh template preloaded with the default function library. Rendering before
/// `parse` yields the empty string.
pub fn Template::new() -> Template {
  { renderer: _data => "", funcs: default_funcs() }
}

///|
/// Register (or override) a pipeline function, returning `self` for chaining —
/// the analogue of Go's `Template.Funcs`. Register before `parse`.
pub fn Template::func(self : Template, name : String, f : FuncFn) -> Template {
  self.funcs[name] = f
  self
}

///|
/// Parse `source` into this template's node tree, returning `self` for chaining.
/// Raises `ParseError` on a syntax error (unclosed action, dangling `{{end}}`, …).
pub fn Template::parse(
  self : Template,
  source : String,
) -> Template raise TemplateError {
  let parser : Parser = { pieces: scan(source), pos: 0 }
  let (nodes, term) = parser.parse_list()
  if term != "" {
    raise ParseError(
      "unexpected {{" + term + "}} with no matching block opener",
    )
  }
  let funcs = self.funcs
  self.renderer = data => {
    let env : Env = { root: data, funcs }
    let sb = StringBuilder::new()
    render_nodes(env, nodes, data, Map([]), sb)
    sb.to_string()
  }
  self
}

///|
/// Render the template against `data`, producing the output string. Raises
/// `ExecError` on an execution fault (undefined variable/function, bad range
/// target, …).
pub fn Template::render(
  self : Template,
  data : Value,
) -> String raise TemplateError {
  (self.renderer)(data)
}

///|
/// Parse and render `source` against `data` in one call, with the default
/// function library.
pub fn render(source : String, data : Value) -> String raise TemplateError {
  Template::new().parse(source).render(data)
}

///|
/// Expose a parsed `.api` `Spec` as template `Value` data: a `Dict` with
/// `service` (string), `routes` (list of `{verb, path, handler, summary,
/// hasSummary}`), `types` (list of `{name, fields:[{name, type}]}`) and
/// `hasTypes` (bool). This is what the template-driven generator ranges over.
pub fn spec_to_value(spec : Spec) -> Value {
  let routes : Array[Value] = []
  for r in spec.routes {
    routes.push(
      Dict(
        Map([
          ("verb", Str(r.verb)),
          ("path", Str(r.path)),
          ("handler", Str(r.handler)),
          ("summary", Str(r.summary)),
          ("hasSummary", Bool(r.summary != "")),
        ]),
      ),
    )
  }
  let types : Array[Value] = []
  for t in spec.types {
    let fields : Array[Value] = []
    for f in t.fields {
      fields.push(Dict(Map([("name", Str(f.name)), ("type", Str(f.type_))])))
    }
    types.push(Dict(Map([("name", Str(t.name)), ("fields", List(fields))])))
  }
  Dict(
    Map([
      ("service", Str(spec.service)),
      ("routes", List(routes)),
      ("types", List(types)),
      ("hasTypes", Bool(spec.types.length() != 0)),
    ]),
  )
}

///|
/// Generate code from a `Spec` with a caller-supplied template instead of the
/// built-in generator — the runtime-template path goctl exposes for custom
/// scaffolds. Equivalent to `render(template_source, spec_to_value(spec))`.
pub fn generate_with(
  spec : Spec,
  template_source : String,
) -> String raise TemplateError {
  render(template_source, spec_to_value(spec))
}