package auth0

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/form3tech-oss/jwt-go"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/auth/util"
	"go.uber.org/multierr"
)

const (
	CustomEmailClaimKey = "https://tidbcloud.com/claims/email"
)

// Conf contains the Audience, the Secret and the domain
// The domain is the auth0 tenant domain
// The secret is used for HMAC validations
// A SPA must use RSA256
// For API access we may prefer HMAC
type Conf struct {
	Audience                 string
	Secret                   []byte
	Domain                   string
	ClientCredentialAudience string
}

// GetClaim is a function to Project an interface value out of JWT claims
type GetClaim func(context.Context, jwt.MapClaims) (interface{}, error)

// IntoGetClaim is needed because Go forces us to define newtypes
type IntoGetClaim interface {
	ToGetClaim() GetClaim
}

// GetEmailFunc is defined for EmailFunc to work with IntoGetClaim
type GetEmailFunc func(jwt.MapClaims) (string, error)

// ToGetClaim satisfies the IntoGetClaim interface
func (f GetClaim) ToGetClaim() GetClaim {
	return f
}

// ToGetClaim satisfies the IntoGetClaim interface
func (f GetEmailFunc) ToGetClaim() GetClaim {
	return func(ctx context.Context, claims jwt.MapClaims) (interface{}, error) {
		return f(claims)
	}
}

// GetEmailIntoClaim is an alias of GetEmailClaim that is newtyped for the IntoGetClaim interface.
var GetEmailIntoClaim GetEmailFunc = GetEmailClaim

// AuthTokenClaimKey is the key for storing the TokenClaim, which is usually a `*tenant.User`.
var AuthTokenClaimKey util.ContextKey = "AuthTokenClaimKey" //nolint:gosec

// ClaimsKey is the key for claims
var ClaimsKey util.ContextKey = "ClaimsKey"

// AuthTypeKey is the key for auth0 auth type, m2m or mail
var AuthTypeKey util.ContextKey = "AuthTypeKey"

// AuthUserIDKey is the key for auth0 auth user id
var AuthUserIDKey util.ContextKey = "AuthUserIDKey"

// AuthValidatorIDKey is the key for AuthValidatorID
var AuthValidatorIDKey util.ContextKey = "AuthValidatorID"

// AuthHttpMethod is the key for HTTP Method of proxy API
var AuthHttpMethod util.ContextKey = "AuthHttpMethod"

// AuthHttpURI is the key for HTTP URI of proxy API
var AuthHttpURI util.ContextKey = "AuthHttpURI"

const (
	ClientCredentialsAuthType = "client-credentials"
	GmailAuthType             = "gmail"
	AuthByID                  = "auth0"
)

func IsAuth0SessionAuth(ctx context.Context) bool {
	authBy, err := util.GetContextAuthBy(ctx)
	return err == nil && authBy == AuthByID
}

// GetAuthValidatorIDFromContext gets the AuthValidatorID from context if exists.
func GetAuthValidatorIDFromContext(ctx context.Context) (AuthValidatorID, error) {
	i := ctx.Value(AuthValidatorIDKey)
	if i == nil {
		return "", errors.New("auth validator id not in context")
	}
	id, ok := i.(AuthValidatorID)
	if !ok {
		return "", errors.New("expect type AuthValidatorID in context")
	}
	return id, nil
}

// GetUserIDFromContext gets the user ID from context if exists.
func GetUserIDFromContext(ctx context.Context) string {
	uid := ctx.Value(AuthUserIDKey)
	if uid == nil {
		return ""
	}
	s, ok := uid.(string)
	if !ok {
		return ""
	}
	return s
}

// TokenFromFirst gets the token from the first extractor that works.
func TokenFromFirst(extractors ...TokenExtractor) TokenExtractor {
	return func(r *http.Request) (string, error) {
		var err error
		for _, ex := range extractors {
			token, exErr := ex(r)
			if exErr != nil {
				err = multierr.Append(err, exErr)
			} else {
				if token != "" {
					return token, nil
				}
			}
		}
		return "", err
	}
}

// GetEmailClaim retrieves the "email" field from claims
func GetEmailClaim(claims jwt.MapClaims) (string, error) {
	if email, ok := claims["email"]; ok {
		return email.(string), nil
	}

	return "", errors.New("no email found")
}

// GetCustomEmailClaim retrieves our custom email field from claims
func GetCustomEmailClaim(claims jwt.MapClaims) (string, error) {
	if email, ok := claims[CustomEmailClaimKey]; ok {
		return email.(string), nil
	}

	return "", errors.New("no custom email found")
}

