package jwt

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/lestrrat-go/jwx/v3/jwa"
	"github.com/lestrrat-go/jwx/v3/jwe"
	"github.com/lestrrat-go/jwx/v3/jwk"
	"github.com/lestrrat-go/jwx/v3/jws"
	"github.com/lestrrat-go/option/v2"
)

type identInsecureNoSignature struct{}
type identKey struct{}
type identKeySet struct{}
type identTypedClaim struct{}
type identVerifyAuto struct{}

func toSignOptions(options ...Option) ([]jws.SignOption, error) {
	soptions := make([]jws.SignOption, 0, len(options))
	for _, option := range options {
		switch option.Ident() {
		case identInsecureNoSignature{}:
			soptions = append(soptions, jws.WithInsecureNoSignature())
		case identKey{}:
			var wk withKey
			if err := option.Value(&wk); err != nil {
				return nil, fmt.Errorf(`toSignOtpions: failed to convert option value to withKey: %w`, err)
			}
			var wksoptions []jws.WithKeySuboption
			for _, subopt := range wk.options {
				wksopt, ok := subopt.(jws.WithKeySuboption)
				if !ok {
					return nil, fmt.Errorf(`expected optional arguments in jwt.WithKey to be jws.WithKeySuboption, but got %T`, subopt)
				}
				wksoptions = append(wksoptions, wksopt)
			}

			soptions = append(soptions, jws.WithKey(wk.alg, wk.key, wksoptions...))
		case identSignOption{}:
			var sigOpt jws.SignOption
			if err := option.Value(&sigOpt); err != nil {
				return nil, fmt.Errorf(`failed to decode SignOption: %w`, err)
			}
			soptions = append(soptions, sigOpt)
		case identBase64Encoder{}:
			var enc jws.Base64Encoder
			if err := option.Value(&enc); err != nil {
				return nil, fmt.Errorf(`failed to decode Base64Encoder: %w`, err)
			}
			soptions = append(soptions, jws.WithBase64Encoder(enc))
		}
	}
	return soptions, nil
}

func toEncryptOptions(options ...Option) ([]jwe.EncryptOption, error) {
	soptions := make([]jwe.EncryptOption, 0, len(options))
	for _, option := range options {
		switch option.Ident() {
		case identKey{}:
			var wk withKey
			if err := option.Value(&wk); err != nil {
				return nil, fmt.Errorf(`toEncryptOptions: failed to convert option value to withKey: %w`, err)
			}
			var wksoptions []jwe.WithKeySuboption
			for _, subopt := range wk.options {
				wksopt, ok := subopt.(jwe.WithKeySuboption)
				if !ok {
					return nil, fmt.Errorf(`expected optional arguments in jwt.WithKey to be jwe.WithKeySuboption, but got %T`, subopt)
				}
				wksoptions = append(wksoptions, wksopt)
			}

			soptions = append(soptions, jwe.WithKey(wk.alg, wk.key, wksoptions...))
		case identEncryptOption{}:
			var encOpt jwe.EncryptOption
			if err := option.Value(&encOpt); err != nil {
				return nil, fmt.Errorf(`failed to decode EncryptOption: %w`, err)
			}
			soptions = append(soptions, encOpt)
		}
	}
	return soptions, nil
}

