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

///|
/// commonName (X.520 §6.2.2), the attribute a name is usually read from.
pub let oid_cn : String = "2.5.4.3"

///|
/// One AttributeTypeAndValue: the attribute's OID in dotted-decimal form, and its value as text.
pub(all) struct Attr {
  oid : String
  value : String
} derive(Eq, Debug)

///|
/// A distinguished name as it is encoded — a sequence of relative distinguished names, each a set
/// of attributes — together with the encoding itself. Path building matches an issuer to its
/// subject by those octets, as webpki and Go do: two readings of one name must not decide whether
/// a certificate chains.
pub(all) struct Name {
  rdns : Array[Array[Attr]]
  raw : Bytes
} derive(Eq, Debug)

///|
/// The value of the first attribute with this OID, in encoding order.
pub fn Name::get(self : Name, oid : String) -> String? {
  for rdn in self.rdns {
    for a in rdn {
      if a.oid == oid {
        return Some(a.value)
      }
    }
  }
  None
}

///|
/// The name's commonName, if it has one.
pub fn Name::cn(self : Name) -> String? {
  self.get(oid_cn)
}

///|
/// An RDNSequence (RFC 5280 §4.1.2.4).
fn read_name(tlv : @asn1.Tlv) -> Name raise @asn1.Refused {
  if tlv.tag != 0x30 {
    raise @asn1.Malformed("Name is not a SEQUENCE")
  }
  let rdns : Array[Array[Attr]] = []
  for rdn in tlv.items() {
    if rdn.tag != 0x31 {
      raise @asn1.Malformed("RelativeDistinguishedName is not a SET")
    }
    let attrs : Array[Attr] = []
    for a in rdn.items() {
      if a.tag != 0x30 {
        raise @asn1.Malformed("AttributeTypeAndValue is not a SEQUENCE")
      }
      let parts = a.items()
      if parts.length() != 2 {
        raise @asn1.Malformed("AttributeTypeAndValue is not a type and a value")
      }
      attrs.push({ oid: parts[0].oid(), value: attr_text(parts[1]), })
    }
    if attrs.length() == 0 {
      raise @asn1.Malformed("empty RelativeDistinguishedName")
    }
    rdns.push(attrs)
  }
  { rdns, raw: tlv.raw.to_owned(), }
}

///|
/// An attribute value as text. Directory strings are decoded; a value of another type (a
/// domainComponent is an IA5String, but an attribute may be anything) keeps its encoding in hex,
/// so a name that carries one still reads instead of refusing the whole certificate.
fn attr_text(t : @asn1.Tlv) -> String raise @asn1.Refused {
  match t.tag {
    0x0c | 0x12 | 0x13 | 0x14 | 0x16 | 0x1a | 0x1e => t.text()
    _ => "#" + @base16.encode(t.raw)
  }
}

///|
/// The RDNs of `name` as their encodings, for the prefix test a directoryName constraint is.
fn rdn_raws(name : Name) -> Array[Bytes] raise @asn1.Refused {
  @asn1.only(name.raw[:]).items().map(r => r.raw.to_owned())
}

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

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

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

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