package jwt

import (
	"crypto"
	"encoding/json"
	"errors"
	"os"
	"strings"
	"time"

	"github.com/kataras/iris/v12/context"

	"github.com/square/go-jose/v3"
	"github.com/square/go-jose/v3/jwt"
)

func init() {
	context.SetHandlerName("iris/middleware/jwt.*", "iris.jwt")
}

// TokenExtractor is a function that takes a context as input and returns
// a token. An empty string should be returned if no token found
// without additional information.
type TokenExtractor func(*context.Context) string

// FromHeader is a token extractor.
// It reads the token from the Authorization request header of form:
// Authorization: "Bearer {token}".
func FromHeader(ctx *context.Context) string {
	authHeader := ctx.GetHeader("Authorization")
	if authHeader == "" {
		return ""
	}

	// pure check: authorization header format must be Bearer {token}
	authHeaderParts := strings.Split(authHeader, " ")
	if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
		return ""
	}

	return authHeaderParts[1]
}

// FromQuery is a token extractor.
// It reads the token from the "token" url query parameter.
func FromQuery(ctx *context.Context) string {
	return ctx.URLParam("token")
}

// FromJSON is a token extractor.
// Reads a json request body and extracts the json based on the given field.
// The request content-type should contain the: application/json header value, otherwise
// this method will not try to read and consume the body.
func FromJSON(jsonKey string) TokenExtractor {
	return func(ctx *context.Context) string {
		if ctx.GetContentTypeRequested() != context.ContentJSONHeaderValue {
			return ""
		}

		var m context.Map
		if err := ctx.ReadJSON(&m); err != nil {
			return ""
		}

		if m == nil {
			return ""
		}

		v, ok := m[jsonKey]
		if !ok {
			return ""
		}

		tok, ok := v.(string)
		if !ok {
			return ""
		}

		return tok
	}
}

// JWT holds the necessary information the middleware need
// to sign and verify tokens.
//
// The `RSA(privateFile, publicFile, password)` package-level helper function
// can be used to decode the SignKey and VerifyKey.
type JWT struct {
	// MaxAge is the expiration duration of the generated tokens.
	MaxAge time.Duration

	// Extractors are used to extract a raw token string value
	// from the request.
	// Builtin extractors:
	// * FromHeader
	// * FromQuery
	// * FromJSON
	// Defaults to a slice of `FromHeader` and `FromQuery`.
	Extractors []TokenExtractor

	// Signer is used to sign the token.
	// It is set on `New` and `Default` package-level functions.
	Signer jose.Signer
	// VerificationKey is used to verify the token (public key).
	VerificationKey interface{}

	// Encrypter is used to, optionally, encrypt the token.
	// It is set on `WithEncryption` method.
	Encrypter jose.Encrypter
	// DecriptionKey is used to decrypt the token (private key)
	DecriptionKey interface{}
}

type privateKey interface{ Public() crypto.PublicKey }

// New returns a new JWT instance.
// It accepts a maximum time duration for token expiration
// and the algorithm among with its key for signing and verification.
//
// See `WithEncryption` method to add token encryption too.
// Use `Token` method to generate a new token string
// and `VerifyToken` method to decrypt, verify and bind claims of an incoming request token.
// Token, by default, is extracted by "Authorization: Bearer {token}" request header and
// url query parameter of "token". Token extractors can be modified through the `Extractors` field.
//
// For example, if you want to sign and verify using RSA-256 key:
// 1. Generate key file, e.g:
// 		$ openssl genrsa -des3 -out private.pem 2048
// 2. Read file contents with io.ReadFile("./private.pem")
// 3. Pass the []byte result to the `ParseRSAPrivateKey(contents, password)` package-level helper
// 4. Use the result *rsa.PrivateKey as "key" input parameter of this `New` function.
//
// See aliases.go file for available algorithms.
func New(maxAge time.Duration, alg SignatureAlgorithm, key interface{}) (*JWT, error) {
	sig, err := jose.NewSigner(jose.SigningKey{
		Algorithm: alg,
		Key:       key,
	}, (&jose.SignerOptions{}).WithType("JWT"))

	if err != nil {
		return nil, err
	}

	j := &JWT{
		Signer:          sig,
		VerificationKey: key,
		MaxAge:          maxAge,
		Extractors:      []TokenExtractor{FromHeader, FromQuery},
	}

	if s, ok := key.(privateKey); ok {
		j.VerificationKey = s.Public()
	}

	return j, nil
}

