// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// id-kp-serverAuth: the purpose a TLS server certificate needs (RFC 5280 §4.2.1.12).
pub let eku_server_auth : String = "1.3.6.1.5.5.7.3.1"

///|
/// id-kp-clientAuth.
pub let eku_client_auth : String = "1.3.6.1.5.5.7.3.2"

///|
/// anyExtendedKeyUsage (RFC 5280 §4.2.1.12).
pub let eku_any : String = "2.5.29.37.0"

///|
/// A GeneralName (RFC 5280 §4.2.1.6). The forms that name a peer are decoded; the rest keep
/// their field number and contents, so a caller can see what was there instead of being told the
/// extension was empty. In a name constraint an `Ip` is an address followed by its mask, eight or
/// thirty-two octets; in a subjectAltName it is the bare address.
pub(all) enum GeneralName {
  Dns(String)
  Email(String)
  Uri(String)
  Ip(Bytes)
  Dir(Name)
  Named(Int32, Bytes)
} derive(Eq, Debug)

///|
/// A NameConstraints (RFC 5280 §4.2.1.10): the subtrees every name below the CA must fall in,
/// and the ones none may.
pub(all) struct Constraints {
  permitted : Array[GeneralName]
  excluded : Array[GeneralName]
} derive(Eq, Debug)

///|
/// The extensions a certificate is judged on (RFC 5280 §4.2). `key_usage` bits are numbered from
/// the top of the first octet — 0 digitalSignature, 5 keyCertSign. `eku` is `None` when the
/// extension is absent, which means any purpose, as against a listed set that does not include
/// the one being asked for. `ski` and `aki` are the key identifiers path building uses to try the
/// likeliest issuer first. `unknown_critical` names the critical extensions this parser does not
/// understand: §4.2 makes such a certificate unusable, and a caller can only obey that rule if it
/// is told rather than left to assume the skip was safe.
pub(all) struct Exts {
  san : Array[GeneralName]
  ca : Bool
  path_len : Int32?
  key_usage : @asn1.Bits?
  eku : Array[String]?
  ski : Bytes?
  aki : Bytes?
  constraints : Constraints?
  unknown_critical : Array[String]
} derive(Eq, Debug)

///|
/// The extensions of a certificate that has none.
let no_exts : Exts = {
  san: [],
  ca: false,
  path_len: None,
  key_usage: None,
  eku: None,
  ski: None,
  aki: None,
  constraints: None,
  unknown_critical: [],
}

///|
/// One GeneralName. `constraint` reads an iPAddress as address and mask.
fn read_general(
  g : @asn1.Tlv,
  constraint~ : Bool,
) -> GeneralName raise @asn1.Refused {
  if (g.tag & 0xc0) != 0x80 {
    raise @asn1.Malformed("GeneralName is not context-tagged")
  }
  match g.tag & 0x1f {
    1 => Email(@asn1.ascii(g.value))
    2 => Dns(@asn1.ascii(g.value))
    4 => {
      if g.tag != 0xa4 {
        raise @asn1.Malformed("directoryName is not EXPLICIT-tagged")
      }
      Dir(read_name(@asn1.only(g.value)))
    }
    6 => Uri(@asn1.ascii(g.value))
    7 => {
      let ok = if constraint {
        g.value.length() == 8 || g.value.length() == 32
      } else {
        g.value.length() == 4 || g.value.length() == 16
      }
      if !ok {
        raise @asn1.Malformed("iPAddress has the wrong length")
      }
      Ip(g.value.to_owned())
    }
    n => Named(n, g.value.to_owned())
  }
}

///|
/// A GeneralNames (RFC 5280 §4.2.1.6). The entries are IMPLICIT-tagged, so the field number is
/// the only thing that says what the octets are; there is no string tag left to read.
fn read_san(v : BytesView) -> Array[GeneralName] raise @asn1.Refused {
  let seq = @asn1.only(v)
  if seq.tag != 0x30 {
    raise @asn1.Malformed("subjectAltName is not a SEQUENCE")
  }
  let out = seq.items().map(g => read_general(g, constraint=false))
  if out.length() == 0 {
    raise @asn1.Malformed("empty subjectAltName")
  }
  out
}

///|
/// A BasicConstraints (RFC 5280 §4.2.1.9) as `(cA, pathLenConstraint)`.
fn read_basic(v : BytesView) -> (Bool, Int32?) raise @asn1.Refused {
  let seq = @asn1.only(v)
  if seq.tag != 0x30 {
    raise @asn1.Malformed("basicConstraints is not a SEQUENCE")
  }
  let items = seq.items()
  let mut i = 0
  let mut ca = false
  if i < items.length() && items[i].tag == 0x01 {
    ca = items[i].bool()
    if !ca {
      raise @asn1.Malformed("basicConstraints spells out the default cA")
    }
    i = i + 1
  }
  let mut path : Int32? = None
  if i < items.length() && items[i].tag == 0x02 {
    if !ca {
      raise @asn1.Malformed("pathLenConstraint without cA")
    }
    let n = items[i].int()
    if n < (0 : BigInt) {
      raise @asn1.Malformed("negative pathLenConstraint")
    }
    if n > (255 : BigInt) {
      raise @asn1.Unsupported("pathLenConstraint over 255")
    }
    path = Some(n.to_int())
    i = i + 1
  }
  if i != items.length() {
    raise @asn1.Malformed("unexpected field in basicConstraints")
  }
  (ca, path)
}