func toVerifyOptions(options ...Option) ([]jws.VerifyOption, error) {
	voptions := make([]jws.VerifyOption, 0, len(options))
	for _, option := range options {
		switch option.Ident() {
		case identKey{}:
			var wk withKey
			if err := option.Value(&wk); err != nil {
				return nil, fmt.Errorf(`toVerifyOptions: failed to convert option value to withKey: %w`, err)
			}
			var wksoptions []jws.WithKeySuboption
			for _, subopt := range wk.options {
				wksopt, ok := subopt.(jws.WithKeySuboption)
				if !ok {
					return nil, fmt.Errorf(`expected optional arguments in jwt.WithKey to be jws.WithKeySuboption, but got %T`, subopt)
				}
				wksoptions = append(wksoptions, wksopt)
			}

			voptions = append(voptions, jws.WithKey(wk.alg, wk.key, wksoptions...))
		case identKeySet{}:
			var wks withKeySet
			if err := option.Value(&wks); err != nil {
				return nil, fmt.Errorf(`failed to convert option value to withKeySet: %w`, err)
			}
			var wkssoptions []jws.WithKeySetSuboption
			for _, subopt := range wks.options {
				wkssopt, ok := subopt.(jws.WithKeySetSuboption)
				if !ok {
					return nil, fmt.Errorf(`expected optional arguments in jwt.WithKey to be jws.WithKeySetSuboption, but got %T`, subopt)
				}
				wkssoptions = append(wkssoptions, wkssopt)
			}

			voptions = append(voptions, jws.WithKeySet(wks.set, wkssoptions...))
		case identVerifyAuto{}:
			var vo jws.VerifyOption
			if err := option.Value(&vo); err != nil {
				return nil, fmt.Errorf(`failed to decode VerifyOption: %w`, err)
			}
			voptions = append(voptions, vo)
		case identKeyProvider{}:
			var kp jws.KeyProvider
			if err := option.Value(&kp); err != nil {
				return nil, fmt.Errorf(`failed to decode KeyProvider: %w`, err)
			}
			voptions = append(voptions, jws.WithKeyProvider(kp))
		case identBase64Encoder{}:
			var enc jws.Base64Encoder
			if err := option.Value(&enc); err != nil {
				return nil, fmt.Errorf(`failed to decode Base64Encoder: %w`, err)
			}
			voptions = append(voptions, jws.WithBase64Encoder(enc))
		case identContext{}:
			var ctx context.Context
			if err := option.Value(&ctx); err != nil {
				return nil, fmt.Errorf(`failed to decode Context: %w`, err)
			}
			voptions = append(voptions, jws.WithContext(ctx))
		default:
			return nil, fmt.Errorf(`invalid jws.VerifyOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`))
		}
	}
	return voptions, nil
}

type withKey struct {
	alg     jwa.KeyAlgorithm
	key     any
	options []Option
}

// WithKey is a multipurpose option. It can be used for either jwt.Sign, jwt.Parse (and
// its siblings), and jwt.Serializer methods. For signatures, please see the documentation
// for `jws.WithKey` for more details. For encryption, please see the documentation
// for `jwe.WithKey`.
//
// It is the caller's responsibility to match the suboptions to the operation that they
// are performing. For example, you are not allowed to do this, because the operation
// is to generate a signature, and yet you are passing options for jwe:
//
//	jwt.Sign(token, jwt.WithKey(alg, key, jweOptions...))
//
// In the above example, the creation of the option via `jwt.WithKey()` will work, but
// when `jwt.Sign()` is called, the fact that you passed JWE suboptions will be
// detected, and an error will occur.
func WithKey(alg jwa.KeyAlgorithm, key any, suboptions ...Option) SignEncryptParseOption {
	return &signEncryptParseOption{option.New(identKey{}, &withKey{
		alg:     alg,
		key:     key,
		options: suboptions,
	})}
}

type withKeySet struct {
	set     jwk.Set
	options []any
}

// WithKeySet forces the Parse method to verify the JWT message
// using one of the keys in the given key set.
//
// Key IDs (`kid`) in the JWS message and the JWK in the given `jwk.Set`
// must match in order for the key to be a candidate to be used for
// verification.
//
// This is for security reasons. If you must disable it, you can do so by
// specifying `jws.WithRequireKid(false)` in the suboptions. But we don't
// recommend it unless you know exactly what the security implications are
//
// When using this option, keys MUST have a proper 'alg' field
// set. This is because we need to know the exact algorithm that
// you (the user) wants to use to verify the token. We do NOT
// trust the token's headers, because they can easily be tampered with.
//
// However, there _is_ a workaround if you do understand the risks
// of allowing a library to automatically choose a signature verification strategy,
// and you do not mind the verification process having to possibly
// attempt using multiple times before succeeding to verify. See
// `jws.InferAlgorithmFromKey` option
func WithKeySet(set jwk.Set, options ...any) ParseOption {
	return &parseOption{option.New(identKeySet{}, &withKeySet{
		set:     set,
		options: options,
	})}
}

// WithIssuer specifies that expected issuer value. If not specified,
// the value of issuer is not verified at all.
func WithIssuer(s string) ValidateOption {
	return WithValidator(issuerClaimValueIs(s))
}

// WithSubject specifies that expected subject value. If not specified,
// the value of subject is not verified at all.
func WithSubject(s string) ValidateOption {
	return WithValidator(ClaimValueIs(SubjectKey, s))
}

// WithJwtID specifies that expected jti value. If not specified,
// the value of jti is not verified at all.
func WithJwtID(s string) ValidateOption {
	return WithValidator(ClaimValueIs(JwtIDKey, s))
}