// Default key filenames for `RSA`.
const (
	DefaultSignFilename = "jwt_sign.key"
	DefaultEncFilename  = "jwt_enc.key"
)

// RSA returns a new `JWT` instance.
// It tries to parse RSA256 keys from "filenames[0]" (defaults to  "jwt_sign.key") and
// "filenames[1]" (defaults to "jwt_enc.key") files or generates and exports new random keys.
//
// It panics on errors.
// Use the `New` package-level function instead for more options.
func RSA(maxAge time.Duration, filenames ...string) *JWT {
	var (
		signFilename = DefaultSignFilename
		encFilename  = DefaultEncFilename
	)

	switch len(filenames) {
	case 1:
		signFilename = filenames[0]
	case 2:
		encFilename = filenames[1]
	}

	// Do not try to create or load enc key if only sign key already exists.
	withEncryption := true
	if fileExists(signFilename) {
		withEncryption = fileExists(encFilename)
	}

	sigKey, err := LoadRSA(signFilename, 2048)
	if err != nil {
		panic(err)
	}

	j, err := New(maxAge, RS256, sigKey)
	if err != nil {
		panic(err)
	}

	if withEncryption {
		encKey, err := LoadRSA(encFilename, 2048)
		if err != nil {
			panic(err)
		}
		err = j.WithEncryption(A128CBCHS256, RSA15, encKey)
		if err != nil {
			panic(err)
		}
	}

	return j
}

const (
	signEnv = "JWT_SECRET"
	encEnv  = "JWT_SECRET_ENC"
)

func getenv(key string, def string) string {
	v := os.Getenv(key)
	if v == "" {
		return def
	}

	return v
}

// HMAC returns a new `JWT` instance.
// It tries to read hmac256 secret keys from system environment variables:
// * JWT_SECRET for signing and verification key and
// * JWT_SECRET_ENC for encryption and decryption key
// and defaults them to the given "keys" respectfully.
//
// It panics on errors.
// Use the `New` package-level function instead for more options.
func HMAC(maxAge time.Duration, keys ...string) *JWT {
	var defaultSignSecret, defaultEncSecret string

	switch len(keys) {
	case 1:
		defaultSignSecret = keys[0]
	case 2:
		defaultEncSecret = keys[1]
	}

	signSecret := getenv(signEnv, defaultSignSecret)
	encSecret := getenv(encEnv, defaultEncSecret)

	j, err := New(maxAge, HS256, []byte(signSecret))
	if err != nil {
		panic(err)
	}

	if encSecret != "" {
		err = j.WithEncryption(A128GCM, DIRECT, []byte(encSecret))
		if err != nil {
			panic(err)
		}
	}

	return j
}

// WithEncryption method enables encryption and decryption of the token.
// It sets an appropriate encrypter(`Encrypter` and the `DecriptionKey` fields) based on the key type.
func (j *JWT) WithEncryption(contentEncryption ContentEncryption, alg KeyAlgorithm, key interface{}) error {
	var publicKey interface{} = key
	if s, ok := key.(privateKey); ok {
		publicKey = s.Public()
	}

	enc, err := jose.NewEncrypter(contentEncryption, jose.Recipient{
		Algorithm: alg,
		Key:       publicKey,
	},
		(&jose.EncrypterOptions{}).WithType("JWT").WithContentType("JWT"),
	)

	if err != nil {
		return err
	}

	j.Encrypter = enc
	j.DecriptionKey = key
	return nil
}

// Expiry returns a new standard Claims with
// the `Expiry` and `IssuedAt` fields of the "claims" filled
// based on the given "maxAge" duration.
//
// See the `JWT.Expiry` method too.
func Expiry(maxAge time.Duration, claims Claims) Claims {
	now := time.Now()
	claims.Expiry = NewNumericDate(now.Add(maxAge))
	claims.IssuedAt = NewNumericDate(now)
	return claims
}

// Expiry method same as `Expiry` package-level function,
// it returns a Claims with the expiration fields of the "claims"
// filled based on the JWT's `MaxAge` field.
// Only use it when this standard "claims"
// is embedded on a custom claims structure.
// Usage:
// type UserClaims struct {
// 	jwt.Claims
// 	Username string
// }
// [...]
// standardClaims := j.Expiry(jwt.Claims{...})
// customClaims := UserClaims{
// 	Claims:   standardClaims,
// 	Username: "kataras",
// }
// j.WriteToken(ctx, customClaims)
func (j *JWT) Expiry(claims Claims) Claims {
	return Expiry(j.MaxAge, claims)
}

