// Cedar AST → source text (stringify / marshal).
// Inverse of parsing: takes AST nodes and produces valid Cedar source.
// Precedence-based parenthesization keeps output minimal.
// Reference: cedar-go/internal/parser/cedar_marshal.go

// ---------------------------------------------------------------------------
// Precedence levels (higher = binds tighter)
// ---------------------------------------------------------------------------

///|
fn expr_prec(expr : @ast.Expr) -> Int {
  match expr {
    @ast.If(_, _, _) => 1
    @ast.Or(_, _) => 2
    @ast.And(_, _) => 3
    @ast.HasAttr(_, _)
    | @ast.Like(_, _)
    | @ast.Is(_, _)
    | @ast.GetTag(_, _)
    | @ast.HasTag(_, _) => 4
    @ast.BinaryApp(op, _, _) => binary_prec(op)
    @ast.UnaryApp(_, _) => 7
    _ => 8
  }
}

///|
fn binary_prec(op : @ast.BinaryOp) -> Int {
  match op {
    @ast.Eq | @ast.Ne | @ast.Less | @ast.LessEq => 4
    @ast.Gt | @ast.Ge | @ast.In_ => 4
    @ast.Contains | @ast.ContainsAll | @ast.ContainsAny => 4
    @ast.Add | @ast.Sub => 5
    @ast.Mul => 6
  }
}

///|
fn need_parens(inner_prec : Int, outer_prec : Int, is_right : Bool) -> Bool {
  inner_prec < outer_prec || (is_right && inner_prec == outer_prec)
}

///|
fn paren(
  s : String,
  inner_prec : Int,
  outer_prec : Int,
  is_right : Bool,
) -> String {
  if need_parens(inner_prec, outer_prec, is_right) {
    "(" + s + ")"
  } else {
    s
  }
}

// ---------------------------------------------------------------------------
// Keyword / ident helpers
// ---------------------------------------------------------------------------

///|
fn is_reserved(s : String) -> Bool {
  match s {
    "permit" | "forbid" | "when" | "unless" => true
    "in" | "has" | "like" | "is" => true
    "if" | "then" | "else" => true
    "true" | "false" => true
    "principal" | "action" | "resource" | "context" => true
    _ => false
  }
}

///|
fn is_valid_ident(s : String) -> Bool {
  if s == "" || is_reserved(s) {
    return false
  }
  // First character: [a-zA-Z_]  (ASCII 65-90, 97-122, 95)
  let first = s[0]
  if !(first >= 65 && first <= 90) &&
    !(first >= 97 && first <= 122) &&
    first != 95 {
    return false
  }
  // Rest: [a-zA-Z0-9_]*  (ASCII 48-57, 65-90, 97-122, 95)
  for i = 1; i < s.length(); i = i + 1 {
    let c = s[i]
    if !(c >= 48 && c <= 57) &&
      !(c >= 65 && c <= 90) &&
      !(c >= 97 && c <= 122) &&
      c != 95 {
      return false
    }
  }
  true
}

///|
fn ident_str(s : String) -> String {
  if is_valid_ident(s) {
    s
  } else {
    "\"" + s + "\""
  }
}

// ---------------------------------------------------------------------------
// Literal → Cedar source
// ---------------------------------------------------------------------------

///|
fn literal_str(l : @ast.Literal) -> String {
  match l {
    @ast.Bool(true) => "true"
    @ast.Bool(false) => "false"
    @ast.Long(v) => v.to_string()
    @ast.String(v) => "\"" + v + "\""
    @ast.EntityUID(uid) => uid.type_ + "::\"" + uid.id + "\""
  }
}

// ---------------------------------------------------------------------------
// BinaryOp → (symbol, is_method_call)
// ---------------------------------------------------------------------------

///|
fn binary_sym(op : @ast.BinaryOp) -> (String, Bool) {
  match op {
    @ast.Eq => ("==", false)
    @ast.Ne => ("!=", false)
    @ast.Less => ("<", false)
    @ast.LessEq => ("<=", false)
    @ast.Gt => (">", false)
    @ast.Ge => (">=", false)
    @ast.Add => ("+", false)
    @ast.Sub => ("-", false)
    @ast.Mul => ("*", false)
    @ast.In_ => ("in", false)
    @ast.Contains => (".contains", true)
    @ast.ContainsAll => (".containsAll", true)
    @ast.ContainsAny => (".containsAny", true)
  }
}

