package auth

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/cortezaproject/corteza-server/pkg/payload"
	"github.com/go-oauth2/oauth2/v4"
	"github.com/go-oauth2/oauth2/v4/generates"
	"github.com/go-oauth2/oauth2/v4/models"
	"github.com/lestrrat-go/jwx/jwa"
	"github.com/lestrrat-go/jwx/jwk"
	"github.com/lestrrat-go/jwx/jwt"
	"github.com/spf13/cast"
)

type (
	tokenIssuer struct {
		defaultRequest *TokenRequest

		store tokenIssuerStore

		lookup tokenIssuerLookup

		generator tokenIssuerGenerator

		signer tokenIssuerSigner
	}

	TokenRequest struct {
		AccessToken  string
		RefreshToken string
		Expiration   time.Duration
		Audience     string
		Issuer       string
		IssuedAt     time.Time
		ClientID     uint64
		UserID       uint64
		Roles        []uint64
		Scope        []string
	}

	IssuerOptFn func(*tokenIssuer) error
	IssueOptFn  func(*TokenRequest) error

	tokenIssuerStore     func(context.Context, TokenRequest) error
	tokenIssuerLookup    func(context.Context, string) error
	tokenIssuerGenerator func(context.Context, TokenRequest) (string, string, error)
	tokenIssuerSigner    func(token jwt.Token) ([]byte, error)
)

var (
	TokenIssuer *tokenIssuer

	now = func() *time.Time {
		c := time.Now().Truncate(time.Second)
		return &c
	}
)

func NewTokenIssuer(opt ...IssuerOptFn) (tm *tokenIssuer, err error) {
	tm = &tokenIssuer{
		defaultRequest: &TokenRequest{Issuer: "cortezaproject.org"},

		store: func(ctx context.Context, request TokenRequest) error {
			return fmt.Errorf("token issuer store not configured")
		},

		lookup: func(context.Context, string) error {
			return fmt.Errorf("token issuer lookup not configured")
		},

		signer: func(token jwt.Token) ([]byte, error) {
			return nil, fmt.Errorf("token issuer signer not configured")
		},

		generator: DefaultAccessTokenGenerator,
	}

	for _, fn := range opt {
		if err = fn(tm); err != nil {
			return
		}
	}

	return
}

func (tm *tokenIssuer) Issue(ctx context.Context, opt ...IssueOptFn) (_ []byte, err error) {
	var req = tm.newTokenRequest()
	if err = req.apply(opt...); err != nil {
		return
	}

	if len(req.AccessToken+req.RefreshToken) > 0 {
		panic("can not issue new token with preset access and refresh tokens, " +
			"this is most likely an implementation mistake")
	}

	if req.AccessToken, req.RefreshToken, err = tm.generator(ctx, *req); err != nil {
		return
	}

	if err = tm.store(ctx, *req); err != nil {
		return
	}

	return tm.sign(req)
}

func (tm *tokenIssuer) Sign(opt ...IssueOptFn) (_ []byte, err error) {
	var req = tm.newTokenRequest()
	if err = req.apply(opt...); err != nil {
		return
	}

	return tm.sign(req)
}

func (tm *tokenIssuer) sign(req *TokenRequest) ([]byte, error) {
	if token, err := makeToken(req); err != nil {
		return nil, err
	} else {
		return tm.signer(token)
	}
}

func (tm *tokenIssuer) newTokenRequest() *TokenRequest {
	return &TokenRequest{
		Issuer:     tm.defaultRequest.Issuer,
		ClientID:   tm.defaultRequest.ClientID,
		Expiration: tm.defaultRequest.Expiration,
		IssuedAt:   *now(),
	}
}

func (tm *tokenIssuer) Validate(ctx context.Context, token jwt.Token) (err error) {
	if err = tm.lookup(ctx, token.JwtID()); err != nil {
		return errUnauthorized()
	}

	return nil
}