// Token generates and returns a new token string.
// See `VerifyToken` too.
func (j *JWT) Token(claims interface{}) (string, error) {
	// switch c := claims.(type) {
	// case Claims:
	// 	claims = Expiry(j.MaxAge, c)
	// case map[string]interface{}: let's not support map.
	// 	now := time.Now()
	// 	c["iat"] = now.Unix()
	// 	c["exp"] = now.Add(j.MaxAge).Unix()
	// }
	if c, ok := claims.(Claims); ok {
		claims = Expiry(j.MaxAge, c)
	}

	var (
		token string
		err   error
	)

	// jwt.Builder and jwt.NestedBuilder contain same methods but they are not the same.
	if j.DecriptionKey != nil {
		token, err = jwt.SignedAndEncrypted(j.Signer, j.Encrypter).Claims(claims).CompactSerialize()
	} else {
		token, err = jwt.Signed(j.Signer).Claims(claims).CompactSerialize()
	}

	if err != nil {
		return "", err
	}

	return token, nil
}

/* Let's no support maps, typed claim is the way to go.
// validateMapClaims validates claims of map type.
func validateMapClaims(m map[string]interface{}, e jwt.Expected, leeway time.Duration) error {
	if !e.Time.IsZero() {
		if v, ok := m["nbf"]; ok {
			if notBefore, ok := v.(NumericDate); ok {
				if e.Time.Add(leeway).Before(notBefore.Time()) {
					return ErrNotValidYet
				}
			}
		}

		if v, ok := m["exp"]; ok {
			if exp, ok := v.(int64); ok {
				if e.Time.Add(-leeway).Before(time.Unix(exp, 0)) {
					return ErrExpired
				}
			}
		}

		if v, ok := m["iat"]; ok {
			if issuedAt, ok := v.(int64); ok {
				if e.Time.Add(leeway).Before(time.Unix(issuedAt, 0)) {
					return ErrIssuedInTheFuture
				}
			}
		}
	}

	return nil
}
*/

// WriteToken is a helper which just generates(calls the `Token` method) and writes
// a new token to the client in plain text format.
//
// Use the `Token` method to get a new generated token raw string value.
func (j *JWT) WriteToken(ctx *context.Context, claims interface{}) error {
	token, err := j.Token(claims)
	if err != nil {
		ctx.StatusCode(500)
		return err
	}

	_, err = ctx.WriteString(token)
	return err
}

var (
	// ErrMissing when token cannot be extracted from the request.
	ErrMissing = errors.New("token is missing")
	// ErrExpired indicates that token is used after expiry time indicated in exp claim.
	ErrExpired = errors.New("token is expired (exp)")
	// ErrNotValidYet indicates that token is used before time indicated in nbf claim.
	ErrNotValidYet = errors.New("token not valid yet (nbf)")
	// ErrIssuedInTheFuture indicates that the iat field is in the future.
	ErrIssuedInTheFuture = errors.New("token issued in the future (iat)")
)

type (
	claimsValidator interface {
		ValidateWithLeeway(e jwt.Expected, leeway time.Duration) error
	}
	claimsAlternativeValidator interface { // to keep iris-contrib/jwt MapClaims compatible.
		Validate() error
	}
	claimsContextValidator interface {
		Validate(ctx *context.Context) error
	}
)

// IsValidated reports whether a token is already validated through
// `VerifyToken`. It returns true when the claims are compatible
// validators: a `Claims` value or a value that implements the `Validate() error` method.
func IsValidated(ctx *context.Context) bool { // see the `ReadClaims`.
	return ctx.Values().Get(needsValidationContextKey) == nil
}

func validateClaims(ctx *context.Context, claims interface{}) (err error) {
	switch c := claims.(type) {
	case claimsValidator:
		err = c.ValidateWithLeeway(jwt.Expected{Time: time.Now()}, 0)
	case claimsAlternativeValidator:
		err = c.Validate()
	case claimsContextValidator:
		err = c.Validate(ctx)
	case *json.RawMessage:
		// if the data type is raw message (json []byte)
		// then it should contain exp (and iat and nbf) keys.
		// Unmarshal raw message to validate against.
		v := new(Claims)
		err = json.Unmarshal(*c, v)
		if err == nil {
			return validateClaims(ctx, v)
		}
	default:
		ctx.Values().Set(needsValidationContextKey, struct{}{})
	}

	if err != nil {
		switch err {
		case jwt.ErrExpired:
			return ErrExpired
		case jwt.ErrNotValidYet:
			return ErrNotValidYet
		case jwt.ErrIssuedInTheFuture:
			return ErrIssuedInTheFuture
		}
	}

	return err
}