// WithAudience specifies that expected audience value.
// `Validate()` will return true if one of the values in the `aud` element
// matches this value. If not specified, the value of `aud` is not
// verified at all.
func WithAudience(s string) ValidateOption {
	return WithValidator(audienceClaimContainsString(s))
}

// WithClaimValue specifies the expected value for a given claim.
// The stored and expected values are compared with reflect.DeepEqual,
// so slice-, map-, and struct-valued claims are supported in addition
// to scalars. See [ClaimValueIs] for edge-case semantics.
func WithClaimValue(name string, v any) ValidateOption {
	return WithValidator(ClaimValueIs(name, v))
}

// WithTypedClaim allows a private claim to be parsed into the object type of
// your choice. It works much like the RegisterCustomField, but the effect
// is only applicable to the jwt.Parse function call which receives this option.
//
// While this can be extremely useful, this option should be used with caution:
// There are many caveats that your entire team/user-base needs to be aware of,
// and therefore in general its use is discouraged. Only use it when you know
// what you are doing, and you document its use clearly for others.
//
// First and foremost, this is a "per-object" option. Meaning that given the same
// serialized format, it is possible to generate two objects whose internal
// representations may differ. That is, if you parse one _WITH_ the option,
// and the other _WITHOUT_, their internal representation may completely differ.
// This could potentially lead to problems.
//
// Second, specifying this option will slightly slow down the decoding process
// as it needs to consult multiple definitions sources (global and local), so
// be careful if you are decoding a large number of tokens, as the effects will stack up.
//
// Finally, this option will also NOT work unless the tokens themselves support such
// parsing mechanism. For example, while tokens obtained from `jwt.New()` and
// `openid.New()` will respect this option, if you provide your own custom
// token type, it will need to implement the TokenWithDecodeCtx interface.
func WithTypedClaim(name string, object any) ParseOption {
	return &parseOption{option.New(identTypedClaim{}, claimPair{Name: name, Value: object})}
}

// WithRequiredClaim specifies that the claim identified the given name
// must exist in the token. Only the existence of the claim is checked:
// the actual value associated with that field is not checked.
func WithRequiredClaim(name string) ValidateOption {
	return WithValidator(IsRequired(name))
}

// WithMaxDelta specifies that given two claims `c1` and `c2` that represent time, the difference in
// time.Duration must be less than equal to the value specified by `d`. If `c1` or `c2` is the
// empty string, the current time (as computed by `time.Now` or the object passed via
// `WithClock()`) is used for the comparison.
//
// `c1` and `c2` are also assumed to be required, therefore not providing either claim in the
// token will result in an error.
//
// Because there is no way of reliably knowing how to parse private claims, we currently only
// support `iat`, `exp`, and `nbf` claims.
//
// If the empty string is passed to c1 or c2, then the current time (as calculated by time.Now() or
// the clock object provided via WithClock()) is used.
//
// For example, in order to specify that `exp` - `iat` should be less than 10*time.Second, you would write
//
//	jwt.Validate(token, jwt.WithMaxDelta(10*time.Second, jwt.ExpirationKey, jwt.IssuedAtKey))
//
// If AcceptableSkew of 2 second is specified, the above will return valid for any value of
// `exp` - `iat`  between 8 (10-2) and 12 (10+2).
func WithMaxDelta(dur time.Duration, c1, c2 string) ValidateOption {
	return WithValidator(MaxDeltaIs(c1, c2, dur))
}

// WithMinDelta is almost exactly the same as WithMaxDelta, but force validation to fail if
// the difference between time claims are less than dur.
//
// For example, in order to specify that `exp` - `iat` should be greater than 10*time.Second, you would write
//
//	jwt.Validate(token, jwt.WithMinDelta(10*time.Second, jwt.ExpirationKey, jwt.IssuedAtKey))
//
// The validation would fail if the difference is less than 10 seconds.
func WithMinDelta(dur time.Duration, c1, c2 string) ValidateOption {
	return WithValidator(MinDeltaIs(c1, c2, dur))
}

// WithVerifyAuto specifies that the JWS verification should be attempted
// by using the data available in the JWS message. Currently only verification
// method available is to use the keys available in the JWKS URL pointed
// in the `jku` field.
//
// Please read the documentation for `jws.VerifyAuto` for more details.
func WithVerifyAuto(f jwk.Fetcher, options ...jwk.FetchOption) ParseOption {
	return &parseOption{option.New(identVerifyAuto{}, jws.WithVerifyAuto(f, options...))}
}

func WithInsecureNoSignature() SignOption {
	return &signEncryptParseOption{option.New(identInsecureNoSignature{}, (any)(nil))}
}
