package jwt

import (
	"sync"

	"github.com/micro/go-micro/v2/auth"
	"github.com/micro/go-micro/v2/auth/token"
	jwtToken "github.com/micro/go-micro/v2/auth/token/jwt"
)

// NewAuth returns a new instance of the Auth service
func NewAuth(opts ...auth.Option) auth.Auth {
	j := new(jwt)
	j.Init(opts...)
	return j
}

type rule struct {
	role     string
	resource *auth.Resource
}

type jwt struct {
	options auth.Options
	jwt     token.Provider
	rules   []*rule

	sync.Mutex
}

func (j *jwt) String() string {
	return "jwt"
}

func (j *jwt) Init(opts ...auth.Option) {
	j.Lock()
	defer j.Unlock()

	for _, o := range opts {
		o(&j.options)
	}

	if len(j.options.Namespace) == 0 {
		j.options.Namespace = auth.DefaultNamespace
	}

	j.jwt = jwtToken.NewTokenProvider(
		token.WithPrivateKey(j.options.PrivateKey),
		token.WithPublicKey(j.options.PublicKey),
	)
}

func (j *jwt) Options() auth.Options {
	j.Lock()
	defer j.Unlock()
	return j.options
}

func (j *jwt) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
	options := auth.NewGenerateOptions(opts...)
	account := &auth.Account{
		ID:        id,
		Type:      options.Type,
		Roles:     options.Roles,
		Provider:  options.Provider,
		Metadata:  options.Metadata,
		Namespace: options.Namespace,
	}

	// generate a JWT secret which can be provided to the Token() method
	// and exchanged for an access token
	secret, err := j.jwt.Generate(account)
	if err != nil {
		return nil, err
	}
	account.Secret = secret.Token

	// return the account
	return account, nil
}

func (j *jwt) Grant(role string, res *auth.Resource) error {
	j.Lock()
	defer j.Unlock()
	j.rules = append(j.rules, &rule{role, res})
	return nil
}

func (j *jwt) Revoke(role string, res *auth.Resource) error {
	j.Lock()
	defer j.Unlock()

	rules := make([]*rule, 0, len(j.rules))

	var ruleFound bool
	for _, r := range rules {
		if r.role == role && r.resource == res {
			ruleFound = true
		} else {
			rules = append(rules, r)
		}
	}

	if !ruleFound {
		return auth.ErrNotFound
	}

	j.rules = rules
	return nil
}

func (j *jwt) Verify(acc *auth.Account, res *auth.Resource) error {
	j.Lock()
	if len(res.Namespace) == 0 {
		res.Namespace = j.options.Namespace
	}
	rules := j.rules
	j.Unlock()

	for _, rule := range rules {
		// validate the rule applies to the requested resource
		if rule.resource.Namespace != "*" && rule.resource.Namespace != res.Namespace {
			continue
		}
		if rule.resource.Type != "*" && rule.resource.Type != res.Type {
			continue
		}
		if rule.resource.Name != "*" && rule.resource.Name != res.Name {
			continue
		}
		if rule.resource.Endpoint != "*" && rule.resource.Endpoint != res.Endpoint {
			continue
		}

		// a blank role indicates anyone can access the resource, even without an account
		if rule.role == "" {
			return nil
		}

		// all furter checks require an account
		if acc == nil {
			continue
		}

		// this rule allows any account access, allow the request
		if rule.role == "*" {
			return nil
		}

		// if the account has the necessary role, allow the request
		for _, r := range acc.Roles {
			if r == rule.role {
				return nil
			}
		}
	}

	// no rules matched, forbid the request
	return auth.ErrForbidden
}

func (j *jwt) Inspect(token string) (*auth.Account, error) {
	return j.jwt.Inspect(token)
}

func (j *jwt) Token(opts ...auth.TokenOption) (*auth.Token, error) {
	options := auth.NewTokenOptions(opts...)

	secret := options.RefreshToken
	if len(options.Secret) > 0 {
		secret = options.Secret
	}

	account, err := j.jwt.Inspect(secret)
	if err != nil {
		return nil, err
	}

	tok, err := j.jwt.Generate(account, token.WithExpiry(options.Expiry))
	if err != nil {
		return nil, err
	}

	return &auth.Token{
		Created:      tok.Created,
		Expiry:       tok.Expiry,
		AccessToken:  tok.Token,
		RefreshToken: tok.Token,
	}, nil
}