// Package auth provides authentication and authorization capability
package auth

import (
	"context"
	"errors"
	"time"
)

const (
	// BearerScheme used for Authorization header.
	BearerScheme = "Bearer "
	// ScopePublic is the scope applied to a rule to allow access to the public.
	ScopePublic = ""
	// ScopeAccount is the scope applied to a rule to limit to users with any valid account.
	ScopeAccount = "*"
)

var (
	// ErrInvalidToken is when the token provided is not valid.
	ErrInvalidToken = errors.New("invalid token provided")
	// ErrForbidden is when a user does not have the necessary scope to access a resource.
	ErrForbidden = errors.New("resource forbidden")
)

// Auth provides authentication and authorization.
type Auth interface {
	// Init the auth
	Init(opts ...Option)
	// Options set for auth
	Options() Options
	// Generate a new account
	Generate(id string, opts ...GenerateOption) (*Account, error)
	// Inspect a token
	Inspect(token string) (*Account, error)
	// Token generated using refresh token or credentials
	Token(opts ...TokenOption) (*Token, error)
	// String returns the name of the implementation
	String() string
}

// Rules manages access to resources.
type Rules interface {
	// Verify an account has access to a resource using the rules
	Verify(acc *Account, res *Resource, opts ...VerifyOption) error
	// Grant access to a resource
	Grant(rule *Rule) error
	// Revoke access to a resource
	Revoke(rule *Rule) error
	// List returns all the rules used to verify requests
	List(...ListOption) ([]*Rule, error)
}

// Account provided by an auth provider.
type Account struct {
	// Any other associated metadata
	Metadata map[string]string `json:"metadata"`
	// ID of the account e.g. email
	ID string `json:"id"`
	// Type of the account, e.g. service
	Type string `json:"type"`
	// Issuer of the account
	Issuer string `json:"issuer"`
	// Secret for the account, e.g. the password
	Secret string `json:"secret"`
	// Scopes the account has access to
	Scopes []string `json:"scopes"`
}

// Token can be short or long lived.
type Token struct {
	// Time of token creation
	Created time.Time `json:"created"`
	// Time of token expiry
	Expiry time.Time `json:"expiry"`
	// The token to be used for accessing resources
	AccessToken string `json:"access_token"`
	// RefreshToken to be used to generate a new token
	RefreshToken string `json:"refresh_token"`
}

// Expired returns a boolean indicating if the token needs to be refreshed.
func (t *Token) Expired() bool {
	return t.Expiry.Unix() < time.Now().Unix()
}

// Resource is an entity such as a user or.
type Resource struct {
	// Name of the resource, e.g. go.micro.service.notes
	Name string `json:"name"`
	// Type of resource, e.g. service
	Type string `json:"type"`
	// Endpoint resource e.g NotesService.Create
	Endpoint string `json:"endpoint"`
}

// Access defines the type of access a rule grants.
type Access int

const (
	// AccessGranted to a resource.
	AccessGranted Access = iota
	// AccessDenied to a resource.
	AccessDenied
)

// Rule is used to verify access to a resource.
type Rule struct {
	// Resource the rule applies to
	Resource *Resource
	// ID of the rule, e.g. "public"
	ID string
	// Scope the rule requires, a blank scope indicates open to the public and * indicates the rule
	// applies to any valid account
	Scope string
	// Access determines if the rule grants or denies access to the resource
	Access Access
	// Priority the rule should take when verifying a request, the higher the value the sooner the
	// rule will be applied
	Priority int32
}

type accountKey struct{}

// AccountFromContext gets the account from the context, which
// is set by the auth wrapper at the start of a call. If the account
// is not set, a nil account will be returned. The error is only returned
// when there was a problem retrieving an account.
func AccountFromContext(ctx context.Context) (*Account, bool) {
	acc, ok := ctx.Value(accountKey{}).(*Account)
	return acc, ok
}

// ContextWithAccount sets the account in the context.
func ContextWithAccount(ctx context.Context, account *Account) context.Context {
	return context.WithValue(ctx, accountKey{}, account)
}
