///|
pub(all) struct ScriptAudit {
  warnings : Array[Diagnostic]
  commands : Int
  predicates : Int
  max_depth : Int
  literal_redirects : Array[String]
  literal_mailboxes : Array[String]
} derive(Eq, Debug)

///|
priv struct Auditor {
  warnings : Array[Diagnostic]
  mut commands : Int
  mut predicates : Int
  mut max_depth : Int
  redirects : Array[String]
  mailboxes : Array[String]
}

///|
fn constant_condition(expr : Test) -> Bool? {
  match expr {
    Call("true", _, _) => Some(true)
    Call("false", _, _) => Some(false)
    Call(_, _, _) => None
    Not(child, _) => constant_condition(child).map(value => !value)
    AnyOf(children, _) => {
      let mut unknown = false
      for child in children {
        match constant_condition(child) {
          Some(true) => return Some(true)
          None => unknown = true
          _ => ()
        }
      }
      if unknown {
        None
      } else {
        Some(false)
      }
    }
    AllOf(children, _) => {
      let mut unknown = false
      for child in children {
        match constant_condition(child) {
          Some(false) => return Some(false)
          None => unknown = true
          _ => ()
        }
      }
      if unknown {
        None
      } else {
        Some(true)
      }
    }
  }
}

///|
fn test_span(expr : Test) -> Span {
  match expr {
    Call(_, _, span) | Not(_, span) | AnyOf(_, span) | AllOf(_, span) => span
  }
}

///|
fn Auditor::warn(
  self : Auditor,
  code : String,
  message : String,
  span : Span,
) -> Unit {
  self.warnings.push({ code, message, span })
}

///|
fn Auditor::condition(self : Auditor, expr : Test, depth : Int) -> Unit {
  self.predicates += 1
  if depth > self.max_depth {
    self.max_depth = depth
  }
  match expr {
    Not(child, _) => self.condition(child, depth + 1)
    AnyOf(children, _) | AllOf(children, _) =>
      for child in children {
        self.condition(child, depth + 1)
      }
    Call(name, args, span) =>
      if name == "header" || name == "address" || name == "envelope" {
        let mut wildcard = false
        for arg in args {
          match arg {
            Flag("matches") => wildcard = true
            _ => ()
          }
        }
        if wildcard {
          match args.last() {
            Some(Literal("*")) | Some(Strings(["*"])) =>
              self.warn(
                "lint.broad_match", "通配符 * 会匹配该字段的任意值;确认是否只想检查存在性。",
                span,
              )
            _ => ()
          }
        }
      }
  }
}

///|
fn Auditor::body(
  self : Auditor,
  body : Array[Statement],
  depth : Int,
  guarded : Bool,
  initially_reachable : Bool,
) -> Bool {
  if depth > self.max_depth {
    self.max_depth = depth
  }
  let mut reachable = initially_reachable
  let mut stopped = false
  for statement in body {
    match statement {
      Command(name, args, span) => {
        self.commands += 1
        if !reachable {
          self.warn(
            "lint.unreachable", "此语句位于已终止或恒假路径中。",
            span,
          )
        }
        if name == "stop" {
          reachable = false
          stopped = true
        }
        if reachable && !guarded && name == "discard" {
          self.warn(
            "lint.unconditional_discard", "此路径无邮件条件保护地取消默认保留;部署前应回放样本。",
            span,
          )
        }
        if name == "redirect" || name == "fileinto" {
          match args.last() {
            Some(Literal(target)) if !target.contains("${") => {
              let collection = if name == "redirect" {
                self.redirects
              } else {
                self.mailboxes
              }
              if !collection.contains(target) {
                collection.push(target)
              }
            }
            _ => ()
          }
          if name == "redirect" && reachable && !guarded {
            self.warn(
              "lint.unconditional_redirect", "转发动作没有邮件条件保护;请确认目的地和循环转发策略。",
              span,
            )
          }
        }
      }
      Branch(branches, otherwise, _) => {
        let mut remaining = reachable
        let mut all_stop = true
        for branch in branches {
          self.condition(branch.condition, depth + 1)
          let constant = constant_condition(branch.condition)
          if constant == Some(false) {
            self.warn(
              "lint.constant_false",
              "此条件恒为假。",
              test_span(branch.condition),
            )
          }
          let possible = remaining && constant != Some(false)
          let branch_stops = self.body(
            branch.body,
            depth + 1,
            guarded || constant != Some(true),
            possible,
          )
          if possible && !branch_stops {
            all_stop = false
          }
          if constant == Some(true) {
            remaining = false
          }
        }
        let else_stops = self.body(otherwise, depth + 1, true, remaining)
        if remaining && !else_stops {
          all_stop = false
        }
        if reachable && all_stop {
          reachable = false
          stopped = true
        }
      }
    }
  }
  stopped
}

///|
pub fn Program::audit(self : Program) -> ScriptAudit {
  let state : Auditor = {
    warnings: [],
    commands: 0,
    predicates: 0,
    max_depth: 0,
    redirects: [],
    mailboxes: [],
  }
  ignore(state.body(self.syntax.statements, 0, false, true))
  {
    warnings: state.warnings,
    commands: state.commands,
    predicates: state.predicates,
    max_depth: state.max_depth,
    literal_redirects: state.redirects,
    literal_mailboxes: state.mailboxes,
  }
}