func makeToken(req *TokenRequest) (_ jwt.Token, err error) {
	var (
		roles = make([]string, len(req.Roles))
		token = jwt.New()

		toString = func(i uint64) string {
			return strconv.FormatUint(i, 10)
		}
	)

	if len(req.Scope) == 0 {

		req.Scope = []string{"profile", "api"}
	}

	if req.IssuedAt.IsZero() {
		req.IssuedAt = *now()
	}

	for i, r := range req.Roles {
		roles[i] = toString(r)
	}

	if err = token.Set(jwt.JwtIDKey, req.AccessToken); err != nil {
		return
	}

	if err = token.Set(jwt.SubjectKey, toString(req.UserID)); err != nil {
		return
	}

	if err = token.Set(jwt.ExpirationKey, now().Add(req.Expiration).Unix()); err != nil {
		return
	}

	if req.Audience != "" {
		if err = token.Set(jwt.AudienceKey, req.Audience); err != nil {
			return
		}
	}

	if err = token.Set(jwt.IssuerKey, req.Issuer); err != nil {
		return
	}

	if err = token.Set(jwt.IssuedAtKey, req.IssuedAt.Unix()); err != nil {
		return
	}

	if err = token.Set("clientID", toString(req.ClientID)); err != nil {
		return
	}

	if err = token.Set("scope", strings.Join(req.Scope, " ")); err != nil {
		return
	}

	if err = token.Set("roles", roles); err != nil {
		return
	}

	return token, nil
}

func IdentityFromToken(token jwt.Token) *identity {
	if token == nil {
		return Anonymous()
	}

	var (
		roles, _ = token.Get("roles")
	)

	return Authenticated(
		cast.ToUint64(token.Subject()),
		payload.ParseUint64s(cast.ToStringSlice(roles))...,
	)
}

func DefaultAccessTokenGenerator(ctx context.Context, req TokenRequest) (string, string, error) {
	return generates.NewAccessGenerate().Token(
		ctx,
		&oauth2.GenerateBasic{
			Client: &models.Client{
				ID: strconv.FormatUint(req.ClientID, 10),
			},
			UserID:    strconv.FormatUint(req.UserID, 10),
			CreateAt:  *now(),
			TokenInfo: nil,
			Request:   nil,
		},
		true,
	)
}

func WithSecretSigner(secret string) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		if len(secret) == 0 {
			return fmt.Errorf("JWK missing")
		}

		var key jwk.Key
		if key, err = jwk.New([]byte(secret)); err != nil {
			return fmt.Errorf("could not parse JWK: %w", err)
		}

		tm.signer = func(token jwt.Token) ([]byte, error) {
			return jwt.Sign(token, jwa.HS512, key)
		}

		return nil
	}
}

func WithDefaultClientID(ID uint64) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.defaultRequest.ClientID = ID
		return
	}
}

func WithDefaultExpiration(exp time.Duration) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.defaultRequest.Expiration = exp
		return
	}
}

func WithDefaultIssuer(iss string) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.defaultRequest.Issuer = iss
		return
	}
}

func WithStore(fn tokenIssuerStore) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.store = fn
		return
	}
}

func WithLookup(fn tokenIssuerLookup) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.lookup = fn
		return
	}
}

func WithGenerator(fn tokenIssuerGenerator) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.generator = fn
		return
	}
}

func WithSigner(fn tokenIssuerSigner) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.signer = fn
		return
	}
}

func (req *TokenRequest) apply(opt ...IssueOptFn) (err error) {
	for _, fn := range opt {
		if err = fn(req); err != nil {
			return
		}
	}

	return
}

func WithExpiration(e time.Duration) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.Expiration = e
		return
	}
}

func WithIdentity(i Identifiable) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.UserID = i.Identity()
		t.Roles = i.Roles()
		return
	}
}

func WithAccessToken(access string) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.AccessToken = access
		return
	}
}

func WithScope(ss ...string) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.Scope = ss
		return
	}
}

func WithAudience(aud string) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.Audience = aud
		return
	}
}

func WithClientID(id uint64) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.ClientID = id
		return
	}
}
package auth

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/cortezaproject/corteza-server/pkg/payload"
	"github.com/go-oauth2/oauth2/v4"
	"github.com/go-oauth2/oauth2/v4/generates"
	"github.com/go-oauth2/oauth2/v4/models"
	"github.com/lestrrat-go/jwx/jwa"
	"github.com/lestrrat-go/jwx/jwk"
	"github.com/lestrrat-go/jwx/jwt"
	"github.com/spf13/cast"
)

