package spiffebundle

import (
	"crypto"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"sync"
	"time"

	"github.com/go-jose/go-jose/v4"
	"github.com/spiffe/go-spiffe/v2/bundle/jwtbundle"
	"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
	"github.com/spiffe/go-spiffe/v2/internal/jwtutil"
	"github.com/spiffe/go-spiffe/v2/internal/x509util"
	"github.com/spiffe/go-spiffe/v2/spiffeid"
)

const (
	x509SVIDUse = "x509-svid"
	jwtSVIDUse  = "jwt-svid"
)

type bundleDoc struct {
	jose.JSONWebKeySet
	SequenceNumber *uint64 `json:"spiffe_sequence,omitempty"`
	RefreshHint    *int64  `json:"spiffe_refresh_hint,omitempty"`
}

// Bundle is a collection of trusted public key material for a trust domain,
// conforming to the SPIFFE Bundle Format as part of the SPIFFE Trust Domain
// and Bundle specification:
// https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md
type Bundle struct {
	trustDomain spiffeid.TrustDomain

	mtx             sync.RWMutex
	refreshHint     *time.Duration
	sequenceNumber  *uint64
	jwtAuthorities  map[string]crypto.PublicKey
	x509Authorities []*x509.Certificate
}

// New creates a new bundle.
func New(trustDomain spiffeid.TrustDomain) *Bundle {
	return &Bundle{
		trustDomain:    trustDomain,
		jwtAuthorities: make(map[string]crypto.PublicKey),
	}
}

// Load loads a bundle from a file on disk. The file must contain a JWKS
// document following the SPIFFE Trust Domain and Bundle specification.
func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) {
	bundleBytes, err := os.ReadFile(path)
	if err != nil {
		return nil, wrapSpiffebundleErr(fmt.Errorf("unable to read SPIFFE bundle: %w", err))
	}

	return Parse(trustDomain, bundleBytes)
}

// Read decodes a bundle from a reader. The contents must contain a JWKS
// document following the SPIFFE Trust Domain and Bundle specification.
func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) {
	b, err := io.ReadAll(r)
	if err != nil {
		return nil, wrapSpiffebundleErr(fmt.Errorf("unable to read: %v", err))
	}

	return Parse(trustDomain, b)
}

// Parse parses a bundle from bytes. The data must be a JWKS document following
// the SPIFFE Trust Domain and Bundle specification.
func Parse(trustDomain spiffeid.TrustDomain, bundleBytes []byte) (*Bundle, error) {
	jwks := &bundleDoc{}
	if err := json.Unmarshal(bundleBytes, jwks); err != nil {
		return nil, wrapSpiffebundleErr(fmt.Errorf("unable to parse JWKS: %v", err))
	}

	bundle := New(trustDomain)
	if jwks.RefreshHint != nil {
		bundle.SetRefreshHint(time.Second * time.Duration(*jwks.RefreshHint))
	}
	if jwks.SequenceNumber != nil {
		bundle.SetSequenceNumber(*jwks.SequenceNumber)
	}

	if jwks.Keys == nil {
		// The parameter keys MUST be present.
		// https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#413-keys
		return nil, wrapSpiffebundleErr(errors.New("no authorities found"))
	}
	for i, key := range jwks.Keys {
		switch key.Use {
		// Two SVID types are supported: x509-svid and jwt-svid.
		case x509SVIDUse:
			if len(key.Certificates) != 1 {
				return nil, wrapSpiffebundleErr(fmt.Errorf("expected a single certificate in %s entry %d; got %d", x509SVIDUse, i, len(key.Certificates)))
			}
			bundle.AddX509Authority(key.Certificates[0])
		case jwtSVIDUse:
			if err := bundle.AddJWTAuthority(key.KeyID, key.Key); err != nil {
				return nil, wrapSpiffebundleErr(fmt.Errorf("error adding authority %d of JWKS: %v", i, errors.Unwrap(err)))
			}
		}
	}

	return bundle, nil
}

// FromX509Bundle creates a bundle from an X.509 bundle.
// The function panics in case of a nil X.509 bundle.
func FromX509Bundle(x509Bundle *x509bundle.Bundle) *Bundle {
	bundle := New(x509Bundle.TrustDomain())
	bundle.x509Authorities = x509Bundle.X509Authorities()
	return bundle
}

// FromJWTBundle creates a bundle from a JWT bundle.
// The function panics in case of a nil JWT bundle.
func FromJWTBundle(jwtBundle *jwtbundle.Bundle) *Bundle {
	bundle := New(jwtBundle.TrustDomain())
	bundle.jwtAuthorities = jwtBundle.JWTAuthorities()
	return bundle
}

// FromX509Authorities creates a bundle from X.509 certificates.
func FromX509Authorities(trustDomain spiffeid.TrustDomain, x509Authorities []*x509.Certificate) *Bundle {
	bundle := New(trustDomain)
	bundle.x509Authorities = x509util.CopyX509Authorities(x509Authorities)
	return bundle
}

