package jwk

import (
	"fmt"
	"reflect"
	"sync"

	"github.com/lestrrat-go/blackmagic"
	"github.com/lestrrat-go/jwx/v3/internal/json"
	"github.com/lestrrat-go/jwx/v3/jwa"
)

// KeyParser represents a type that can parse a JSON representation of a JWK into
// a jwk.Key.
// See KeyConvertor for a type that can convert a raw key into a jwk.Key
type KeyParser interface {
	// ParseKey parses a JSON payload to a `jwk.Key` object. The first
	// argument is an object that contains some hints as to what kind of
	// key the JSON payload contains.
	//
	// If your KeyParser decides that the payload is not something
	// you can parse, and you would like to continue parsing with
	// the remaining KeyParser instances that are registered,
	// return a `jwk.ContinueError()`. Any other errors will immediately
	// halt the parsing process.
	//
	// When unmarshaling JSON, use the unmarshaler object supplied as
	// the second argument. This will ensure that the JSON is unmarshaled
	// in a way that is compatible with the rest of the library.
	ParseKey(probe *KeyProbe, unmarshaler KeyUnmarshaler, payload []byte) (Key, error)
}

// KeyParseFunc is a type of KeyParser that is based on a function/closure
type KeyParseFunc func(probe *KeyProbe, unmarshaler KeyUnmarshaler, payload []byte) (Key, error)

func (f KeyParseFunc) ParseKey(probe *KeyProbe, unmarshaler KeyUnmarshaler, payload []byte) (Key, error) {
	return f(probe, unmarshaler, payload)
}

// protects keyParsers
var muKeyParser sync.RWMutex

// list of parsers
var keyParsers = []KeyParser{KeyParseFunc(defaultParseKey)}

// RegisterKeyParser adds a new KeyParser. Parsers are called in FILO order.
// That is, the last parser to be registered is called first. There is no
// check for duplicate entries.
func RegisterKeyParser(kp KeyParser) {
	muKeyParser.Lock()
	defer muKeyParser.Unlock()
	keyParsers = append(keyParsers, kp)
}

func defaultParseKey(probe *KeyProbe, unmarshaler KeyUnmarshaler, data []byte) (Key, error) {
	var key Key
	var kty string
	var d json.RawMessage
	if err := probe.Get("Kty", &kty); err != nil {
		return nil, fmt.Errorf(`jwk.Parse: failed to get "kty" hint: %w`, err)
	}
	// We ignore errors from this field, as it's optional
	_ = probe.Get("D", &d)
	switch v, _ := jwa.LookupKeyType(kty); v {
	case jwa.RSA():
		if d != nil {
			key = newRSAPrivateKey()
		} else {
			key = newRSAPublicKey()
		}
	case jwa.EC():
		if d != nil {
			key = newECDSAPrivateKey()
		} else {
			key = newECDSAPublicKey()
		}
	case jwa.OctetSeq():
		key = newSymmetricKey()
	case jwa.OKP():
		if d != nil {
			key = newOKPPrivateKey()
		} else {
			key = newOKPPublicKey()
		}
	default:
		return nil, fmt.Errorf(`invalid key type from JSON (%s)`, kty)
	}

	if err := unmarshaler.UnmarshalKey(data, key); err != nil {
		return nil, fmt.Errorf(`failed to unmarshal JSON into key (%T): %w`, key, err)
	}
	// Enforce the trust boundary: a key that fails its own Validate() must
	// never escape Parse/ParseKey. All built-in key types implement this
	// interface via NewKeyValidationError, so callers can still use
	// jwk.IsKeyValidationError on the wrapped error.
	if v, ok := key.(interface{ Validate() error }); ok {
		if err := v.Validate(); err != nil {
			return nil, fmt.Errorf(`jwk.Parse: key validation failed: %w`, err)
		}
	}
	return key, nil
}

type keyUnmarshaler struct {
	localReg *json.Registry
}

func (ku *keyUnmarshaler) UnmarshalKey(data []byte, key any) error {
	if ku.localReg != nil {
		dcKey, ok := key.(json.DecodeCtxContainer)
		if !ok {
			return fmt.Errorf(`typed field was requested, but the key (%T) does not support DecodeCtx`, key)
		}
		dc := json.NewDecodeCtx(ku.localReg)
		dcKey.SetDecodeCtx(dc)
		defer func() { dcKey.SetDecodeCtx(nil) }()
	}

	if err := json.Unmarshal(data, key); err != nil {
		return fmt.Errorf(`failed to unmarshal JSON into key (%T): %w`, key, err)
	}

	return nil
}

