///|
pub(all) struct ExpressionInfo {
  operator : String
  level : Int32
  span : Span
  variables : Array[String]
} derive(Debug)

///|
fn expression_level(op : Char, specs : Array[VarSpec]) -> Int32 {
  for spec in specs {
    if !(spec.modifier is NoModifier) {
      return 4
    }
  }
  if op == ' ' {
    1
  } else if op == '+' || op == '#' {
    2
  } else {
    3
  }
}

///|
fn expression_span(op : Char, specs : Array[VarSpec]) -> Span {
  {
    start: specs[0].span.start - (if op == ' ' { 1 } else { 2 }),
    end: specs[specs.length() - 1].span.end + 1,
  }
}

///|
fn Template::expressions(self : Template) -> Array[ExpressionInfo] {
  let result : Array[ExpressionInfo] = []
  for segment in self.segments {
    if segment is Expression(op, specs) {
      result.push({
        operator: if op == ' ' {
          ""
        } else {
          op.to_string()
        },
        level: expression_level(op, specs),
        span: expression_span(op, specs),
        variables: specs.map(s => s.name),
      })
    }
  }
  result
}

///|
/// Static policy. This does not validate a host, scheme, or prevent SSRF.
pub(all) struct TemplatePolicy {
  allow_reserved : Bool
  allow_fragment : Bool
  allow_prefix : Bool
  allow_explode : Bool
  allowed_variables : Array[String]?
  max_level : Int32
  max_references : Int32
  forbid_repeated_variables : Bool
} derive(Debug)

///|
pub fn TemplatePolicy::new(
  allow_reserved~ : Bool = true,
  allow_fragment~ : Bool = true,
  allow_prefix~ : Bool = true,
  allow_explode~ : Bool = true,
  allowed_variables~ : Array[String]? = None,
  max_level~ : Int32 = 4,
  max_references~ : Int32 = 65536,
  forbid_repeated_variables~ : Bool = false,
) -> TemplatePolicy {
  {
    allow_reserved,
    allow_fragment,
    allow_prefix,
    allow_explode,
    allowed_variables,
    max_level,
    max_references,
    forbid_repeated_variables,
  }
}

///|
/// Static policy findings, at most `max_findings` of them.
pub fn Template::audit(
  self : Template,
  policy : TemplatePolicy,
  max_findings~ : Int32 = 256,
) -> Result[Array[TemplateError], TemplateError] {
  if max_findings < 0 {
    return Err(problem("InvalidLimit", "negative finding budget", 0, 0))
  }
  let findings : Array[TemplateError] = []
  if policy.max_level < 0 || policy.max_level > 4 || policy.max_references < 0 {
    return Err(
      problem("InvalidPolicy", "invalid level or reference limit", 0, 0),
    )
  }
  let seen : Map[String, Bool] = Map([])
  let allowed : Map[String, Bool] = Map([])
  if policy.allowed_variables is Some(names) {
    for name in names {
      allowed[name] = true
    }
  }
  let mut count = 0
  for segment in self.segments {
    if segment is Expression(op, specs) &&
      expression_level(op, specs) > policy.max_level {
      if findings.length() == max_findings {
        return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
      }
      findings.push(
        problem(
          "LevelPolicy",
          "expression exceeds allowed RFC level",
          expression_span(op, specs).start,
          expression_span(op, specs).end,
        ),
      )
    }
  }
  for segment in self.segments {
    if segment is Expression(op, specs) {
      for spec in specs {
        if count >= policy.max_references {
          if findings.length() == max_findings {
            return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
          }
          findings.push(
            problem(
              "ReferencePolicy",
              "reference count exceeds limit",
              spec.span.start,
              spec.span.end,
            ),
          )
        }
        count = count + 1
        if policy.forbid_repeated_variables && seen.contains(spec.name) {
          if findings.length() == max_findings {
            return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
          }
          findings.push(
            problem(
              "RepeatedPolicy",
              "repeated variable is disallowed",
              spec.span.start,
              spec.span.end,
            ),
          )
        }
        if policy.forbid_repeated_variables {
          seen[spec.name] = true
        }
        if !policy.allow_reserved && (op == '+' || op == '#') {
          if findings.length() == max_findings {
            return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
          }
          findings.push(
            problem(
              "ReservedPolicy",
              "reserved expansion is disallowed",
              spec.span.start,
              spec.span.end,
            ),
          )
        }
        if !policy.allow_fragment && op == '#' {
          if findings.length() == max_findings {
            return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
          }
          findings.push(
            problem(
              "FragmentPolicy",
              "fragment expansion is disallowed",
              spec.span.start,
              spec.span.end,
            ),
          )
        }
        if !policy.allow_prefix && spec.modifier is Prefix(_) {
          if findings.length() == max_findings {
            return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
          }
          findings.push(
            problem(
              "PrefixPolicy",
              "prefix modifier is disallowed",
              spec.span.start,
              spec.span.end,
            ),
          )
        }
        if !policy.allow_explode && spec.modifier is Explode {
          if findings.length() == max_findings {
            return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
          }
          findings.push(
            problem(
              "ExplodePolicy",
              "explode modifier is disallowed",
              spec.span.start,
              spec.span.end,
            ),
          )
        }
        if policy.allowed_variables is Some(_) && !allowed.contains(spec.name) {
          if findings.length() == max_findings {
            return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
          }
          findings.push(
            problem(
              "VariablePolicy",
              "variable is not allowed",
              spec.span.start,
              spec.span.end,
            ),
          )
        }
      }
    }
  }
  Ok(findings)
}

///|
pub extend ExpressionInfo with Debug::{to_repr}

///|
pub extend TemplatePolicy with Debug::{to_repr}