type (
	tokenIssuer struct {
		defaultRequest *TokenRequest

		// store issued tokens
		store tokenIssuerStore

		// lookup for issued tokens
		lookup tokenIssuerLookup

		// generator for issued tokens
		generator tokenIssuerGenerator

		// signer for issued tokens
		signer tokenIssuerSigner
	}

	TokenRequest struct {
		AccessToken  string
		RefreshToken string
		Expiration   time.Duration
		Audience     string
		Issuer       string
		IssuedAt     time.Time
		ClientID     uint64
		UserID       uint64
		Roles        []uint64
		Scope        []string
	}

	IssuerOptFn func(*tokenIssuer) error
	IssueOptFn  func(*TokenRequest) error

	tokenIssuerStore     func(context.Context, TokenRequest) error
	tokenIssuerLookup    func(context.Context, string) error
	tokenIssuerGenerator func(context.Context, TokenRequest) (string, string, error)
	tokenIssuerSigner    func(token jwt.Token) ([]byte, error)
)

var (
	TokenIssuer *tokenIssuer

	// wrapper around time.Now() that will aid service testing
	now = func() *time.Time {
		c := time.Now().Truncate(time.Second)
		return &c
	}
)

// NewTokenIssuer initializes and returns new instance of JWT manager
func NewTokenIssuer(opt ...IssuerOptFn) (tm *tokenIssuer, err error) {
	tm = &tokenIssuer{
		defaultRequest: &TokenRequest{Issuer: "cortezaproject.org"},

		store: func(ctx context.Context, request TokenRequest) error {
			return fmt.Errorf("token issuer store not configured")
		},

		lookup: func(context.Context, string) error {
			return fmt.Errorf("token issuer lookup not configured")
		},

		signer: func(token jwt.Token) ([]byte, error) {
			return nil, fmt.Errorf("token issuer signer not configured")
		},

		generator: DefaultAccessTokenGenerator,
	}

	for _, fn := range opt {
		if err = fn(tm); err != nil {
			return
		}
	}

	return
}

// Issue issues new access token, stores it and returns signed JWT.
func (tm *tokenIssuer) Issue(ctx context.Context, opt ...IssueOptFn) (_ []byte, err error) {
	var req = tm.newTokenRequest()
	if err = req.apply(opt...); err != nil {
		return
	}

	if len(req.AccessToken+req.RefreshToken) > 0 {
		panic("can not issue new token with preset access and refresh tokens, " +
			"this is most likely an implementation mistake")
	}

	if req.AccessToken, req.RefreshToken, err = tm.generator(ctx, *req); err != nil {
		return
	}

	if err = tm.store(ctx, *req); err != nil {
		return
	}

	return tm.sign(req)
}

func (tm *tokenIssuer) Sign(opt ...IssueOptFn) (_ []byte, err error) {
	var req = tm.newTokenRequest()
	if err = req.apply(opt...); err != nil {
		return
	}

	return tm.sign(req)
}

func (tm *tokenIssuer) sign(req *TokenRequest) ([]byte, error) {
	if token, err := makeToken(req); err != nil {
		return nil, err
	} else {
		return tm.signer(token)
	}
}

// Returns new token request and copies relevant values from the default token request on the issuer
func (tm *tokenIssuer) newTokenRequest() *TokenRequest {
	return &TokenRequest{
		Issuer:     tm.defaultRequest.Issuer,
		ClientID:   tm.defaultRequest.ClientID,
		Expiration: tm.defaultRequest.Expiration,
		IssuedAt:   *now(),
	}
}

// Validate performs token validation by checking existence of access-token in the store
func (tm *tokenIssuer) Validate(ctx context.Context, token jwt.Token) (err error) {
	if err = tm.lookup(ctx, token.JwtID()); err != nil {
		return errUnauthorized()
	}

	return nil
}