// keyProber is the object that starts the probing. When Probe() is called,
// it creates (possibly from a cached value) an object that is used to
// hold hint values.
type keyProber struct {
	mu     sync.RWMutex
	pool   *sync.Pool
	fields map[string]reflect.StructField
	typ    reflect.Type
}

func (kp *keyProber) AddField(field reflect.StructField) error {
	kp.mu.Lock()
	defer kp.mu.Unlock()

	if _, ok := kp.fields[field.Name]; ok {
		return fmt.Errorf(`field name %s is already registered`, field.Name)
	}
	kp.fields[field.Name] = field
	kp.makeStructType()

	// Update pool (note: the logic is the same, but we need to recreate it
	// so that we don't accidentally use old stored values)
	kp.pool = &sync.Pool{
		New: kp.makeStruct,
	}
	return nil
}

func (kp *keyProber) makeStructType() {
	// DOES NOT LOCK
	fields := make([]reflect.StructField, 0, len(kp.fields))
	for _, f := range kp.fields {
		fields = append(fields, f)
	}
	kp.typ = reflect.StructOf(fields)
}

func (kp *keyProber) makeStruct() any {
	return reflect.New(kp.typ)
}

func (kp *keyProber) Probe(data []byte) (*KeyProbe, error) {
	kp.mu.RLock()
	defer kp.mu.RUnlock()

	// if the field list unchanged, so is the pool object, so effectively
	// we should be using the cached version
	v := kp.pool.Get()
	if v == nil {
		return nil, fmt.Errorf(`probe: failed to get object from pool`)
	}
	rv, ok := v.(reflect.Value)
	if !ok {
		return nil, fmt.Errorf(`probe: value returned from pool as of type %T, expected reflect.Value`, v)
	}

	if err := json.Unmarshal(data, rv.Interface()); err != nil {
		return nil, fmt.Errorf(`probe: failed to unmarshal data: %w`, err)
	}

	return &KeyProbe{data: rv}, nil
}

// KeyProbe is the object that carries the hints when parsing a key.
// The exact list of fields can vary depending on the types of key
// that are registered.
//
// Use `Get()` to access the value of a field.
//
// The underlying data stored in a KeyProbe is recycled each
// time a value is parsed, therefore you are not allowed to hold
// onto this object after ParseKey() is done.
type KeyProbe struct {
	data reflect.Value
}

// Get returns the value of the field with the given `name“.
// `dst` must be a pointer to a value that can hold the type of
// the value of the field, which is determined by the
// field type registered through `jwk.RegisterProbeField()`
func (kp *KeyProbe) Get(name string, dst any) error {
	f := kp.data.Elem().FieldByName(name)
	if !f.IsValid() {
		return fmt.Errorf(`field %s not found`, name)
	}

	if err := blackmagic.AssignIfCompatible(dst, f.Addr().Interface()); err != nil {
		return fmt.Errorf(`failed to assign value of field %q to %T: %w`, name, dst, err)
	}
	return nil
}

// We don't really need the object, we need to know its type
var keyProbe = &keyProber{
	fields: make(map[string]reflect.StructField),
}

// RegisterProbeField adds a new field to be probed during the initial
// phase of parsing. This is done by partially parsing the JSON payload,
// and we do this by calling `json.Unmarshal` using a dynamic type that
// can possibly be modified during runtime. This function is used to
// add a new field to this dynamic type.
//
// Note that the `Name` field for the given `reflect.StructField` must start
// with an upper case alphabet, such that it is treated as an exported field.
// So for example, if you want to probe the "my_hint" field, you should specify
// the field name as "MyHint" or similar.
//
// Also the field name must be unique. If you believe that your field name may
// collide with other packages that may want to add their own probes,
// it is the responsibility of the caller
// to ensure that the field name is unique (possibly by prefixing the field
// name with a unique string). It is important to note that the field name
// need not be the same as the JSON field name. For example, your field name
// could be "MyPkg_MyHint", while the actual JSON field name could be "my_hint".
//
// If the field name is not unique, an error is returned.
func RegisterProbeField(p reflect.StructField) error {
	// locking is done inside keyProbe
	return keyProbe.AddField(p)
}

// KeyUnmarshaler is a thin wrapper around json.Unmarshal. It behaves almost
// exactly like json.Unmarshal, but it allows us to add extra magic that
// is specific to this library before calling the actual json.Unmarshal.
type KeyUnmarshaler interface {
	UnmarshalKey(data []byte, key any) error
}
