// Package credentials provides credentials management for Kerberos 5 authentication.
package credentials

import (
	"time"

	"github.com/hashicorp/go-uuid"
	"gopkg.in/jcmturner/gokrb5.v7/iana/nametype"
	"gopkg.in/jcmturner/gokrb5.v7/keytab"
	"gopkg.in/jcmturner/gokrb5.v7/types"
)

const (
	// AttributeKeyADCredentials assigned number for AD credentials.
	AttributeKeyADCredentials = "gokrb5AttributeKeyADCredentials"
)

// Credentials struct for a user.
// Contains either a keytab, password or both.
// Keytabs are used over passwords if both are defined.
type Credentials struct {
	username    string
	displayName string
	realm       string
	cname       types.PrincipalName
	keytab      *keytab.Keytab
	password    string
	attributes  map[string]interface{}
	validUntil  time.Time

	authenticated   bool
	human           bool
	authTime        time.Time
	groupMembership map[string]bool
	sessionID       string
}

// ADCredentials contains information obtained from the PAC.
type ADCredentials struct {
	EffectiveName       string
	FullName            string
	UserID              int
	PrimaryGroupID      int
	LogOnTime           time.Time
	LogOffTime          time.Time
	PasswordLastSet     time.Time
	GroupMembershipSIDs []string
	LogonDomainName     string
	LogonDomainID       string
	LogonServer         string
}

// New creates a new Credentials instance.
func New(username string, realm string) *Credentials {
	uid, err := uuid.GenerateUUID()
	if err != nil {
		uid = "00unique-sess-ions-uuid-unavailable0"
	}
	return &Credentials{
		username:        username,
		displayName:     username,
		realm:           realm,
		cname:           types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, username),
		keytab:          keytab.New(),
		attributes:      make(map[string]interface{}),
		groupMembership: make(map[string]bool),
		sessionID:       uid,
		human:           true,
	}
}

// NewFromPrincipalName creates a new Credentials instance with the user details provides as a PrincipalName type.
func NewFromPrincipalName(cname types.PrincipalName, realm string) *Credentials {
	uid, err := uuid.GenerateUUID()
	if err != nil {
		uid = "00unique-sess-ions-uuid-unavailable0"
	}
	return &Credentials{
		username:        cname.PrincipalNameString(),
		displayName:     cname.PrincipalNameString(),
		realm:           realm,
		cname:           cname,
		keytab:          keytab.New(),
		attributes:      make(map[string]interface{}),
		groupMembership: make(map[string]bool),
		sessionID:       uid,
		human:           true,
	}
}

// WithKeytab sets the Keytab in the Credentials struct.
func (c *Credentials) WithKeytab(kt *keytab.Keytab) *Credentials {
	c.keytab = kt
	c.password = ""
	return c
}

// Keytab returns the credential's Keytab.
func (c *Credentials) Keytab() *keytab.Keytab {
	return c.keytab
}

// HasKeytab queries if the Credentials has a keytab defined.
func (c *Credentials) HasKeytab() bool {
	if c.keytab != nil && len(c.keytab.Entries) > 0 {
		return true
	}
	return false
}

// WithPassword sets the password in the Credentials struct.
func (c *Credentials) WithPassword(password string) *Credentials {
	c.password = password
	c.keytab = keytab.New() // clear any keytab
	return c
}

// Password returns the credential's password.
func (c *Credentials) Password() string {
	return c.password
}

// HasPassword queries if the Credentials has a password defined.
func (c *Credentials) HasPassword() bool {
	if c.password != "" {
		return true
	}
	return false
}

// SetValidUntil sets the expiry time of the credentials
func (c *Credentials) SetValidUntil(t time.Time) {
	c.validUntil = t
}

// SetADCredentials adds ADCredentials attributes to the credentials
func (c *Credentials) SetADCredentials(a ADCredentials) {
	c.SetAttribute(AttributeKeyADCredentials, a)
	if a.FullName != "" {
		c.SetDisplayName(a.FullName)
	}
	if a.EffectiveName != "" {
		c.SetUserName(a.EffectiveName)
	}
	for i := range a.GroupMembershipSIDs {
		c.AddAuthzAttribute(a.GroupMembershipSIDs[i])
	}
}

// Methods to implement goidentity.Identity interface

