// SPF 求值(RFC 7208)。
//
// 覆盖全部机制与修饰符:all / include / a / mx / ptr / ip4 / ip6 / exists /
// redirect / exp;包含宏展开、10 次 DNS 查询上限与 2 次 void lookup 上限。

///|
pub enum SpfQualifier {
  Plus
  Minus
  Tilde
  Question
} derive(Eq)

///|
pub enum SpfKind {
  All
  Include
  A
  Mx
  Ptr
  Ip4
  Ip6
  Exists
  Redirect
  Exp
  Unknown
} derive(Eq)

///|
pub struct SpfTerm {
  qualifier : SpfQualifier
  kind : SpfKind
  /// 机制参数(域名、IP 或宏字符串)
  value : String
  cidr4 : Int?
  cidr6 : Int?
  raw : String
}

///|
pub struct SpfRecord {
  terms : Array[SpfTerm]
}

///|
/// 宏展开上下文。
pub struct MacroCtx {
  sender : String
  domain : String
  ip : String
  helo : String
}

///|
fn spf_lower(text : StringView) -> String {
  ascii_lower(text)
}

///|
fn starts_with_ignore_case(text : String, prefix : String) -> Bool {
  if text.length() < prefix.length() {
    return false
  }
  spf_lower(text[:prefix.length()]) == spf_lower(prefix)
}

///|
/// 解析 SPF 记录。返回 None 表示不是 `v=spf1` 记录。
pub fn parse_spf_record(text : String) -> SpfRecord? {
  let trimmed = trim_wsp(strip_txt_quotes(text))
  if !starts_with_ignore_case(trimmed, "v=spf1") {
    return None
  }
  let terms : Array[SpfTerm] = []
  let mut first = true
  for raw_term in trimmed.split(" ") {
    let term_text = trim_wsp(raw_term)
    if term_text.length() == 0 {
      continue
    }
    if first {
      first = false
      continue
    }
    terms.push(parse_spf_term(term_text))
  }
  Some({ terms, })
}