// ---------------------------------------------------------------------------
// Pattern → Cedar source
// ---------------------------------------------------------------------------

///|
fn pattern_str(p : @ast.Pattern) -> String {
  let mut out = "\""
  for elem in p.elements {
    match elem {
      @ast.Char(c) => out = out + c.to_string()
      @ast.Wildcard => out = out + "*"
    }
  }
  out + "\""
}

// ---------------------------------------------------------------------------
// Name → Cedar source
// ---------------------------------------------------------------------------

///|
fn name_str(n : @ast.Name) -> String {
  if n.ns.length() == 0 {
    n.name
  } else {
    let mut out = ""
    for seg in n.ns {
      out = out + seg + "::"
    }
    out + n.name
  }
}

// ---------------------------------------------------------------------------
// Scope constraint → Cedar source
// ---------------------------------------------------------------------------

///|
fn scope_str(sc : @ast.ScopeConstraint) -> String {
  match sc {
    @ast.All => ""
    @ast.Eq(uid) => " == " + uid.type_ + "::\"" + uid.id + "\""
    @ast.In(uid) => " in " + uid.type_ + "::\"" + uid.id + "\""
    @ast.Is(ty) => " is " + ty.0
    @ast.IsIn(ty, uid) =>
      " is " + ty.0 + " in " + uid.type_ + "::\"" + uid.id + "\""
    @ast.InSet(entities) => {
      let mut out = " in ["
      for i = 0; i < entities.length(); i = i + 1 {
        if i > 0 {
          out = out + ", "
        }
        out = out + entities[i].type_ + "::\"" + entities[i].id + "\""
      }
      out + "]"
    }
  }
}

///|
fn scope_multiline(sc : @ast.ScopeConstraint) -> Bool {
  match sc {
    @ast.All => false
    _ => true
  }
}

// ---------------------------------------------------------------------------
// Annotation → Cedar source
// ---------------------------------------------------------------------------

///|
fn annotation_str(ann : @ast.Annotation) -> String {
  if ann.value == "" {
    "@" + ann.key
  } else {
    "@" + ann.key + "(\"" + ann.value + "\")"
  }
}

// ---------------------------------------------------------------------------
// Core expression formatting (recursive, precedence-aware).
// `is_right`: true when this expr is the right operand of a left-assoc
// binary operator, forcing parens at equal precedence.
// ---------------------------------------------------------------------------

