package jwt

import (
	"fmt"
	"time"

	"github.com/kataras/jwt"
)

// Signer holds common options to sign and generate a token.
// Its Sign method can be used to generate a token which can be sent to the client.
// Its NewTokenPair can be used to construct a token pair (access_token, refresh_token).
//
// It does not support JWE, JWK.
type Signer struct {
	Alg Alg
	Key interface{}

	// MaxAge to set "exp" and "iat".
	// Recommended value for access tokens: 15 minutes.
	// Defaults to 0, no limit.
	MaxAge  time.Duration
	Options []SignOption

	Encrypt func([]byte) ([]byte, error)
}

// NewSigner accepts the signature algorithm among with its (private or shared) key
// and the max life time duration of generated tokens and returns a JWT signer.
// See its Sign method.
//
// Usage:
//
//	signer := NewSigner(HS256, secret, 15*time.Minute)
//	token, err := signer.Sign(userClaims{Username: "kataras"})
func NewSigner(signatureAlg Alg, signatureKey interface{}, maxAge time.Duration) *Signer {
	if signatureAlg == HS256 {
		// A tiny helper if the end-developer uses string instead of []byte for hmac keys.
		if k, ok := signatureKey.(string); ok {
			signatureKey = []byte(k)
		}
	}

	s := &Signer{
		Alg:    signatureAlg,
		Key:    signatureKey,
		MaxAge: maxAge,
	}

	if maxAge > 0 {
		s.Options = []SignOption{MaxAge(maxAge)}
	}

	return s
}

// WithEncryption enables AES-GCM payload-only decryption.
func (s *Signer) WithEncryption(key, additionalData []byte) *Signer {
	encrypt, _, err := jwt.GCM(key, additionalData)
	if err != nil {
		panic(err) // important error before serve, stop everything.
	}

	s.Encrypt = encrypt
	return s
}

// Sign generates a new token based on the given "claims" which is valid up to "s.MaxAge".
func (s *Signer) Sign(claims interface{}, opts ...SignOption) ([]byte, error) {
	if len(opts) > 0 {
		opts = append(opts, s.Options...)
	} else {
		opts = s.Options
	}

	return SignEncrypted(s.Alg, s.Key, s.Encrypt, claims, opts...)
}

// NewTokenPair accepts the access and refresh claims plus the life time duration for the refresh token
// and generates a new token pair which can be sent to the client.
// The same token pair can be json-decoded.
func (s *Signer) NewTokenPair(accessClaims interface{}, refreshClaims interface{}, refreshMaxAge time.Duration, accessOpts ...SignOption) (TokenPair, error) {
	if refreshMaxAge <= s.MaxAge {
		return TokenPair{}, fmt.Errorf("refresh max age should be bigger than access token's one[%d - %d]", refreshMaxAge, s.MaxAge)
	}

	accessToken, err := s.Sign(accessClaims, accessOpts...)
	if err != nil {
		return TokenPair{}, err
	}

	refreshToken, err := Sign(s.Alg, s.Key, refreshClaims, MaxAge(refreshMaxAge))
	if err != nil {
		return TokenPair{}, err
	}

	tokenPair := jwt.NewTokenPair(accessToken, refreshToken)
	return tokenPair, nil
}