package auth

import (
	"errors"
	"net/http"

	"code.justin.tv/common/golibs/statsd"
	"code.justin.tv/tshadwell/jwt"
	"code.justin.tv/tshadwell/jwt/claim"
)

type claimsData struct {
	RequesterIP    string   `json:"requester_ip"`
	RequesterLogin string   `json:"requester_login"`
	Objects        []int    `json:"objects"`
	Actions        []string `json:"actions"`
	Version        string   `json:"version"`
}

type Claims struct {
	Expires   claim.Exp  `json:"exp"`
	Issuer    claim.Iss  `json:"iss" issuer:"code.justin.tv/web/cartman"`
	NotBefore claim.Nbf  `json:"nbf"`
	Audience  []string   `json:"aud"`
	Subject   int        `json:"sub"`
	Data      claimsData `json:"data"`
}

type Handler interface {
	AuthorizeRequest(token string, currentChannel int, r *http.Request, s statsd.Stats) error
}

type JwtHandler struct {
	Header    jwt.Header
	Algorithm jwt.Algorithm
}

var (
	ErrAuthorizationTokenRequired = errors.New("This request requires an Authorization token header")
	// ErrBadToken indicates that either the header has a non matching algorithm,
	// the signature is bad, or the encoding is bad.
	ErrBadToken = errors.New("Invalid token")
	// ErrBadClaim indicates that the claims made in the token body are invalid.
	ErrBadClaim                   = errors.New("Invalid claim")
	ErrCurrentChannelNotInObjects = errors.New("The current channel is not in the list of allowed objects")
)

func NewHandler(keyPath string) JwtHandler {
	publicKey, err := jwt.ReadRSAPublicKey(keyPath)
	if err != nil {
		panic(err)
	}
	return JwtHandler{Algorithm: jwt.RSAValidateOnly(jwt.RS256, publicKey)}
}

func (a JwtHandler) AuthorizeRequest(token string, currentChannel int, r *http.Request, s statsd.Stats) error {
	s.IncrBy("authorization.total.attempts", 1)

	err := AuthorizeCartmanToken(r, currentChannel)
	if err == nil {
		s.IncrBy("authorization.cartman.success", 1)
		return nil
	} else {
		// Do nothing, fall back to legacy JWT handler for now
		// TODO - Use this as the primary authorization check
		s.IncrBy("authorization.cartman.failure", 1)
		// return err
	}

	if token == "" {
		s.IncrBy("authorization.total.failure", 1)
		return ErrAuthorizationTokenRequired
	}

	var h jwt.Header
	var claims Claims

	if err := jwt.DecodeAndValidate(&h, &claims, a.Algorithm, []byte(token)); err != nil {
		s.IncrBy("authorization.total.failure", 1)
		return err
	}

	if err := a.Header.ValidateEqual(a.Header); err != nil {
		s.IncrBy("authorization.total.failure", 1)
		return err
	}

	if !a.audienceIsValid(claims.Audience) {
		s.IncrBy("authorization.total.failure", 1)
		return ErrBadClaim
	}

	if !a.objectsContainsChannel(claims.Data.Objects, currentChannel) {
		s.IncrBy("authorization.total.failure", 1)
		return ErrCurrentChannelNotInObjects
	}

	if !claim.Validate(claims) {
		s.IncrBy("authorization.total.failure", 1)
		return ErrBadClaim
	}

	s.IncrBy("authorization.total.success", 1)
	return nil
}

func (a JwtHandler) audienceIsValid(audience []string) bool {
	for _, v := range audience {
		if v == "code.justin.tv/video/spectre" {
			return true
		}
	}
	return false
}

func (a JwtHandler) objectsContainsChannel(objects []int, currentChannel int) bool {
	for _, v := range objects {
		if v == currentChannel {
			return true
		}
	}
	return false
}