///|
fn parse_spf_term(text : String) -> SpfTerm {
  // 修饰符
  let eq = text.find("=")
  match eq {
    Some(i) => {
      let name = spf_lower(text[:i])
      if name == "redirect" || name == "exp" {
        let kind = if name == "redirect" { Redirect } else { Exp }
        return {
          qualifier: Plus,
          kind,
          value: text[i + 1:].to_owned(),
          cidr4: None,
          cidr6: None,
          raw: text,
        }
      }
    }
    None => ()
  }
  let chars = text.to_array()
  let mut qualifier = Plus
  let mut start = 0
  if chars.length() > 0 {
    let c = chars[0]
    if c == '+' {
      start = 1
    } else if c == '-' {
      qualifier = Minus
      start = 1
    } else if c == '~' {
      qualifier = Tilde
      start = 1
    } else if c == '?' {
      qualifier = Question
      start = 1
    }
  }
  let mut name_end = start
  while name_end < chars.length() &&
        chars[name_end] != ':' &&
        chars[name_end] != '/' {
    name_end = name_end + 1
  }
  let rest = StringBuilder()
  for i in start.. All
    "include" => Include
    "a" => A
    "mx" => Mx
    "ptr" => Ptr
    "ip4" => Ip4
    "ip6" => Ip6
    "exists" => Exists
    _ => Unknown
  }
  let mut value = ""
  let mut tail = ""
  if name_end < chars.length() && chars[name_end] == ':' {
    let mut value_end = name_end + 1
    while value_end < chars.length() && chars[value_end] != '/' {
      value_end = value_end + 1
    }
    let value_buf = StringBuilder()
    let value_start = name_end + 1
    for i in value_start.. (Int?, Int?) {
  if tail.length() == 0 {
    return (None, None)
  }
  // 形如 /24 或 /24//64 或 //64
  let doubled = tail.find("//")
  match doubled {
    Some(i) => {
      let first = tail[:i]
      let second = tail[i + 2:]
      (
        parse_leading_int(first.to_owned()),
        parse_leading_int(second.to_owned()),
      )
    }
    None => (parse_leading_int(tail), None)
  }
}

///|
fn parse_leading_int(text : String) -> Int? {
  let chars = text.to_array()
  let mut i = 0
  while i < chars.length() && chars[i] == '/' {
    i = i + 1
  }
  let start = i
  while i < chars.length() && chars[i] >= '0' && chars[i] <= '9' {
    i = i + 1
  }
  if i == start {
    return None
  }
  let mut value = 0
  for j in start.. AuthStatus {
  match q {
    Plus => Pass
    Minus => Fail
    Tilde => SoftFail
    Question => Neutral
  }
}

///|
fn qualifier_text(q : SpfQualifier) -> String {
  match q {
    Plus => "+"
    Minus => "-"
    Tilde => "~"
    Question => "?"
  }
}

///|
/// 解析 IPv4,返回四个八位组。
pub fn parse_ipv4(text : String) -> Array[Int]? {
  let parts : Array[Int] = []
  for part in text.split(".") {
    let token = trim_wsp(part)
    if token.length() == 0 || token.length() > 3 {
      return None
    }
    let mut value = 0
    for c in token {
      if c < '0' || c > '9' {
        return None
      }
      value = value * 10 + (c.to_int() - '0'.to_int())
    }
    if value > 255 {
      return None
    }
    parts.push(value)
  }
  if parts.length() != 4 {
    return None
  }
  Some(parts)
}

///|
/// 解析 IPv6,返回八个 16 位分组。
pub fn parse_ipv6(text : String) -> Array[Int]? {
  let input = trim_wsp(text)
  if input.length() == 0 {
    return None
  }
  let groups : Array[Int] = []
  let left : Array[String] = []
  let right : Array[String] = []
  let double_colon = input.find("::")
  let mut has_double = false
  let mut left_text = input
  let mut right_text = ""
  match double_colon {
    Some(i) => {
      has_double = true
      left_text = input[:i].to_owned()
      right_text = input[i + 2:].to_owned()
    }
    None => ()
  }
  for part in left_text.split(":") {
    let token = trim_wsp(part)
    if token.length() > 0 {
      left.push(token)
    }
  }
  for part in right_text.split(":") {
    let token = trim_wsp(part)
    if token.length() > 0 {
      right.push(token)
    }
  }
  let left_groups = parse_ipv6_side(left)
  let right_groups = parse_ipv6_side(right)
  let (left_values, left_ok) = left_groups
  let (right_values, right_ok) = right_groups
  if !left_ok || !right_ok {
    return None
  }
  if !has_double && left_values.length() + right_values.length() != 8 {
    return None
  }
  if has_double && left_values.length() + right_values.length() > 7 {
    return None
  }
  for value in left_values {
    groups.push(value)
  }
  if has_double {
    let fill = 8 - left_values.length() - right_values.length()
    for _ in 0.. (Array[Int], Bool) {
  let values : Array[Int] = []
  for part in parts {
    // 末尾允许 IPv4 形式
    if part.contains(".") {
      match parse_ipv4(part) {
        Some(v4) => {
          values.push(v4[0] * 256 + v4[1])
          values.push(v4[2] * 256 + v4[3])
        }
        None => return (values, false)
      }
    } else {
      if part.length() > 4 {
        return (values, false)
      }
      let mut value = 0
      for c in part {
        let digit = if c >= '0' && c <= '9' {
          c.to_int() - '0'.to_int()
        } else if c >= 'a' && c <= 'f' {
          c.to_int() - 'a'.to_int() + 10
        } else if c >= 'A' && c <= 'F' {
          c.to_int() - 'A'.to_int() + 10
        } else {
          return (values, false)
        }
        value = value * 16 + digit
      }
      values.push(value)
    }
  }
  (values, true)
}

///|
fn match_v4(ip : Array[Int], net : Array[Int], prefix : Int) -> Bool {
  if prefix < 0 || prefix > 32 {
    return false
  }
  let mut bits = 0
  for i in 0..<4 {
    let remain = prefix - bits
    if remain <= 0 {
      return true
    }
    if remain >= 8 {
      if ip[i] != net[i] {
        return false
      }
      bits = bits + 8
    } else {
      let mask = (255 << (8 - remain)) & 255
      return (ip[i] & mask) == (net[i] & mask)
    }
  }
  true
}

///|
fn match_v6(ip : Array[Int], net : Array[Int], prefix : Int) -> Bool {
  if prefix < 0 || prefix > 128 {
    return false
  }
  let mut bits = 0
  for i in 0..<8 {
    let remain = prefix - bits
    if remain <= 0 {
      return true
    }
    if remain >= 16 {
      if ip[i] != net[i] {
        return false
      }
      bits = bits + 16
    } else {
      let mask = (0xFFFF << (16 - remain)) & 0xFFFF
      return (ip[i] & mask) == (net[i] & mask)
    }
  }
  true
}

///|
fn percent_encode(text : StringView) -> String {
  let out = StringBuilder()
  let digits : Array[Char] = [
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
  ]
  for c in text {
    let code = c.to_int()
    let safe = (code >= 48 && code <= 57) ||
      (code >= 65 && code <= 90) ||
      (code >= 97 && code <= 122) ||
      code == 45 ||
      code == 46 ||
      code == 95 ||
      code == 126
    if safe {
      out.write_char(c)
    } else {
      out.write_char('%')
      out.write_char(digits[(code >> 4) & 15])
      out.write_char(digits[code & 15])
    }
  }
  out.to_string()
}

///|
fn find_from(text : String, needle : StringView, start : Int) -> Int? {
  if start < 0 || start >= text.length() {
    return None
  }
  let tail = text[start:].to_owned()
  match tail.find(needle) {
    Some(i) => Some(start + i)
    None => None
  }
}

///|
/// 展开 SPF 宏。`current_domain` 是正在求值的域,用于 `%{d}`。
pub fn expand_macro(
  pattern : String,
  ctx : MacroCtx,
  current_domain : String,
) -> String {
  let out = StringBuilder()
  let mut i = 0
  let n = pattern.length()
  while i < n {
    match find_from(pattern, "%{", i) {
      Some(start) => {
        if start > i {
          out.write_string(pattern[i:start].to_owned())
        }
        match find_from(pattern, "}", start + 2) {
          Some(end) => {
            let body = pattern[start + 2:end].to_owned()
            out.write_string(expand_one(body, ctx, current_domain))
            i = end + 1
          }
          None => {
            out.write_string(pattern[start:].to_owned())
            i = n
          }
        }
      }
      None => {
        out.write_string(pattern[i:].to_owned())
        i = n
      }
    }
  }
  out.to_string()
}

///|
fn expand_one(body : String, ctx : MacroCtx, current_domain : String) -> String {
  let chars = body.to_array()
  if chars.length() == 0 {
    return ""
  }
  let letter = chars[0]
  let upper = letter >= 'A' && letter <= 'Z'
  let lower = if upper {
    Int::unsafe_to_char(letter.to_int() + 32)
  } else {
    letter
  }
  let (local, sender_domain) = match ctx.sender.split_once("@") {
    Some((l, d)) => (l.to_owned(), d.to_owned())
    None => ("", ctx.sender)
  }
  let raw = match lower {
    's' => ctx.sender
    'l' => local
    'o' => sender_domain
    'd' => current_domain
    'i' => ctx.ip
    'v' => if ctx.ip.contains(":") { "ip6" } else { "in-addr" }
    'h' => ctx.helo
    _ => ""
  }
  // 变换器:数字(保留右侧 N 段)、r(反转)、自定义分隔符
  let mut keep = -1
  let mut reverse = false
  let sep_buf = StringBuilder()
  for j in 1..= '0' && c <= '9' {
      if keep < 0 {
        keep = 0
      }
      keep = keep * 10 + (c.to_int() - '0'.to_int())
    } else if c == 'r' || c == 'R' {
      reverse = true
    } else {
      sep_buf.write_char(c)
    }
  }
  let separators = sep_buf.to_string()
  let seps = if separators.length() == 0 { "." } else { separators }
  let mut parts : Array[String] = []
  for part in raw.split(seps) {
    parts.push(part.to_owned())
  }
  if reverse {
    let reversed : Array[String] = []
    let mut k = parts.length()
    while k > 0 {
      k = k - 1
      reversed.push(parts[k])
    }
    parts = reversed
  }
  if keep > 0 && parts.length() > keep {
    let trimmed : Array[String] = []
    let start = parts.length() - keep
    for k in start.. 0 {
      joined.write_string(seps)
    }
    joined.write_string(parts[k])
  }
  let result = joined.to_string()
  if upper {
    percent_encode(result)
  } else {
    result
  }
}

///|
/// DNS 查询计数与限额。
struct SpfCounter {
  lookups : Array[Int]
  voids : Array[Int]
}

///|
const SPF_MAX_LOOKUPS : Int = 10

///|
const SPF_MAX_VOIDS : Int = 2

///|
fn count_lookup(counter : SpfCounter) -> Bool {
  counter.lookups[0] = counter.lookups[0] + 1
  counter.lookups[0] <= SPF_MAX_LOOKUPS
}

///|
fn count_void(counter : SpfCounter) -> Bool {
  counter.voids[0] = counter.voids[0] + 1
  counter.voids[0] <= SPF_MAX_VOIDS
}

///|
/// 单域名求值结果。
struct SpfOutcome {
  status : AuthStatus
  mechanism : String
  reason : String
}

///|
/// 对发件域做 SPF 求值。
pub fn[R : Resolver] evaluate_spf(
  resolver : R,
  domain : String,
  ip : String,
  helo? : String,
  sender? : String,
) -> SpfResult {
  let ctx : MacroCtx = {
    sender: sender.unwrap_or("postmaster@" + domain),
    domain,
    ip,
    helo: helo.unwrap_or(""),
  }
  let counter : SpfCounter = { lookups: [0], voids: [0], }
  let outcome = eval_domain(resolver, normalize_name(domain), ctx, counter, 0)
  {
    domain: normalize_name(domain),
    identity: ctx.sender,
    status: outcome.status,
    qualifier: "",
    mechanism: outcome.mechanism,
    reason: outcome.reason,
    dns_lookups: counter.lookups[0],
    void_lookups: counter.voids[0],
  }
}

///|
fn[R : Resolver] eval_domain(
  resolver : R,
  domain : String,
  ctx : MacroCtx,
  counter : SpfCounter,
  depth : Int,
) -> SpfOutcome {
  if depth > 10 {
    return {
      status: PermError,
      mechanism: "",
      reason: "include/redirect recursion too deep",
    }
  }
  let records = resolver.txt(domain)
  if records.length() == 0 {
    if !count_void(counter) {
      return {
        status: PermError,
        mechanism: "",
        reason: "too many void lookups",
      }
    }
    return { status: None_, mechanism: "", reason: "no SPF record", }
  }
  let mut spf : SpfRecord? = None
  let mut count = 0
  for record in records {
    match parse_spf_record(record) {
      Some(parsed) => {
        spf = Some(parsed)
        count = count + 1
      }
      None => ()
    }
  }
  if count == 0 {
    return { status: None_, mechanism: "", reason: "no SPF record", }
  }
  if count > 1 {
    return { status: PermError, mechanism: "", reason: "multiple SPF records", }
  }
  let record = match spf {
    Some(r) => r
    None => return { status: None_, mechanism: "", reason: "no SPF record", }
  }
  let client4 = parse_ipv4(ctx.ip)
  let client6 = parse_ipv6(ctx.ip)
  let mut redirect : String? = None
  let mut explanation : String? = None
  for term in record.terms {
    if term.kind == Redirect {
      redirect = Some(term.value)
      continue
    }
    if term.kind == Exp {
      explanation = Some(term.value)
      continue
    }
    if term.kind == Unknown {
      continue
    }
    let matched = match term.kind {
      All => true
      Ip4 =>
        match client4 {
          Some(client) =>
            match parse_ipv4(term.value) {
              Some(net) => match_v4(client, net, term.cidr4.unwrap_or(32))
              None => false
            }
          None => false
        }
      Ip6 =>
        match client6 {
          Some(client) =>
            match parse_ipv6(term.value) {
              Some(net) => match_v6(client, net, term.cidr4.unwrap_or(128))
              None => false
            }
          None => false
        }
      Include => {
        if !count_lookup(counter) {
          return {
            status: PermError,
            mechanism: term.raw,
            reason: "too many DNS lookups",
          }
        }
        let target = expand_macro(term.value, ctx, domain)
        if target.length() == 0 {
          false
        } else {
          let inner = eval_domain(resolver, target, ctx, counter, depth + 1)
          inner.status == Pass
        }
      }
      A => {
        if !count_lookup(counter) {
          return {
            status: PermError,
            mechanism: term.raw,
            reason: "too many DNS lookups",
          }
        }
        let target = if term.value.length() == 0 {
          domain
        } else {
          expand_macro(term.value, ctx, domain)
        }
        match_a_or_aaaa(resolver, target, client4, client6, term, counter)
      }
      Mx => {
        if !count_lookup(counter) {
          return {
            status: PermError,
            mechanism: term.raw,
            reason: "too many DNS lookups",
          }
        }
        let target = if term.value.length() == 0 {
          domain
        } else {
          expand_macro(term.value, ctx, domain)
        }
        let hosts = resolver.mx(target)
        if hosts.length() == 0 {
          if !count_void(counter) {
            return {
              status: PermError,
              mechanism: term.raw,
              reason: "too many void lookups",
            }
          }
          false
        } else {
          let mut hit = false
          for host in hosts {
            if match_a_or_aaaa(resolver, host, client4, client6, term, counter) {
              hit = true
            }
          }
          hit
        }
      }
      Ptr => {
        if !count_lookup(counter) {
          return {
            status: PermError,
            mechanism: term.raw,
            reason: "too many DNS lookups",
          }
        }
        let target = if term.value.length() == 0 {
          domain
        } else {
          expand_macro(term.value, ctx, domain)
        }
        match_ptr(resolver, ctx.ip, target)
      }
      Exists => {
        if !count_lookup(counter) {
          return {
            status: PermError,
            mechanism: term.raw,
            reason: "too many DNS lookups",
          }
        }
        let target = expand_macro(term.value, ctx, domain)
        let found = resolver.a(target)
        if found.length() == 0 {
          if !count_void(counter) {
            return {
              status: PermError,
              mechanism: term.raw,
              reason: "too many void lookups",
            }
          }
          false
        } else {
          true
        }
      }
      _ => false
    }
    if matched {
      let mut reason = "matched \{term.raw}"
      if term.qualifier == Minus {
        match explanation {
          Some(pattern) => {
            let target = expand_macro(pattern, ctx, domain)
            let texts = resolver.txt(target)
            if texts.length() > 0 {
              reason = reason + ": " + trim_wsp(strip_txt_quotes(texts[0]))
            }
          }
          None => ()
        }
      }
      return {
        status: qualifier_status(term.qualifier),
        mechanism: term.raw,
        reason,
      }
    }
  }
  match redirect {
    Some(target) => {
      if !count_lookup(counter) {
        return {
          status: PermError,
          mechanism: "",
          reason: "too many DNS lookups",
        }
      }
      let expanded = expand_macro(target, ctx, domain)
      let inner = eval_domain(resolver, expanded, ctx, counter, depth + 1)
      {
        status: inner.status,
        mechanism: "redirect=" + expanded,
        reason: "redirect to \{expanded}: \{inner.reason}",
      }
    }
    None => { status: Neutral, mechanism: "", reason: "no mechanism matched", }
  }
}

///|
fn[R : Resolver] match_a_or_aaaa(
  resolver : R,
  host : String,
  client4 : Array[Int]?,
  client6 : Array[Int]?,
  term : SpfTerm,
  counter : SpfCounter,
) -> Bool {
  match client4 {
    Some(client) => {
      let addresses = resolver.a(host)
      if addresses.length() == 0 {
        if !count_void(counter) {
          return false
        }
      }
      for address in addresses {
        match parse_ipv4(address) {
          Some(net) =>
            if match_v4(client, net, term.cidr4.unwrap_or(32)) {
              return true
            }
          None => ()
        }
      }
      false
    }
    None =>
      match client6 {
        Some(client) => {
          let addresses = resolver.aaaa(host)
          if addresses.length() == 0 {
            if !count_void(counter) {
              return false
            }
          }
          for address in addresses {
            match parse_ipv6(address) {
              Some(net) =>
                if match_v6(client, net, term.cidr6.unwrap_or(128)) {
                  return true
                }
              None => ()
            }
          }
          false
        }
        None => false
      }
  }
}

///|
fn[R : Resolver] match_ptr(resolver : R, ip : String, domain : String) -> Bool {
  let reverse = match parse_ipv4(ip) {
    Some(v4) => "\{v4[3]}.\{v4[2]}.\{v4[1]}.\{v4[0]}.in-addr.arpa"
    None => return false
  }
  let names = resolver.ptr(reverse)
  let target = normalize_name(domain)
  let suffix = "." + target
  for name in names {
    let host = normalize_name(name)
    if host == target || host.has_suffix(suffix) {
      // 前向确认:主机名必须解析回同一个 IP
      let addresses = resolver.a(host)
      for address in addresses {
        if address == ip {
          return true
        }
      }
    }
  }
  false
}