package guardian

import (
	"bytes"
	"encoding/hex"
	"fmt"

	"code.justin.tv/systems/guardian/osin"

	"github.com/satori/go.uuid"
)

// Client is an oauth application
type Client struct {
	ID            string   `json:"id,omitempty"`
	Secret        string   `json:"secret,omitempty"`
	SecretHash    string   `json:"secret_hash,omitempty"`
	HashAlgorithm string   `json:"hash_algorithm,omitempty"`
	Version       string   `json:"version,omitempty"`
	RedirectURI   string   `json:"redirect_uri,omitempty"`
	Name          string   `json:"name,omitempty"`
	Description   string   `json:"description,omitempty"`
	Homepage      string   `json:"homepage,omitempty"`
	Scopes        []string `json:"scopes,omitempty"`

	// CNs of groups that have access to this client
	Groups []string `json:"groups,omitempty"`
}

// DefaultClientScopes contains "user_ro"
var DefaultClientScopes = []string{"user_ro", "group_ro"}

// GetID returns client ID
func (client *Client) GetID() (id string) {
	id = client.ID
	return
}

// GetSecretHash returns hashed secret
func (client *Client) GetSecretHash() (hash string) {
	hash = client.SecretHash
	return
}

// GetHashAlgorithm returns name of algorithm used to hash secret
func (client *Client) GetHashAlgorithm() (alg string) {
	alg = client.HashAlgorithm
	return
}

// SetHashAlgorithm sets hash algorithm
func (client *Client) SetHashAlgorithm(algorithm string) {
	client.HashAlgorithm = algorithm
}

// GetVersion returns version
func (client *Client) GetVersion() (version string) {
	version = client.Version
	return
}

// CompareSecret compares plaintext secret with hashed secret
func (client *Client) CompareSecret(secret string) (ok bool) {
	var (
		h   []byte
		b   []byte
		err error
	)

	_, b, err = DefaultTokenGenerator.HashSecret(secret, nil)
	if err != nil {
		err = fmt.Errorf("guardian: error hashing secret: %s", err.Error())
		return
	}

	h, err = hex.DecodeString(client.SecretHash)
	if err != nil {
		return
	}

	if bytes.Compare(b, h) == 0 {
		ok = true
	}
	return
}

// GetRedirectURI returns client's configured redirect uri
func (client *Client) GetRedirectURI() (uri string) {
	uri = client.RedirectURI
	return
}

// GetUserData is a noop
func (client *Client) GetUserData() (data interface{}) {
	return
}

// NewVersion sets new version
func (client *Client) NewVersion() (version string) {
	version = uuid.NewV4().String()
	client.Version = version
	return
}

// RegenerateSecret generates a new client secret for the client and sets
// new version
func (client *Client) RegenerateSecret() (err error) {
	var s []byte
	_, s, err = DefaultTokenGenerator.GenerateSecret()
	if err != nil {
		err = fmt.Errorf("guardian: error generating client secret: %s", err.Error())
		return
	}

	var secretHash []byte
	_, secretHash, err = DefaultTokenGenerator.HashSecret("", s)
	if err != nil {
		err = fmt.Errorf("guardian: error hashing client secret: %s", err.Error())
		return
	}

	client.SecretHash = hex.EncodeToString(secretHash)
	client.Secret = hex.EncodeToString(s)
	client.Version = client.NewVersion()
	return
}

// AuthorizeUser checks to see if user is authorized for this client by
// checking for intersection between client whitelisted groups and user
// groups. If no groups are listed for a client, then group membership does not
// gate access to this client.
func (client *Client) AuthorizeUser(user *User) (ok bool) {
	if user == nil {
		return
	}

	if len(client.Groups) == 0 {
		ok = true
		return
	}

	if len(user.Groups) == 0 {
		return
	}

	ok = StringSliceIntersect(user.Groups, client.Groups)
	return
}

// NewClient takes a client configuration and assigns an ID, client secret and
// version.
func NewClient(name, redirectURI, description, homepage string, groups ...string) (client *Client, err error) {
	client = &Client{
		ID:            uuid.NewV4().String(),
		RedirectURI:   redirectURI,
		Name:          name,
		Homepage:      homepage,
		Description:   description,
		HashAlgorithm: DefaultHashAlgorithm,
		Scopes:        DefaultClientScopes,
		Groups:        groups,
	}

	err = client.RegenerateSecret()
	if err != nil {
		return
	}
	return
}

// CompareVersion compares client and access data versions
func CompareVersion(data *osin.AccessData) (ok bool) {
	if data == nil || data.Client == nil {
		return false
	}
	return data.Client.GetVersion() == data.Version
}
