package jsonweb

import (
	"errors"

	"github.com/golang-jwt/jwt"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/kit/platform"
)

const kind = "jwt"

var (
	// ErrKeyNotFound should be returned by a KeyStore when
	// a key cannot be located for the provided key ID
	ErrKeyNotFound = errors.New("key not found")

	// EmptyKeyStore is a KeyStore implementation which contains no keys
	EmptyKeyStore = KeyStoreFunc(func(string) ([]byte, error) {
		return nil, ErrKeyNotFound
	})
)

// KeyStore is a type which holds a set of keys accessed
// via an id
type KeyStore interface {
	Key(string) ([]byte, error)
}

// KeyStoreFunc is a function which can be used as a KeyStore
type KeyStoreFunc func(string) ([]byte, error)

// Key delegates to the receiver KeyStoreFunc
func (k KeyStoreFunc) Key(v string) ([]byte, error) { return k(v) }

// TokenParser is a type which can parse and validate tokens
type TokenParser struct {
	keyStore KeyStore
	parser   *jwt.Parser
}

// NewTokenParser returns a configured token parser used to
// parse Token types from strings
func NewTokenParser(keyStore KeyStore) *TokenParser {
	return &TokenParser{
		keyStore: keyStore,
		parser: &jwt.Parser{
			ValidMethods: []string{jwt.SigningMethodHS256.Alg()},
		},
	}
}

// Parse takes a string then parses and validates it as a jwt based on
// the key described within the token
func (t *TokenParser) Parse(v string) (*Token, error) {
	jwt, err := t.parser.ParseWithClaims(v, &Token{}, func(jwt *jwt.Token) (interface{}, error) {
		token, ok := jwt.Claims.(*Token)
		if !ok {
			return nil, errors.New("missing kid in token claims")
		}

		// fetch key for "kid" from key store
		return t.keyStore.Key(token.KeyID)
	})

	if err != nil {
		return nil, err
	}

	token, ok := jwt.Claims.(*Token)
	if !ok {
		return nil, errors.New("token is unexpected type")
	}

	return token, nil
}

// IsMalformedError returns true if the error returned represents
// a jwt malformed token error
func IsMalformedError(err error) bool {
	verr, ok := err.(*jwt.ValidationError)
	return ok && verr.Errors&jwt.ValidationErrorMalformed > 0
}

// Token is a structure which is serialized as a json web token
// It contains the necessary claims required to authorize
type Token struct {
	jwt.StandardClaims
	// KeyID is the identifier of the key used to sign the token
	KeyID string `json:"kid"`
	// Permissions is the set of authorized permissions for the token
	Permissions []influxdb.Permission `json:"permissions"`
	// UserID for the token
	UserID string `json:"uid,omitempty"`
}

// PermissionSet returns the set of permissions associated with the token.
func (t *Token) PermissionSet() (influxdb.PermissionSet, error) {
	return t.Permissions, nil
}

// Identifier returns the identifier for this Token
// as found in the standard claims
func (t *Token) Identifier() platform.ID {
	id, err := platform.IDFromString(t.Id)
	if err != nil || id == nil {
		return platform.ID(1)
	}

	return *id
}

// GetUserID returns an invalid id as tokens are generated
// with permissions rather than for or by a particular user
func (t *Token) GetUserID() platform.ID {
	id, err := platform.IDFromString(t.UserID)
	if err != nil {
		return platform.InvalidID()
	}
	return *id
}

// Kind returns the string "jwt" which is used for auditing
func (t *Token) Kind() string {
	return kind
}

// EphemeralAuth creates a influxdb Auth form a jwt token
func (t *Token) EphemeralAuth(orgID platform.ID) *influxdb.Authorization {
	return &influxdb.Authorization{
		ID:          t.Identifier(),
		OrgID:       orgID,
		Status:      influxdb.Active,
		Permissions: t.Permissions,
	}
}
