//Package claim implements some standard JWT claims. An example is present in the parent `jwt` package.
package claim

import (
	"crypto/subtle"
	"encoding/json"
	"reflect"
	"time"
)

var (
	_ interface {
		json.Marshaler
		json.Unmarshaler
	} = &Exp{}
	_ interface {
		json.Marshaler
		json.Unmarshaler
	} = &Nbf{}
)

//Validates a Claims object. A struct field implementing Claim
//is validated by calling Validate()
func Validate(claims interface{}) (ok bool) {
	v := reflect.Indirect(reflect.ValueOf(claims))
	t := v.Type()

	for i, ed := 0, v.NumField(); i < ed; i++ {
		fv, ft := v.Field(i), t.Field(i)

		var c Claim
		c, ok = fv.Interface().(Claim)

		if !ok {
			continue
		}

		if !c.Valid(ft.Tag) {
			return false
		}

	}

	return true
}

type Claim interface {
	Valid(reflect.StructTag) bool
}

type Iat time.Time

// Marshal and Unmarshal conform to the correct JWT time specification
func (e Iat) MarshalJSON() ([]byte, error) { return json.Marshal((time.Time)(e).Unix()) }
func (e *Iat) UnmarshalJSON(b []byte) (err error) {
	var t int64
	err = json.Unmarshal(b, &t)

	*e = (Iat)(time.Unix(t, 0))
	return
}

/*
Expiration field for Claims struct composition.

	type Claims struct {
		Expires claim.Exp `json:"exp"`
	}

From spec:

	The "exp" (expiration time) claim identifies the expiration time on
	or after which the JWT MUST NOT be accepted for processing.  The
	processing of the "exp" claim requires that the current date/time
	MUST be before the expiration date/time listed in the "exp" claim.
	Implementers MAY provide for some small leeway, usually no more than
	a few minutes, to account for clock skew.  Its value MUST be a number
	containing a NumericDate value.  Use of this claim is OPTIONAL.
*/
type Exp time.Time

func (e Exp) Valid(reflect.StructTag) bool {
	return time.Time(e).After(time.Now())
}

// Marshal and Unmarshal conform to the correct JWT time specification
func (e Exp) MarshalJSON() ([]byte, error) { return json.Marshal((time.Time)(e).Unix()) }
func (e *Exp) UnmarshalJSON(b []byte) (err error) {
	var t int64
	err = json.Unmarshal(b, &t)

	*e = (Exp)(time.Unix(t, 0))
	return
}

/*
Issuer field for Claims struct composition.

	type Claims struct {
		Issuser claim.Iss `json:"iss" issuer:"http://twitch.tv/validate"`
	}

From spec:

	The "iss" (issuer) claim identifies the principal that issued the
	JWT.  The processing of this claim is generally application specific.
	The "iss" value is a case-sensitive string containing a StringOrURI
	value.  Use of this claim is OPTIONAL.

*/
type Iss string

func (i Iss) Valid(r reflect.StructTag) bool {
	return string(i) == r.Get("issuer")
}

/*
Not before field for Claims struct composition.

	type Claims struct {
		NotBefore Nbf `json:"nbf"`
	}
*/
type Nbf time.Time

// Marshal and Unmarshal conform to the correct JWT time specification
func (e Nbf) MarshalJSON() ([]byte, error) { return json.Marshal((time.Time)(e).Unix()) }
func (e *Nbf) UnmarshalJSON(b []byte) (err error) {
	var t int64
	err = json.Unmarshal(b, &t)

	*e = (Nbf)(time.Unix(t, 0))
	return
}

func (n Nbf) Valid(reflect.StructTag) bool {
	return time.Time(n).Before(time.Now())
}

/*
Subject field for Claims struct composition.

	type Claims struct {
		claim.Sub
	}

	var alg = claim.HS512([]byte("secret")

	[...]

	var cl Claims

	if err = jwt.DecodeAndValidate(&header, &cl, alg, b); err != nil {
		return
	}

	cl.Sub.ShouldEqual = "clientid"

	if ok, err = claim.Validate(cl); err != nil || !ok {
		return
	}

Spec:
	The "sub" (subject) claim identifies the principal that is the
	subject of the JWT.  The claims in a JWT are normally statements
	about the subject.  The subject value MUST either be scoped to be
	locally unique in the context of the issuer or be globally unique.
	The processing of this claim is generally application specific.  The
	"sub" value is a case-sensitive string containing a StringOrURI
	value.  Use of this claim is OPTIONAL.
*/
type Sub struct {
	ShouldEqual string `json:"-"`
	Sub         string `json:"sub"`
}

func (su *Sub) SubShouldEqual(s string) {
	su.ShouldEqual = s
}

func (s Sub) Valid(r reflect.StructTag) bool {
	return subtle.ConstantTimeCompare(
		[]byte(s.ShouldEqual),
		[]byte(s.Sub),
	) == 1
}