// GetSubjectClaim retrieves sub from Auth0 token JWT
func GetSubjectClaim(claims jwt.MapClaims) (string, error) {
	if sub, ok := claims["sub"]; ok {
		return sub.(string), nil
	}

	return "", errors.New("no sub found")
}

// TokenExtractor is adapted from (MIT) https://github.com/auth0/go-jwt-middleware/blob/master/jwtmiddleware.go
type TokenExtractor func(r *http.Request) (string, error)

// TokenRawFromHTTPHeader is a "TokenExtractor" that takes a give request and extracts
// the JWT token from the Authorization header.
// adapted from (MIT) https://github.com/auth0/go-jwt-middleware/blob/master/jwtmiddleware.go
func TokenRawFromHTTPHeader(r *http.Request) (string, error) {
	authHeader := r.Header.Get("Authorization")
	if authHeader == "" {
		return "", nil // No error, just no token
	}
	return util.ExtractBearerToken(authHeader)
}

// TokenFromCookie gets the JWT token from a cookies named "jwt"
func TokenFromCookie(r *http.Request) (string, error) {
	cookie, err := r.Cookie("jwt")
	if err != nil {
		return "", err
	}
	if cookie.Value == "" {
		return "", errors.New("empty cookie value")
	}
	return cookie.Value, nil
}

// TokenFromParameter returns a function that extracts the token from the specified
// query string parameter
// adapted from (MIT) https://github.com/auth0/go-jwt-middleware/blob/master/jwtmiddleware.go
func TokenFromParameter(param string) TokenExtractor {
	return func(r *http.Request) (string, error) {
		val := r.URL.Query().Get(param)
		if val == "" {
			return "", errors.New("No auth param found")
		}
		return val, nil
	}
}

// ParseToken is taken from the jwt example docs
func ParseToken(secret []byte, jwks Jwks, tokenString string) (jwt.MapClaims, error) {

	// Parse takes the token string and a function for looking up the key. The latter is especially
	// useful if you use multiple keys for your application.  The standard is to use 'kid' in the
	// head of the token to identify which key to use, but the parsed token (head and claims) is provided
	// to the callback, providing flexibility.
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
			return secret, nil
		}
		if _, ok := token.Method.(*jwt.SigningMethodRSA); ok {
			cert, err := MakePemCert(token, jwks)
			if err != nil {
				return nil, errors.Annotate(err, "MakePemCert")
			}
			pubKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
			if err != nil || pubKey == nil {
				return nil, errors.Annotate(err, "ParseRSAPublicKeyFromPEM")
			}
			return pubKey, nil
		}

		return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
	})

	if err != nil {
		return nil, errors.Annotate(err, "jwt.Parse")
	}

	if !token.Valid {
		return nil, fmt.Errorf("invalid token " + tokenString)
	}

	if claims, ok := token.Claims.(jwt.MapClaims); ok {
		return claims, nil
	}

	return nil, errcode.NewInternalErr(fmt.Errorf("internal error during claims processing"))
}

// GetJWKS retrieves the public key from Auth0
// You should call this function at startup
func GetJWKS(domain string) (Jwks, error) {
	var jwks = Jwks{}
	var err error
	resp, err := http.Get("https://" + domain + "/.well-known/jwks.json")
	if err != nil {
		return jwks, errors.Annotate(err, "Get jwks.json")
	}

	defer deferCloseErr(resp.Body, &err)

	err = json.NewDecoder(resp.Body).Decode(&jwks)
	return jwks, errors.Annotate(err, "json decode")
}

// deferCloseErr captures the error returned from closing (if an error occurs).
// This is designed to be used in a defer statement.
func deferCloseErr(c io.Closer, err *error) {
	if cerr := c.Close(); cerr != nil && *err == nil {
		*err = cerr
	}
}

// MakePemCert makes a cert
func MakePemCert(token *jwt.Token, jwks Jwks) (string, error) {
	cert := ""

	for k := range jwks.Keys {
		if token.Header["kid"] == jwks.Keys[k].Kid {
			cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
		}
	}

	if cert == "" {
		err := errors.New("unable to find appropriate key")
		return cert, err
	}

	return cert, nil
}

// Jwks is the return from Auth0 api call for keys
type Jwks struct {
	Keys []JSONWebKeys `json:"keys"`
}

// JSONWebKeys is the return from Auth0 api call for keys
type JSONWebKeys struct {
	Kty string   `json:"kty"`
	Kid string   `json:"kid"`
	Use string   `json:"use"`
	N   string   `json:"n"`
	E   string   `json:"e"`
	X5c []string `json:"x5c"`
}
