package user

import (
	"context"
	"fmt"
	"time"

	"code.justin.tv/beefcake/server/internal/awsutil"
	"code.justin.tv/beefcake/server/internal/config"
	"code.justin.tv/beefcake/server/rpc/beefcake"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
	"github.com/aws/aws-sdk-go/service/dynamodb/expression"
)

// dynamodb attributes
const (
	PermissionsAttribute     = "Permissions"
	RoleMembershipsAttribute = "RoleMemberships"
	LastAccessTimeAttribute  = "LastAccessTime"
)

type errUserDoesNotExist struct {
	UserID string
}

func (err errUserDoesNotExist) Error() string {
	return fmt.Sprintf("user does not exist: %s", err.UserID)
}

// Users update permissions for users
type Users struct {
	Config   *config.Config
	DynamoDB dynamodbiface.DynamoDBAPI
}

// Get fetches a single user with their permissions
func (u *Users) Get(ctx context.Context, userID string) (*User, error) {
	res, err := u.DynamoDB.GetItemWithContext(ctx, &dynamodb.GetItemInput{
		Key: map[string]*dynamodb.AttributeValue{
			u.Config.UsersHashKey.Get(): {S: aws.String(userID)},
		},
		TableName: aws.String(u.Config.UsersTableName.Get()),
	})
	if err != nil {
		return nil, err
	}

	r := new(User)
	if err := dynamodbattribute.UnmarshalMap(res.Item, r); err != nil {
		return nil, err
	}
	return r, nil
}

// AddRoleMembership adds all permissions to a user.
func (u *Users) AddRoleMembership(ctx context.Context, userID string, role *beefcake.User_RoleMembership, permissions Permissions) error {
	if err := u.updateRoleMembership(ctx, userID, role, permissions); err != nil {
		if _, ok := err.(errUserDoesNotExist); ok {
			return u.setRoleMembership(ctx, userID, role, permissions)
		}
		return err
	}

	return nil
}

func (u *Users) updateRoleMembership(ctx context.Context, userID string, userRole *beefcake.User_RoleMembership, permissions Permissions) error {
	update := expression.Set(
		expression.Name(u.rolePath(userRole.GetId())),
		expression.Value(roleMembership(*userRole)),
	)
	for permissionID, perm := range permissions {
		update = update.Set(
			expression.Name(u.permissionPath(permissionID)),
			expression.Value(perm),
		)
	}

	eb := expression.NewBuilder().
		WithCondition(expression.And(
			expression.Name(PermissionsAttribute).AttributeExists(),
			expression.Name(RoleMembershipsAttribute).AttributeExists(),
		)).
		WithUpdate(update)

	expr, err := eb.Build()
	if err != nil {
		return err
	}

	if _, err := u.DynamoDB.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{
		ConditionExpression:       expr.Condition(),
		ExpressionAttributeNames:  expr.Names(),
		ExpressionAttributeValues: expr.Values(),
		Key: map[string]*dynamodb.AttributeValue{
			u.Config.UsersHashKey.Get(): {S: aws.String(userID)},
		},
		TableName:        aws.String(u.Config.UsersTableName.Get()),
		UpdateExpression: expr.Update(),
	}); err != nil {
		if awsutil.IsStatusCode(err, dynamodb.ErrCodeConditionalCheckFailedException) {
			return errUserDoesNotExist{UserID: userID}
		}
		return err
	}

	return nil
}

func (u *Users) setRoleMembership(ctx context.Context, userID string, role *beefcake.User_RoleMembership, permissions Permissions) error {
	expr, err := expression.NewBuilder().
		WithCondition(expression.And(
			expression.Name(PermissionsAttribute).AttributeNotExists(),
			expression.Name(RoleMembershipsAttribute).AttributeNotExists(),
		)).
		WithUpdate(expression.
			Set(
				expression.Name(RoleMembershipsAttribute),
				expression.Value(RoleMemberships([]*beefcake.User_RoleMembership{role})),
			).
			Set(
				expression.Name(PermissionsAttribute),
				expression.Value(permissions),
			),
		).
		Build()
	if err != nil {
		return err
	}

	if _, err := u.DynamoDB.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{
		ConditionExpression:       expr.Condition(),
		ExpressionAttributeNames:  expr.Names(),
		ExpressionAttributeValues: expr.Values(),
		Key: map[string]*dynamodb.AttributeValue{
			u.Config.UsersHashKey.Get(): {S: aws.String(userID)},
		},
		TableName:        aws.String(u.Config.UsersTableName.Get()),
		UpdateExpression: expr.Update(),
	}); err != nil {
		return err
	}

	return nil
}

// UpdateAccessTime updates the time that a user has accessed their permissions
func (u *Users) UpdateAccessTime(ctx context.Context, userID string, lastAccessTime time.Time) error {
	expr, err := expression.NewBuilder().
		WithUpdate(expression.Set(
			expression.Name(LastAccessTimeAttribute),
			expression.Value(lastAccessTime),
		)).
		Build()
	if err != nil {
		return err
	}

	_, err = u.DynamoDB.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{
		ExpressionAttributeNames:  expr.Names(),
		ExpressionAttributeValues: expr.Values(),
		Key: map[string]*dynamodb.AttributeValue{
			u.Config.UsersHashKey.Get(): {S: aws.String(userID)},
		},
		TableName:        aws.String(u.Config.UsersTableName.Get()),
		UpdateExpression: expr.Update(),
	})
	return err
}

// RemoveRoleMembership removes all permissions from a user from a role.
func (u *Users) RemoveRoleMembership(ctx context.Context, userID, roleID string, permissions Permissions) error {
	update := expression.Remove(expression.Name(u.rolePath(roleID)))
	for permID := range permissions {
		update = update.Remove(expression.Name(u.permissionPath(permID)))
	}

	expr, err := expression.NewBuilder().
		WithUpdate(update).
		Build()
	if err != nil {
		return err
	}

	_, err = u.DynamoDB.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{
		ExpressionAttributeNames:  expr.Names(),
		ExpressionAttributeValues: expr.Values(),
		Key: map[string]*dynamodb.AttributeValue{
			u.Config.UsersHashKey.Get(): {S: aws.String(userID)},
		},
		TableName:        aws.String(u.Config.UsersTableName.Get()),
		UpdateExpression: expr.Update(),
	})
	return err
}

// RemovePermissions removes all permissions from a user.
func (u *Users) RemovePermissions(ctx context.Context, userID string, permissions Permissions) error {
	if len(permissions) == 0 {
		return nil
	}

	var update expression.UpdateBuilder
	for permID := range permissions {
		update = update.Remove(expression.Name(u.permissionPath(permID)))
	}

	expr, err := expression.NewBuilder().
		WithUpdate(update).
		Build()
	if err != nil {
		return err
	}

	_, err = u.DynamoDB.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{
		ExpressionAttributeNames:  expr.Names(),
		ExpressionAttributeValues: expr.Values(),
		Key: map[string]*dynamodb.AttributeValue{
			u.Config.UsersHashKey.Get(): {S: aws.String(userID)},
		},
		TableName:        aws.String(u.Config.UsersTableName.Get()),
		UpdateExpression: expr.Update(),
	})
	return err
}

// permissionPath returns the dynamodb path to a permission id
func (u *Users) permissionPath(permissionID string) string {
	return PermissionsAttribute + "." + permissionID
}

// rolePath returns the dynamodb path to a permission id
func (u *Users) rolePath(roleID string) string {
	return RoleMembershipsAttribute + "." + roleID
}
