package policy

import (
	"encoding/json"
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

// Owners represent a set of LDAP Groups that own a role
type Owners struct {
	set          map[string]struct{}
	LDAPGroups   []string `json:"ldapgroups,omitempty"`
	InvalidState bool     `json:"invalidstate"`
}

// UnmarshalJSON does just that
func (own *Owners) UnmarshalJSON(buf []byte) error {

	type Alias Owners
	aux := &struct {
		*Alias
	}{
		Alias: (*Alias)(own),
	}

	err := json.Unmarshal(buf, aux)
	if err != nil {
		return err
	}

	own.set = make(map[string]struct{})
	for _, team := range own.LDAPGroups {
		own.set[team] = struct{}{}
	}

	return nil
}

func (own Owners) String() string {
	return fmt.Sprintf("%s", own.LDAPGroups)
}

// NewOwners takes a slice of LDAPGroups and returns Owners
// if empty slice passed in, will return a nil pointer
func NewOwners(groups []string) *Owners {
	if len(groups) == 0 {
		return nil
	}

	owners := &Owners{
		LDAPGroups: groups,
	}
	owners.set = make(map[string]struct{})
	for _, team := range groups {
		owners.set[team] = struct{}{}
	}
	return owners
}

// IsElement tells if the string is an element in owners
func (own *Owners) IsElement(team string) bool {
	_, ok := own.set[team]
	return ok

}

// DoesNotContain returns true if any of the given ldapGroups DO NOT exist
// in own
func (own *Owners) DoesNotContain(ldapGroups []string) bool {
	var ok bool
	for _, team := range ldapGroups {
		_, ok = own.set[team]
		if !ok {
			return true
		}
	}
	return false
}

// ContainsAll returns true if all passed values exist in owners
func (own *Owners) ContainsAll(ldapGroups []string) bool {
	return !own.DoesNotContain(ldapGroups)
}

// Contains returns true if any ldapGroups value exists in Owners
func (own *Owners) Contains(ldapGroups []string) bool {
	var ok bool
	for _, team := range ldapGroups {
		_, ok = own.set[team]
		if ok {
			return ok
		}
	}
	return false
}

// GetRoleOwners takes a roleName and returns an Owner
func (pg *IAMPolicyGenerator) GetRoleOwners(roleName string) (*Owners, error) {
	if roleName == "" {
		return nil, fmt.Errorf("please provide rolename to get role owners")
	}
	// GetItem returns no error if the object doesn't exist
	// erroring handled in NewOwners
	input := &dynamodb.GetItemInput{
		TableName:      aws.String(pg.RoleOwnerTableName),
		ConsistentRead: aws.Bool(true),
		Key: map[string]*dynamodb.AttributeValue{ // Required
			"rolename": { // Required
				S: aws.String(roleName),
			},
		},
		AttributesToGet: []*string{
			aws.String("owners"),
		},
	}
	output, err := pg.DynamoDB.GetItem(input)
	if err != nil {
		return nil, err
	}
	var holder []string
	err = dynamodbattribute.Unmarshal(output.Item["owners"], &holder)
	if err != nil {
		return nil, err
	}

	// NOTE: if holder is empty, roleOwner doesn't exist and fail
	if len(holder) == 0 {
		return nil, nil
	}
	return NewOwners(holder), err
}

// CreateRoleOwners creates the dynamoTable to track
func (pg *IAMPolicyGenerator) CreateRoleOwners(roleName string, roleOwners []string, auth Authorizer) (*Owners, error) {
	// validate input
	switch {
	case roleName == "":
		return nil, InvalidInputError{prepend: "policy.CreateRoleOwners", msg: "roleName can't be empty"}
	case len(roleOwners) == 0:
		return nil, InvalidInputError{prepend: "policy.CreateRoleOwners", msg: "roleOwners can't be empty"}
	}
	err := auth.AuthorizeLDAPGroups(roleOwners)
	if err != nil {
		return nil, err
	}

	owners := NewOwners(roleOwners)

	input := &dynamodb.PutItemInput{
		TableName: aws.String(pg.RoleOwnerTableName),
		Item: map[string]*dynamodb.AttributeValue{ // Required
			"rolename": { // Required
				S: aws.String(roleName),
			},
			"owners": { // Required
				SS: aws.StringSlice(roleOwners),
			},
		},
		// NOTE: ConditionExpression is implicitly used to determine
		// that role to create DOES NOT exist, ie return `ConditionalCheckFailedException`
		// when trying to create a roleOwner that already exists
		ConditionExpression: aws.String("attribute_not_exists (rolename)"),
	}
	_, err = pg.DynamoDB.PutItem(input)
	if err != nil {
		// If err is due to condition failing (check pre-existence),
		// handle more gracefullys
		switch newErr := err.(type) {
		case awserr.Error:
			if newErr.Code() == `ConditionalCheckFailedException` {
				return nil, RoleOwnerAlreadyExists
			}
			return nil, err
		default:
			return nil, err
		}

	}
	return owners, err

}

// UpdateRoleOwners updates role Owners -_-
func (pg *IAMPolicyGenerator) UpdateRoleOwners(roleName string, owners []string, auth Authorizer) (*Owners, error) {
	if len(owners) == 0 {
		return nil, NoOwnersPassed
	}

	err := auth.AuthorizeLDAPGroups(owners)
	if err != nil {
		return nil, err
	}

	input := &dynamodb.UpdateItemInput{
		TableName: aws.String(pg.RoleOwnerTableName),
		Key: map[string]*dynamodb.AttributeValue{ // Required
			"rolename": { // Required
				S: aws.String(roleName),
			},
		},
		// NOTE: ConditionExpression is implicitly used to determine
		// that role to update exists, ie return `ConditionalCheckFailedException`
		// when trying to update a nonexistent roleOwner
		ConditionExpression: aws.String("attribute_exists (rolename)"),
		UpdateExpression:    aws.String(fmt.Sprintf("set owners = :val1")),
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ // Required
			":val1": { // Required
				SS: aws.StringSlice(owners),
			},
		},
	}
	_, err = pg.DynamoDB.UpdateItem(input)
	if err != nil {
		// If err is due to condition failing (check pre-existence),
		// handle more gracefullys
		switch newErr := err.(type) {
		case awserr.Error:
			if newErr.Code() == `ConditionalCheckFailedException` {
				return nil, nil
			}
		default:
			return nil, err
		}
	}
	return NewOwners(owners), err
}