// UserName returns the credential's username.
func (c *Credentials) UserName() string {
	return c.username
}

// SetUserName sets the username value on the credential.
func (c *Credentials) SetUserName(s string) {
	c.username = s
}

// CName returns the credential's client principal name.
func (c *Credentials) CName() types.PrincipalName {
	return c.cname
}

// SetCName sets the client principal name on the credential.
func (c *Credentials) SetCName(pn types.PrincipalName) {
	c.cname = pn
}

// Domain returns the credential's domain.
func (c *Credentials) Domain() string {
	return c.realm
}

// SetDomain sets the domain value on the credential.
func (c *Credentials) SetDomain(s string) {
	c.realm = s
}

// Realm returns the credential's realm. Same as the domain.
func (c *Credentials) Realm() string {
	return c.Domain()
}

// SetRealm sets the realm value on the credential. Same as the domain
func (c *Credentials) SetRealm(s string) {
	c.SetDomain(s)
}

// DisplayName returns the credential's display name.
func (c *Credentials) DisplayName() string {
	return c.displayName
}

// SetDisplayName sets the display name value on the credential.
func (c *Credentials) SetDisplayName(s string) {
	c.displayName = s
}

// Human returns if the  credential represents a human or not.
func (c *Credentials) Human() bool {
	return c.human
}

// SetHuman sets the credential as human.
func (c *Credentials) SetHuman(b bool) {
	c.human = b
}

// AuthTime returns the time the credential was authenticated.
func (c *Credentials) AuthTime() time.Time {
	return c.authTime
}

// SetAuthTime sets the time the credential was authenticated.
func (c *Credentials) SetAuthTime(t time.Time) {
	c.authTime = t
}

// AuthzAttributes returns the credentials authorizing attributes.
func (c *Credentials) AuthzAttributes() []string {
	s := make([]string, len(c.groupMembership))
	i := 0
	for a := range c.groupMembership {
		s[i] = a
		i++
	}
	return s
}

// Authenticated indicates if the credential has been successfully authenticated or not.
func (c *Credentials) Authenticated() bool {
	return c.authenticated
}

// SetAuthenticated sets the credential as having been successfully authenticated.
func (c *Credentials) SetAuthenticated(b bool) {
	c.authenticated = b
}

// AddAuthzAttribute adds an authorization attribute to the credential.
func (c *Credentials) AddAuthzAttribute(a string) {
	c.groupMembership[a] = true
}

// RemoveAuthzAttribute removes an authorization attribute from the credential.
func (c *Credentials) RemoveAuthzAttribute(a string) {
	if _, ok := c.groupMembership[a]; !ok {
		return
	}
	delete(c.groupMembership, a)
}

// EnableAuthzAttribute toggles an authorization attribute to an enabled state on the credential.
func (c *Credentials) EnableAuthzAttribute(a string) {
	if enabled, ok := c.groupMembership[a]; ok && !enabled {
		c.groupMembership[a] = true
	}
}

// DisableAuthzAttribute toggles an authorization attribute to a disabled state on the credential.
func (c *Credentials) DisableAuthzAttribute(a string) {
	if enabled, ok := c.groupMembership[a]; ok && enabled {
		c.groupMembership[a] = false
	}
}

// Authorized indicates if the credential has the specified authorizing attribute.
func (c *Credentials) Authorized(a string) bool {
	if enabled, ok := c.groupMembership[a]; ok && enabled {
		return true
	}
	return false
}

// SessionID returns the credential's session ID.
func (c *Credentials) SessionID() string {
	return c.sessionID
}

// Expired indicates if the credential has expired.
func (c *Credentials) Expired() bool {
	if !c.validUntil.IsZero() && time.Now().UTC().After(c.validUntil) {
		return true
	}
	return false
}

// Attributes returns the Credentials' attributes map.
func (c *Credentials) Attributes() map[string]interface{} {
	return c.attributes
}

// SetAttribute sets the value of an attribute.
func (c *Credentials) SetAttribute(k string, v interface{}) {
	c.attributes[k] = v
}

// SetAttributes replaces the attributes map with the one provided.
func (c *Credentials) SetAttributes(a map[string]interface{}) {
	c.attributes = a
}

// RemoveAttribute deletes an attribute from the attribute map that has the key provided.
func (c *Credentials) RemoveAttribute(k string) {
	delete(c.attributes, k)
}
