// Package auth deals with authentication and authorization against topics
package auth

import (
	"errors"
	"regexp"
)

// Auther is a generic interface to implement password-based authentication and authorization
type Auther interface {
	// Authenticate checks username and password and returns a user if correct. The method
	// returns in constant-ish time, regardless of whether the user exists or the password is
	// correct or incorrect.
	Authenticate(username, password string) (*User, error)

	// Authorize returns nil if the given user has access to the given topic using the desired
	// permission. The user param may be nil to signal an anonymous user.
	Authorize(user *User, topic string, perm Permission) error
}

// Manager is an interface representing user and access management
type Manager interface {
	// AddUser adds a user with the given username, password and role. The password should be hashed
	// before it is stored in a persistence layer.
	AddUser(username, password string, role Role) error

	// RemoveUser deletes the user with the given username. The function returns nil on success, even
	// if the user did not exist in the first place.
	RemoveUser(username string) error

	// Users returns a list of users. It always also returns the Everyone user ("*").
	Users() ([]*User, error)

	// User returns the user with the given username if it exists, or ErrNotFound otherwise.
	// You may also pass Everyone to retrieve the anonymous user and its Grant list.
	User(username string) (*User, error)

	// ChangePassword changes a user's password
	ChangePassword(username, password string) error

	// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
	// all existing access control entries (Grant) are removed, since they are no longer needed.
	ChangeRole(username string, role Role) error

	// AllowAccess adds or updates an entry in th access control list for a specific user. It controls
	// read/write access to a topic. The parameter topicPattern may include wildcards (*).
	AllowAccess(username string, topicPattern string, read bool, write bool) error

	// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
	// empty) for an entire user. The parameter topicPattern may include wildcards (*).
	ResetAccess(username string, topicPattern string) error

	// DefaultAccess returns the default read/write access if no access control entry matches
	DefaultAccess() (read bool, write bool)
}

// User is a struct that represents a user
type User struct {
	Name   string
	Hash   string // password hash (bcrypt)
	Role   Role
	Grants []Grant
}

// Grant is a struct that represents an access control entry to a topic
type Grant struct {
	TopicPattern string // May include wildcard (*)
	AllowRead    bool
	AllowWrite   bool
}

// Permission represents a read or write permission to a topic
type Permission int

// Permissions to a topic
const (
	PermissionRead  = Permission(1)
	PermissionWrite = Permission(2)
)

// Role represents a user's role, either admin or regular user
type Role string

// User roles
const (
	RoleAdmin     = Role("admin")
	RoleUser      = Role("user")
	RoleAnonymous = Role("anonymous")
)

// Everyone is a special username representing anonymous users
const (
	Everyone = "*"
)

var (
	allowedUsernameRegex     = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`)     // Does not include Everyone (*)
	allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
)

// AllowedRole returns true if the given role can be used for new users
func AllowedRole(role Role) bool {
	return role == RoleUser || role == RoleAdmin
}

// AllowedUsername returns true if the given username is valid
func AllowedUsername(username string) bool {
	return allowedUsernameRegex.MatchString(username)
}

// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
func AllowedTopicPattern(username string) bool {
	return allowedTopicPatternRegex.MatchString(username)
}

// Error constants used by the package
var (
	ErrUnauthenticated = errors.New("unauthenticated")
	ErrUnauthorized    = errors.New("unauthorized")
	ErrInvalidArgument = errors.New("invalid argument")
	ErrNotFound        = errors.New("not found")
)