// FromJWTAuthorities creates a new bundle from JWT authorities.
func FromJWTAuthorities(trustDomain spiffeid.TrustDomain, jwtAuthorities map[string]crypto.PublicKey) *Bundle {
	bundle := New(trustDomain)
	bundle.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities)
	return bundle
}

// TrustDomain returns the trust domain that the bundle belongs to.
func (b *Bundle) TrustDomain() spiffeid.TrustDomain {
	return b.trustDomain
}

// X509Authorities returns the X.509 authorities in the bundle.
func (b *Bundle) X509Authorities() []*x509.Certificate {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	return x509util.CopyX509Authorities(b.x509Authorities)
}

// AddX509Authority adds an X.509 authority to the bundle. If the authority already
// exists in the bundle, the contents of the bundle will remain unchanged.
func (b *Bundle) AddX509Authority(x509Authority *x509.Certificate) {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	for _, r := range b.x509Authorities {
		if r.Equal(x509Authority) {
			return
		}
	}

	b.x509Authorities = append(b.x509Authorities, x509Authority)
}

// RemoveX509Authority removes an X.509 authority from the bundle.
func (b *Bundle) RemoveX509Authority(x509Authority *x509.Certificate) {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	for i, r := range b.x509Authorities {
		if r.Equal(x509Authority) {
			b.x509Authorities = append(b.x509Authorities[:i], b.x509Authorities[i+1:]...)
			return
		}
	}
}

// HasX509Authority checks if the given X.509 authority exists in the bundle.
func (b *Bundle) HasX509Authority(x509Authority *x509.Certificate) bool {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	for _, r := range b.x509Authorities {
		if r.Equal(x509Authority) {
			return true
		}
	}
	return false
}

// SetX509Authorities sets the X.509 authorities in the bundle.
func (b *Bundle) SetX509Authorities(authorities []*x509.Certificate) {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	b.x509Authorities = x509util.CopyX509Authorities(authorities)
}

// JWTAuthorities returns the JWT authorities in the bundle, keyed by key ID.
func (b *Bundle) JWTAuthorities() map[string]crypto.PublicKey {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	return jwtutil.CopyJWTAuthorities(b.jwtAuthorities)
}

// FindJWTAuthority finds the JWT authority with the given key ID from the bundle. If the authority
// is found, it is returned and the boolean is true. Otherwise, the returned
// value is nil and the boolean is false.
func (b *Bundle) FindJWTAuthority(keyID string) (crypto.PublicKey, bool) {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	jwtAuthority, ok := b.jwtAuthorities[keyID]
	return jwtAuthority, ok
}

// HasJWTAuthority returns true if the bundle has a JWT authority with the given key ID.
func (b *Bundle) HasJWTAuthority(keyID string) bool {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	_, ok := b.jwtAuthorities[keyID]
	return ok
}

// AddJWTAuthority adds a JWT authority to the bundle. If a JWT authority already exists
// under the given key ID, it is replaced. A key ID must be specified.
func (b *Bundle) AddJWTAuthority(keyID string, jwtAuthority crypto.PublicKey) error {
	if keyID == "" {
		return wrapSpiffebundleErr(errors.New("keyID cannot be empty"))
	}

	b.mtx.Lock()
	defer b.mtx.Unlock()

	b.jwtAuthorities[keyID] = jwtAuthority
	return nil
}

// RemoveJWTAuthority removes the JWT authority identified by the key ID from the bundle.
func (b *Bundle) RemoveJWTAuthority(keyID string) {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	delete(b.jwtAuthorities, keyID)
}

// SetJWTAuthorities sets the JWT authorities in the bundle.
func (b *Bundle) SetJWTAuthorities(jwtAuthorities map[string]crypto.PublicKey) {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	b.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities)
}

// Empty returns true if the bundle has no X.509 and JWT authorities.
func (b *Bundle) Empty() bool {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	return len(b.x509Authorities) == 0 && len(b.jwtAuthorities) == 0
}

// RefreshHint returns the refresh hint. If the refresh hint is set in
// the bundle, it is returned and the boolean is true. Otherwise, the returned
// value is zero and the boolean is false.
func (b *Bundle) RefreshHint() (refreshHint time.Duration, ok bool) {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	if b.refreshHint != nil {
		return *b.refreshHint, true
	}
	return 0, false
}

// SetRefreshHint sets the refresh hint. The refresh hint value will be
// truncated to time.Second.
func (b *Bundle) SetRefreshHint(refreshHint time.Duration) {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	b.refreshHint = &refreshHint
}

// ClearRefreshHint clears the refresh hint.
func (b *Bundle) ClearRefreshHint() {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	b.refreshHint = nil
}

// SequenceNumber returns the sequence number. If the sequence number is set in
// the bundle, it is returned and the boolean is true. Otherwise, the returned
// value is zero and the boolean is false.
func (b *Bundle) SequenceNumber() (uint64, bool) {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	if b.sequenceNumber != nil {
		return *b.sequenceNumber, true
	}
	return 0, false
}

// SetSequenceNumber sets the sequence number.
func (b *Bundle) SetSequenceNumber(sequenceNumber uint64) {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	b.sequenceNumber = &sequenceNumber
}

