// Package x509ext provides functions for (un)marshalling X.509 extensions not
// supported by the crypto/x509 package.
package x509ext

import (
	"crypto/x509/pkix"
	"encoding/asn1"
	"errors"
	"fmt"

	"github.com/smallstep/go-attestation/oid"
)

// RFC 4043
//
// https://tools.ietf.org/html/rfc4043
var (
	oidPermanentIdentifier = []int{1, 3, 6, 1, 5, 5, 7, 8, 3}
)

//	OtherName ::= SEQUENCE {
//	  type-id    OBJECT IDENTIFIER,
//	  value      [0] EXPLICIT ANY DEFINED BY type-id }
type otherName struct {
	TypeID asn1.ObjectIdentifier
	Value  asn1.RawValue
}

func marshalOtherName(typeID asn1.ObjectIdentifier, value interface{}) (asn1.RawValue, error) {
	valueBytes, err := asn1.MarshalWithParams(value, "explicit,tag:0")
	if err != nil {
		return asn1.RawValue{}, err
	}
	otherName := otherName{
		TypeID: typeID,
		Value:  asn1.RawValue{FullBytes: valueBytes},
	}
	bytes, err := asn1.MarshalWithParams(otherName, "tag:0")
	if err != nil {
		return asn1.RawValue{}, err
	}
	return asn1.RawValue{FullBytes: bytes}, nil
}

// PermanentIdentifier represents an ASN.1 encoded "permanent identifier" as
// defined by RFC4043.
//
//	PermanentIdentifier ::= SEQUENCE {
//	    identifierValue    UTF8String OPTIONAL,
//	    assigner           OBJECT IDENTIFIER OPTIONAL
//	   }
//
// https://datatracker.ietf.org/doc/html/rfc4043
type PermanentIdentifier struct {
	IdentifierValue string                `asn1:"utf8,optional"`
	Assigner        asn1.ObjectIdentifier `asn1:"optional"`
}

func parsePermanentIdentifier(der []byte) (PermanentIdentifier, error) {
	var permID PermanentIdentifier
	if _, err := asn1.UnmarshalWithParams(der, &permID, "explicit,tag:0"); err != nil {
		return PermanentIdentifier{}, err
	}
	return permID, nil
}

// SubjectAltName contains GeneralName variations not supported by the
// crypto/x509 package.
//
// https://datatracker.ietf.org/doc/html/rfc5280
type SubjectAltName struct {
	DirectoryNames       []pkix.Name
	PermanentIdentifiers []PermanentIdentifier
}

// ParseSubjectAltName parses a pkix.Extension into a SubjectAltName struct.
func ParseSubjectAltName(ext pkix.Extension) (*SubjectAltName, error) {
	var out SubjectAltName
	dirNames, otherNames, err := parseSubjectAltName(ext)
	if err != nil {
		return nil, fmt.Errorf("parseSubjectAltName: %v", err)
	}
	out.DirectoryNames = dirNames

	for _, otherName := range otherNames {
		if otherName.TypeID.Equal(oidPermanentIdentifier) {
			permID, err := parsePermanentIdentifier(otherName.Value.FullBytes)
			if err != nil {
				return nil, fmt.Errorf("parsePermanentIdentifier: %v", err)
			}
			out.PermanentIdentifiers = append(out.PermanentIdentifiers, permID)
		}
	}
	return &out, nil
}

// https://datatracker.ietf.org/doc/html/rfc5280#page-35
func parseSubjectAltName(ext pkix.Extension) (dirNames []pkix.Name, otherNames []otherName, err error) {
	err = forEachSAN(ext.Value, func(generalName asn1.RawValue) error {
		switch generalName.Tag {
		case 0: // otherName
			var otherName otherName
			if _, err := asn1.UnmarshalWithParams(generalName.FullBytes, &otherName, "tag:0"); err != nil {
				return fmt.Errorf("OtherName: asn1.UnmarshalWithParams: %v", err)
			}
			otherNames = append(otherNames, otherName)
		case 4: // directoryName
			var rdns pkix.RDNSequence
			if _, err := asn1.Unmarshal(generalName.Bytes, &rdns); err != nil {
				return fmt.Errorf("DirectoryName: asn1.Unmarshal: %v", err)
			}
			var dirName pkix.Name
			dirName.FillFromRDNSequence(&rdns)
			dirNames = append(dirNames, dirName)
		default:
			return fmt.Errorf("expected tag %d", generalName.Tag)
		}
		return nil
	})
	return
}

// Borrowed from the x509 package.
func forEachSAN(extension []byte, callback func(ext asn1.RawValue) error) error {
	var seq asn1.RawValue
	rest, err := asn1.Unmarshal(extension, &seq)
	if err != nil {
		return err
	} else if len(rest) != 0 {
		return errors.New("x509: trailing data after X.509 extension")
	}
	if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
		return asn1.StructuralError{Msg: "bad SAN sequence"}
	}

	rest = seq.Bytes
	for len(rest) > 0 {
		var v asn1.RawValue
		rest, err = asn1.Unmarshal(rest, &v)
		if err != nil {
			return err
		}

		if err := callback(v); err != nil {
			return err
		}
	}

	return nil
}

// MarshalSubjectAltName converts a SubjectAltName struct into a pkix.Extension.
func MarshalSubjectAltName(san *SubjectAltName) (pkix.Extension, error) {
	var generalNames []asn1.RawValue
	for _, permID := range san.PermanentIdentifiers {
		val, err := marshalOtherName(oidPermanentIdentifier, permID)
		if err != nil {
			return pkix.Extension{}, err
		}
		generalNames = append(generalNames, val)
	}
	for _, dirName := range san.DirectoryNames {
		bytes, err := asn1.MarshalWithParams(dirName.ToRDNSequence(), "explicit,tag:4")
		if err != nil {
			return pkix.Extension{}, err
		}
		generalNames = append(generalNames, asn1.RawValue{FullBytes: bytes})
	}
	val, err := asn1.Marshal(generalNames)
	if err != nil {
		return pkix.Extension{}, err
	}
	return pkix.Extension{
		Id:    oid.SubjectAltName,
		Value: val,
	}, nil
}
