//
// Copyright (c) 2026, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   16 Apr 2026 Ross Schwalm Creation
//

**
** San defines the api for a Subject Alternative Name based on RFC 5280.
**
const class San
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  ** Create as [SanType.dnsName]
  static new dnsName(Str name) { San(SanType.dnsName, name) }

  ** Create as [SanType.uri] - Accepts Uri or Str (value stored as Str)
  static new uri(Obj uri)
  {
    //Store uri as a string to avoid adding trailing slash
    if (uri is Str)
    {
      if (Uri.fromStr(uri).isRel) throw ArgErr("Parameter must not be a relative Uri")
      return San(SanType.uri, uri.toStr)
    }
    else if (uri is Uri) return San.uri(uri.toStr)
    throw ArgErr("Parameter must be Uri or Str")
  }

  ** Create as [SanType.ipAddr] - Accepts IpAddr or Str (value stored as IpAddr)
  static new ipAddr(Obj ip)
  {
    IpAddr := Type.find("inet::IpAddr")
    if (ip is Str) return San(SanType.ipAddr, IpAddr.make([ip]))
    else if (IpAddr.fits(ip.typeof)) return San(SanType.ipAddr, ip)
    throw ArgErr("Parameter must be IpAddr or Str")
  }

  ** Create as [SanType.rfc822Name]
  static new rfc822Name(Str email) { San(SanType.rfc822Name, email) }

  ** Create as [SanType.dirName]
  static new dirName(Str dn) { San(SanType.dirName, dn) }

  ** Create as [SanType.otherName]
  static new otherName(Buf buf) { San(SanType.otherName, buf.toImmutable) }

  ** Create as [SanType.registeredId] - Accepts AsnOid or Str (value stored as Str)
  static new registeredId(Obj oid)
  {
    AsnOid := Type.find("asn1::AsnOid")
    if (AsnOid.fits(oid.typeof)) return San(SanType.registeredId, oid->oidStr)
    else if (oid is Str) return San(SanType.registeredId, (Str)oid)
    throw ArgErr("Parameter must be AsnOid or Str")
  }

  private new make(SanType type, Obj val)
  {
    this.type = type
    this.val = val
  }

  ** Convenience for creating a San from a value.
  **
  ** The `value` may be one of the following types:
  **  - `Str`:    returns San.dnsName
  **  - `Uri`:    returns San.uri
  **  - `AsnOid`: returns San.registeredId
  **  - `Buf`:    returns San.otherName
  **  - `IpAddr`: returns San.ipAddr
  **  - `San`:    returns itself
  static new fromValue(Obj value)
  {
    if (value is Str)         return San.dnsName(value)
    else if (value is Uri)    return San.uri(value)
    else if (value is Buf)    return San.otherName(((Buf)value).toImmutable)
    else if (value is San)    return value
    else if (Type.find("asn1::AsnOid").fits(value.typeof))
      return San.registeredId(value)
    else if (Type.find("inet::IpAddr").fits(value.typeof))
      return San.ipAddr(value)
    else throw ArgErr("Unsupported value: ${value} (${value.typeof})")
  }

//////////////////////////////////////////////////////////////////////////
// Identity
//////////////////////////////////////////////////////////////////////////

  ** RFC5280 Type
  const SanType type

  ** Get the value determined by the SanType:
  **  - [SanType.dnsName]\:        Str
  **  - [SanType.rfc822Name]\:     Str
  **  - [SanType.uri]\:            Str
  **  - [SanType.registeredId]\:   Str
  **  - [SanType.dirName]\:        Str
  **  - [SanType.ipAddr]\:         IpAddr
  **  - [SanType.otherName]\:      Buf (DER-encoded SEQUENCE)
  const Obj val

  ** Get a friendly encoding using the format: {SanType.text}:{value}
  override Str toStr()
  {
    if (type == SanType.otherName) return "${type.text}:<bytes>"
    return "${type.text}:${val}"
  }

}

**************************************************************************
** SanType
**************************************************************************

enum class SanType
{
  otherName(0, "otherName"),
  rfc822Name(1, "email"),
  dnsName(2, "DNS"),
  x400Addr(3, "X400"),
  dirName(4, "dirName"),
  ediPartyName(5, "ediPartyName"),
  uri(6, "URI"),
  ipAddr(7, "IP"),
  registeredId(8, "registeredId")

  private new make(Int tagId, Str text)
  {
    this.text = text
    this.tagId = tagId
  }

  static SanType? fromTagId(Int tagId, Bool checked := true)
  {
    v := vals.find { it.tagId == tagId }
    if (v != null) return v
    if (checked) throw UnsupportedErr("Unsupported tag id: ${tagId}")
    return null
  }

  const Int tagId
  const Str text
}