// DeleteRoleOwners will only delete if a user is a member of any single owner group
func (pg *IAMPolicyGenerator) DeleteRoleOwners(roleName string, auth Authorizer) error {
	//Determine if user is member of
	owner, err := pg.GetRoleOwners(roleName)
	if err != nil {
		return err
	}

	// allow nil auth in case of custom usage
	if auth != nil {
		err = auth.AuthorizeLDAPGroups(owner.LDAPGroups)
		if err != nil {
			return err
		}
	}
	input := &dynamodb.DeleteItemInput{
		TableName: aws.String(pg.RoleOwnerTableName),
		Key: map[string]*dynamodb.AttributeValue{ // Required
			"rolename": { // Required
				S: aws.String(roleName),
			},
		},
		// NOTE: ConditionExpression is implicitly used to determine
		// that role to delete exists, ie return `ConditionalCheckFailedException`
		// when trying to delete a nonexistent roleOwner
		ConditionExpression: aws.String("attribute_exists (rolename)"),
		ReturnValues:        aws.String("NONE"),
	}
	_, err = pg.DynamoDB.DeleteItem(input)
	if err != nil {
		// If err is due to condition failing (check pre-existence),
		// handle more gracefullys
		switch newErr := err.(type) {
		case awserr.Error:
			if newErr.Code() == `ConditionalCheckFailedException` {
				return RoleOwnerDoesNotExist
			}
		default:
			return err
		}
	}
	return err
}