///|
fn format_expr_inner(
  e : @ast.Expr,
  outer_prec : Int,
  is_right : Bool,
) -> String {
  let p = expr_prec(e)
  match e {
    @ast.Lit(l) => literal_str(l)
    @ast.Var(@ast.Principal) => "principal"
    @ast.Var(@ast.Action) => "action"
    @ast.Var(@ast.Resource) => "resource"
    @ast.Var(@ast.Context) => "context"
    @ast.GetAttr(inner_e, attr) => {
      let inner = format_expr_inner(inner_e, 8, false)
      if is_valid_ident(attr) {
        inner + "." + attr
      } else {
        inner + "[\"" + attr + "\"]"
      }
    }
    @ast.Set(elems) => {
      let mut out = "["
      for i = 0; i < elems.length(); i = i + 1 {
        if i > 0 {
          out = out + ", "
        }
        out = out + format_expr_inner(elems[i], 0, false)
      }
      out + "]"
    }
    @ast.Record(pairs) => {
      let mut out = "{"
      for i = 0; i < pairs.length(); i = i + 1 {
        let (k, v) = pairs[i]
        if i > 0 {
          out = out + ", "
        }
        out = out + ident_str(k) + ": " + format_expr_inner(v, 0, false)
      }
      out + "}"
    }
    @ast.ExtensionApp(name, args) => {
      let mut out = name_str(name) + "("
      for i = 0; i < args.length(); i = i + 1 {
        if i > 0 {
          out = out + ", "
        }
        out = out + format_expr_inner(args[i], 0, false)
      }
      out + ")"
    }
    @ast.Slot(v) => "?" + v
    @ast.Unknown(v, _) => v
    @ast.If(c, t, el) => {
      let inner = "if " +
        format_expr_inner(c, 0, false) +
        " then " +
        format_expr_inner(t, 0, false) +
        " else " +
        format_expr_inner(el, 0, false)
      paren(inner, p, outer_prec, is_right)
    }
    @ast.And(l, r) => {
      let left = format_expr_inner(l, p, false)
      let right = format_expr_inner(r, p, true)
      paren(left + " && " + right, p, outer_prec, is_right)
    }
    @ast.Or(l, r) => {
      let left = format_expr_inner(l, p, false)
      let right = format_expr_inner(r, p, true)
      paren(left + " || " + right, p, outer_prec, is_right)
    }
    @ast.HasAttr(he, attr) => {
      let inner = format_expr_inner(he, p, false) + " has " + ident_str(attr)
      paren(inner, p, outer_prec, is_right)
    }
    @ast.Like(le, pat) => {
      let inner = format_expr_inner(le, p, false) + " like " + pattern_str(pat)
      paren(inner, p, outer_prec, is_right)
    }
    @ast.Is(ie, ty) => {
      let inner = format_expr_inner(ie, p, false) + " is " + ty.0
      paren(inner, p, outer_prec, is_right)
    }
    @ast.GetTag(ge, tag) => {
      let inner = format_expr_inner(ge, p, false) +
        ".getTag(" +
        format_expr_inner(tag, 0, false) +
        ")"
      paren(inner, p, outer_prec, is_right)
    }
    @ast.HasTag(he, tag) => {
      let inner = format_expr_inner(he, p, false) +
        ".hasTag(" +
        format_expr_inner(tag, 0, false) +
        ")"
      paren(inner, p, outer_prec, is_right)
    }
    @ast.BinaryApp(op, left, right) => {
      let (sym, is_method) = binary_sym(op)
      if is_method {
        let inner = format_expr_inner(left, 8, false) +
          sym +
          "(" +
          format_expr_inner(right, 0, false) +
          ")"
        paren(inner, p, outer_prec, is_right)
      } else {
        let l = format_expr_inner(left, p, false)
        let r = format_expr_inner(right, p, true)
        paren(l + " " + sym + " " + r, p, outer_prec, is_right)
      }
    }
    @ast.UnaryApp(@ast.Not, ne) => {
      let inner = "!" + format_expr_inner(ne, p, false)
      paren(inner, p, outer_prec, is_right)
    }
    @ast.UnaryApp(@ast.Neg, nge) => {
      let inner = "-" + format_expr_inner(nge, p, false)
      paren(inner, p, outer_prec, is_right)
    }
    @ast.UnaryApp(@ast.IsEmpty, iee) => {
      let inner = format_expr_inner(iee, 8, false) + ".isEmpty()"
      paren(inner, p, outer_prec, is_right)
    }
  }
}

// ---------------------------------------------------------------------------
// Condition → Cedar source
// ---------------------------------------------------------------------------

///|
fn condition_str(cond : @ast.Condition) -> String {
  let kw = match cond.kind {
    @ast.When => "when"
    @ast.Unless => "unless"
  }
  kw + " { " + format_expr_inner(cond.body, 0, false) + " }"
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

///|
/// Convert Expr to Cedar source text.
pub fn stringify_expr(e : @ast.Expr) -> String {
  format_expr_inner(e, 0, false)
}

///|
/// Convert Policy to Cedar source text.
pub fn stringify(p : @ast.Policy) -> String {
  let mut s = ""

  for ann in p.annotations {
    s = s + annotation_str(ann) + "\n"
  }

  let eff = match p.effect {
    @ast.Permit => "permit"
    @ast.Forbid => "forbid"
  }

  let ml = scope_multiline(p.principal) ||
    scope_multiline(p.action) ||
    scope_multiline(p.resource)
  if ml {
    s = s + eff + "(\n"
    s = s + "  principal" + scope_str(p.principal) + ",\n"
    s = s + "  action" + scope_str(p.action) + ",\n"
    s = s + "  resource" + scope_str(p.resource) + "\n"
    s = s + ")"
  } else {
    s = s + eff + "(principal, action, resource)"
  }

  for cond in p.conditions {
    s = s + "\n" + condition_str(cond)
  }

  s + ";"
}