// ClearSequenceNumber clears the sequence number.
func (b *Bundle) ClearSequenceNumber() {
	b.mtx.Lock()
	defer b.mtx.Unlock()

	b.sequenceNumber = nil
}

// Marshal marshals the bundle according to the SPIFFE Trust Domain and Bundle
// specification. The trust domain is not marshaled as part of the bundle and
// must be conveyed separately. See the specification for details.
func (b *Bundle) Marshal() ([]byte, error) {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	jwks := bundleDoc{}
	if b.refreshHint != nil {
		tr := int64((*b.refreshHint + (time.Second - 1)) / time.Second)
		jwks.RefreshHint = &tr
	}
	jwks.SequenceNumber = b.sequenceNumber
	for _, x509Authority := range b.x509Authorities {
		jwks.Keys = append(jwks.Keys, jose.JSONWebKey{
			Key:          x509Authority.PublicKey,
			Certificates: []*x509.Certificate{x509Authority},
			Use:          x509SVIDUse,
		})
	}

	for keyID, jwtAuthority := range b.jwtAuthorities {
		jwks.Keys = append(jwks.Keys, jose.JSONWebKey{
			Key:   jwtAuthority,
			KeyID: keyID,
			Use:   jwtSVIDUse,
		})
	}

	return json.Marshal(jwks)
}

// Clone clones the bundle.
func (b *Bundle) Clone() *Bundle {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	return &Bundle{
		trustDomain:     b.trustDomain,
		refreshHint:     copyRefreshHint(b.refreshHint),
		sequenceNumber:  copySequenceNumber(b.sequenceNumber),
		x509Authorities: x509util.CopyX509Authorities(b.x509Authorities),
		jwtAuthorities:  jwtutil.CopyJWTAuthorities(b.jwtAuthorities),
	}
}

// X509Bundle returns an X.509 bundle containing the X.509 authorities in the SPIFFE
// bundle.
func (b *Bundle) X509Bundle() *x509bundle.Bundle {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	// FromX509Authorities makes a copy, so we can pass our internal slice directly.
	return x509bundle.FromX509Authorities(b.trustDomain, b.x509Authorities)
}

// JWTBundle returns a JWT bundle containing the JWT authorities in the SPIFFE bundle.
func (b *Bundle) JWTBundle() *jwtbundle.Bundle {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	// FromJWTBundle makes a copy, so we can pass our internal slice directly.
	return jwtbundle.FromJWTAuthorities(b.trustDomain, b.jwtAuthorities)
}

// GetBundleForTrustDomain returns the SPIFFE bundle for the given trust
// domain. It implements the Source interface. An error will be returned if the
// trust domain does not match that of the bundle.
func (b *Bundle) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	if b.trustDomain != trustDomain {
		return nil, wrapSpiffebundleErr(fmt.Errorf("no SPIFFE bundle for trust domain %q", trustDomain))
	}

	return b, nil
}

// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust
// domain. It implements the x509bundle.Source interface. An error will be
// returned if the trust domain does not match that of the bundle.
func (b *Bundle) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	if b.trustDomain != trustDomain {
		return nil, wrapSpiffebundleErr(fmt.Errorf("no X.509 bundle for trust domain %q", trustDomain))
	}

	return b.X509Bundle(), nil
}

// GetJWTBundleForTrustDomain returns the JWT bundle of the given trust domain.
// It implements the jwtbundle.Source interface. An error will be returned if
// the trust domain does not match that of the bundle.
func (b *Bundle) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) {
	b.mtx.RLock()
	defer b.mtx.RUnlock()

	if b.trustDomain != trustDomain {
		return nil, wrapSpiffebundleErr(fmt.Errorf("no JWT bundle for trust domain %q", trustDomain))
	}

	return b.JWTBundle(), nil
}

// Equal compares the bundle for equality against the given bundle.
func (b *Bundle) Equal(other *Bundle) bool {
	if b == nil || other == nil {
		return b == other
	}

	return b.trustDomain == other.trustDomain &&
		refreshHintEqual(b.refreshHint, other.refreshHint) &&
		sequenceNumberEqual(b.sequenceNumber, other.sequenceNumber) &&
		jwtutil.JWTAuthoritiesEqual(b.jwtAuthorities, other.jwtAuthorities) &&
		x509util.CertsEqual(b.x509Authorities, other.x509Authorities)
}

func refreshHintEqual(a, b *time.Duration) bool {
	if a == nil || b == nil {
		return a == b
	}

	return *a == *b
}

func sequenceNumberEqual(a, b *uint64) bool {
	if a == nil || b == nil {
		return a == b
	}

	return *a == *b
}

func copyRefreshHint(refreshHint *time.Duration) *time.Duration {
	if refreshHint == nil {
		return nil
	}
	copied := *refreshHint
	return &copied
}

func copySequenceNumber(sequenceNumber *uint64) *uint64 {
	if sequenceNumber == nil {
		return nil
	}
	copied := *sequenceNumber
	return &copied
}

func wrapSpiffebundleErr(err error) error {
	return fmt.Errorf("spiffebundle: %w", err)
}
