package jwtvalidation

import (
	"errors"
	"fmt"
	"net/http"
	"time"

	"code.justin.tv/sse/malachai/pkg/internal/apierror"

	"github.com/SermoDigital/jose/crypto"
	"github.com/SermoDigital/jose/jws"
	"github.com/SermoDigital/jose/jwt"
)

// jwt claim validation errors
var (
	ErrExpInPast   = errors.New("exp is passed")
	ErrNbfInFuture = errors.New("nbf is in the future")
)

// jwt field names
const (
	CapabilityClaimName = "cap"
	KeyIDHeaderName     = "kid"
	AlgHeaderName       = "alg"
)

// ClaimWrongType is returned when a claim is the wrong type
type ClaimWrongType struct {
	FieldName string
	TypeName  string
}

func (c ClaimWrongType) Error() string {
	return fmt.Sprintf("%s claim is the wrong type, wanted %s", c.FieldName, c.TypeName)
}

// ClaimMissingError is returned when a claim is missing
type ClaimMissingError struct {
	FieldName string
}

func (c ClaimMissingError) Error() string {
	return fmt.Sprintf("%s claim missing", c.FieldName)
}

// ValidationErrorList represents a list of errors
type ValidationErrorList []error

func (err ValidationErrorList) Error() string {
	return "invalid jwt claims found"
}

// ValidateRequiredClaims ensures that the required claims are present
func (bv *BaseValidator) ValidateRequiredClaims(c jws.Claims) (err error) {
	multiErr := ValidationErrorList{}

	for _, key := range bv.RequiredClaims {
		_, ok := c[key]
		if !ok {
			multiErr = append(multiErr, ClaimMissingError{key})
		}
	}
	if len(multiErr) != 0 {
		err = multiErr
	}
	return
}

// BaseValidator implements
type BaseValidator struct {
	RequiredClaims []string
	Now            func() time.Time
}

var (
	// ErrMissingKID is returned when a jwt does not contain a kid
	ErrMissingKID = apierror.NewError(http.StatusBadRequest, "kid missing from jwt header")
)

// GetKID returns the kid
func GetKID(t jwt.JWT) (kid string, err error) {
	kid, err = GetHeaderString(t, KeyIDHeaderName)
	return
}

// ParseSigningMethod returns the signing method, checking against a whitelist
// of signing methods and returning an error if not whitelisted.
func ParseSigningMethod(t jwt.JWT) (method crypto.SigningMethod, err error) {
	alg, err := GetHeaderString(t, AlgHeaderName)
	if err != nil {
		return
	}

	method = jws.GetSigningMethod(alg)
	switch method {
	case crypto.SigningMethodRS256:
		return
	default:
		// not whitelisted, return an error
		method = nil
		err = errors.New("invalid signing method")
		return
	}
}

// GetHeaderString retrieves a header value from the jwt
func GetHeaderString(t jwt.JWT, header string) (value string, err error) {
	v := t.(jws.JWS).Protected().Get(header)

	var ok bool
	if value, ok = v.(string); !ok {
		err = errors.New("header value is not a string")
	}
	return
}