///|
/// An ExtKeyUsageSyntax (RFC 5280 §4.2.1.12): the key purposes, of which there is at least one.
fn read_eku(v : BytesView) -> Array[String] raise @asn1.Refused {
  let seq = @asn1.only(v)
  if seq.tag != 0x30 {
    raise @asn1.Malformed("extKeyUsage is not a SEQUENCE")
  }
  let out = seq.items().map(o => o.oid())
  if out.length() == 0 {
    raise @asn1.Malformed("empty extKeyUsage")
  }
  out
}

///|
/// An AuthorityKeyIdentifier's keyIdentifier (RFC 5280 §4.2.1.1), when it has one; the issuer
/// name and serial form is read past.
fn read_aki(v : BytesView) -> Bytes? raise @asn1.Refused {
  let seq = @asn1.only(v)
  if seq.tag != 0x30 {
    raise @asn1.Malformed("authorityKeyIdentifier is not a SEQUENCE")
  }
  for f in seq.items() {
    if f.tag == 0x80 {
      return Some(f.value.to_owned())
    }
  }
  None
}

///|
/// A GeneralSubtrees (RFC 5280 §4.2.1.10). `minimum` and `maximum` are fixed by the profile — the
/// first must be left at zero and the second out — so a subtree that sets either is refused
/// rather than read with a meaning nobody implements.
fn read_subtrees(t : @asn1.Tlv) -> Array[GeneralName] raise @asn1.Refused {
  let out : Array[GeneralName] = []
  for s in t.items() {
    if s.tag != 0x30 {
      raise @asn1.Malformed("GeneralSubtree is not a SEQUENCE")
    }
    let parts = s.items()
    if parts.length() != 1 {
      raise @asn1.Malformed("GeneralSubtree sets minimum or maximum")
    }
    out.push(read_general(parts[0], constraint=true))
  }
  if out.length() == 0 {
    raise @asn1.Malformed("empty GeneralSubtrees")
  }
  out
}

///|
/// A NameConstraints (RFC 5280 §4.2.1.10).
fn read_constraints(v : BytesView) -> Constraints raise @asn1.Refused {
  let seq = @asn1.only(v)
  if seq.tag != 0x30 {
    raise @asn1.Malformed("nameConstraints is not a SEQUENCE")
  }
  let mut permitted = []
  let mut excluded = []
  let items = seq.items()
  if items.length() == 0 {
    raise @asn1.Malformed("empty nameConstraints")
  }
  let mut prev = -1
  for f in items {
    let n = f.tag & 0x1f
    if (f.tag & 0xe0) != 0xa0 || n > 1 || n <= prev {
      raise @asn1.Malformed("unexpected field in nameConstraints")
    }
    prev = n
    if n == 0 {
      permitted = read_subtrees(f)
    } else {
      excluded = read_subtrees(f)
    }
  }
  { permitted, excluded, }
}

///|
/// The `[3] EXPLICIT Extensions` of a v3 certificate (RFC 5280 §4.1.2.9).
fn read_exts(t : @asn1.Tlv) -> Exts raise @asn1.Refused {
  let seq = @asn1.only(t.value)
  if seq.tag != 0x30 {
    raise @asn1.Malformed("extensions are not a SEQUENCE")
  }
  let items = seq.items()
  if items.length() == 0 {
    raise @asn1.Malformed("empty extensions")
  }
  let seen : Array[String] = []
  let mut exts = no_exts
  let unknown_critical : Array[String] = []
  for e in items {
    if e.tag != 0x30 {
      raise @asn1.Malformed("extension is not a SEQUENCE")
    }
    let parts = e.items()
    if parts.length() < 2 || parts.length() > 3 {
      raise @asn1.Malformed("extension is not an id, a criticality and a value")
    }
    let oid = parts[0].oid()
    if seen.contains(oid) {
      raise @asn1.Malformed("extension " + oid + " appears twice")
    }
    seen.push(oid)
    let critical = if parts.length() == 3 {
      if !parts[1].bool() {
        raise @asn1.Malformed("extension spells out the default criticality")
      }
      true
    } else {
      false
    }
    let v = parts[parts.length() - 1].octets()
    match oid {
      "2.5.29.17" => exts = { ..exts, san: read_san(v), }
      "2.5.29.19" => {
        let (ca, path_len) = read_basic(v)
        exts = { ..exts, ca, path_len, }
      }
      "2.5.29.15" => exts = { ..exts, key_usage: Some(@asn1.only(v).bits()), }
      "2.5.29.37" => exts = { ..exts, eku: Some(read_eku(v)), }
      "2.5.29.14" =>
        exts = { ..exts, ski: Some(@asn1.only(v).octets().to_owned()), }
      "2.5.29.35" => exts = { ..exts, aki: read_aki(v), }
      "2.5.29.30" => exts = { ..exts, constraints: Some(read_constraints(v)), }
      _ => if critical { unknown_critical.push(oid) }
    }
  }
  { ..exts, unknown_critical, }
}

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

///|
pub extend Exts with Eq::{not_equal, equal}

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

///|
pub extend GeneralName with Eq::{not_equal, equal}

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

///|
pub extend Constraints with Eq::{not_equal, equal}