// VerifyToken verifies (and decrypts) the request token,
// it also validates and binds the parsed token's claims to the "claimsPtr" (destination).
// It does return a nil error on success.
func (j *JWT) VerifyToken(ctx *context.Context, claimsPtr interface{}) error {
	var token string

	for _, extract := range j.Extractors {
		if token = extract(ctx); token != "" {
			break // ok we found it.
		}
	}

	if token == "" {
		return ErrMissing
	}

	var (
		parsedToken *jwt.JSONWebToken
		err         error
	)

	if j.DecriptionKey != nil {
		t, cerr := jwt.ParseSignedAndEncrypted(token)
		if cerr != nil {
			return cerr
		}

		parsedToken, err = t.Decrypt(j.DecriptionKey)
	} else {
		parsedToken, err = jwt.ParseSigned(token)
	}
	if err != nil {
		return err
	}

	if err = parsedToken.Claims(j.VerificationKey, claimsPtr); err != nil {
		return err
	}

	return validateClaims(ctx, claimsPtr)
}

const (
	// ClaimsContextKey is the context key which the jwt claims are stored from the `Verify` method.
	ClaimsContextKey          = "iris.jwt.claims"
	needsValidationContextKey = "iris.jwt.claims.unvalidated"
)

// Verify is a middleware. It verifies and optionally decrypts an incoming request token.
// It does write a 401 unauthorized status code if verification or decryption failed.
// It calls the `ctx.Next` on verified requests.
//
// See `VerifyToken` instead to verify, decrypt, validate and acquire the claims at once.
//
// A call of `ReadClaims` is required to validate and acquire the jwt claims
// on the next request.
func (j *JWT) Verify(ctx *context.Context) {
	var raw json.RawMessage
	if err := j.VerifyToken(ctx, &raw); err != nil {
		ctx.StopWithStatus(401)
		return
	}

	ctx.Values().Set(ClaimsContextKey, raw)
	ctx.Next()
}

// ReadClaims binds the "claimsPtr" (destination)
// to the verified (and decrypted) claims.
// The `Verify` method should be called  first (registered as middleware).
func ReadClaims(ctx *context.Context, claimsPtr interface{}) error {
	v := ctx.Values().Get(ClaimsContextKey)
	if v == nil {
		return ErrMissing
	}

	raw, ok := v.(json.RawMessage)
	if !ok {
		return ErrMissing
	}

	err := json.Unmarshal(raw, claimsPtr)
	if err != nil {
		return err
	}

	if !IsValidated(ctx) {
		// If already validated on `Verify/VerifyToken`
		// then no need to perform the check again.
		ctx.Values().Remove(needsValidationContextKey)
		return validateClaims(ctx, claimsPtr)
	}

	return nil
}

// Get returns and validates (if not already) the claims
// stored on request context's values storage.
//
// Should be used instead of the `ReadClaims` method when
// a custom verification middleware was registered (see the `Verify` method for an example).
//
// Usage:
// j := jwt.New(...)
// [...]
// app.Use(func(ctx iris.Context) {
//	var claims CustomClaims_or_jwt.Claims
// 	if err := j.VerifyToken(ctx, &claims); err != nil {
// 		ctx.StopWithStatus(iris.StatusUnauthorized)
// 		return
// 	}
//
// 	ctx.Values().Set(jwt.ClaimsContextKey, claims)
// 	ctx.Next()
// })
// [...]
// app.Post("/restricted", func(ctx iris.Context){
//	v, err := jwt.Get(ctx)
//  [handle error...]
//  claims,ok := v.(CustomClaims_or_jwt.Claims)
//  if !ok {
// 	  [do you support more than one type of claims? Handle here]
// 	}
//  [use claims...]
// })
func Get(ctx *context.Context) (interface{}, error) {
	claims := ctx.Values().Get(ClaimsContextKey)
	if claims == nil {
		return nil, ErrMissing
	}

	if !IsValidated(ctx) {
		ctx.Values().Remove(needsValidationContextKey)
		err := validateClaims(ctx, claims)
		if err != nil {
			return nil, err
		}
	}

	return claims, nil
}