func makeToken(req *TokenRequest) (_ jwt.Token, err error) {
	var (
		roles = make([]string, len(req.Roles))
		token = jwt.New()

		toString = func(i uint64) string {
			return strconv.FormatUint(i, 10)
		}
	)

	if len(req.Scope) == 0 {
		// for backward compatibility we default
		// unset scope to profile & api
		req.Scope = []string{"profile", "api"}
	}

	if req.IssuedAt.IsZero() {
		req.IssuedAt = *now()
	}

	for i, r := range req.Roles {
		roles[i] = toString(r)
	}

	// The key part: store access token as JWT ID claim.
	// Claim will be extracted when JWT is validated and checked
	if err = token.Set(jwt.JwtIDKey, req.AccessToken); err != nil {
		return
	}

	if err = token.Set(jwt.SubjectKey, toString(req.UserID)); err != nil {
		return
	}

	if err = token.Set(jwt.ExpirationKey, now().Add(req.Expiration).Unix()); err != nil {
		return
	}

	if req.Audience != "" {
		if err = token.Set(jwt.AudienceKey, req.Audience); err != nil {
			return
		}
	}

	if err = token.Set(jwt.IssuerKey, req.Issuer); err != nil {
		return
	}

	if err = token.Set(jwt.IssuedAtKey, req.IssuedAt.Unix()); err != nil {
		return
	}

	if err = token.Set("clientID", toString(req.ClientID)); err != nil {
		return
	}

	if err = token.Set("scope", strings.Join(req.Scope, " ")); err != nil {
		return
	}

	if err = token.Set("roles", roles); err != nil {
		return
	}

	return token, nil
}

// IdentityFromToken decodes sub & roles claims into identity
func IdentityFromToken(token jwt.Token) *identity {
	if token == nil {
		return Anonymous()
	}

	var (
		roles, _ = token.Get("roles")
	)

	return Authenticated(
		cast.ToUint64(token.Subject()),
		payload.ParseUint64s(cast.ToStringSlice(roles))...,
	)
}

// DefaultAccessTokenGenerator uses token generator from oauth2 lib
func DefaultAccessTokenGenerator(ctx context.Context, req TokenRequest) (string, string, error) {
	return generates.NewAccessGenerate().Token(
		ctx,
		&oauth2.GenerateBasic{
			Client: &models.Client{
				ID: strconv.FormatUint(req.ClientID, 10),
			},
			UserID:    strconv.FormatUint(req.UserID, 10),
			CreateAt:  *now(),
			TokenInfo: nil,
			Request:   nil,
		},
		true,
	)
}

// WithSecretSigner configures token issuer with
func WithSecretSigner(secret string) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		if len(secret) == 0 {
			return fmt.Errorf("JWK missing")
		}

		var key jwk.Key
		if key, err = jwk.New([]byte(secret)); err != nil {
			return fmt.Errorf("could not parse JWK: %w", err)
		}

		tm.signer = func(token jwt.Token) ([]byte, error) {
			return jwt.Sign(token, jwa.HS512, key)
		}

		return nil
	}
}

// WithDefaultClientID configures ID of the default auth client
func WithDefaultClientID(ID uint64) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.defaultRequest.ClientID = ID
		return
	}
}

// WithDefaultExpiration configures default token expiration time
func WithDefaultExpiration(exp time.Duration) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.defaultRequest.Expiration = exp
		return
	}
}

// WithDefaultIssuer configures default issuer claim
func WithDefaultIssuer(iss string) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.defaultRequest.Issuer = iss
		return
	}
}

// WithStore configures store function
func WithStore(fn tokenIssuerStore) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.store = fn
		return
	}
}

// WithLookup configures lookup function
func WithLookup(fn tokenIssuerLookup) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.lookup = fn
		return
	}
}

// WithGenerator configures generator function
func WithGenerator(fn tokenIssuerGenerator) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.generator = fn
		return
	}
}

// WithSigner configures signer function
func WithSigner(fn tokenIssuerSigner) IssuerOptFn {
	return func(tm *tokenIssuer) (err error) {
		tm.signer = fn
		return
	}
}

func (req *TokenRequest) apply(opt ...IssueOptFn) (err error) {
	for _, fn := range opt {
		if err = fn(req); err != nil {
			return
		}
	}

	return
}

func WithExpiration(e time.Duration) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.Expiration = e
		return
	}
}

func WithIdentity(i Identifiable) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.UserID = i.Identity()
		t.Roles = i.Roles()
		return
	}
}

func WithAccessToken(access string) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.AccessToken = access
		return
	}
}

func WithScope(ss ...string) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.Scope = ss
		return
	}
}

func WithAudience(aud string) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.Audience = aud
		return
	}
}

func WithClientID(id uint64) IssueOptFn {
	return func(t *TokenRequest) (err error) {
		t.ClientID = id
		return
	}
}
