package policy

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/iam"
)

/*
Put updates role policy
Any user can update a role policy with secrets they have access to
	* get current policyArn+doc.
 	* If exists, determine which keys are new. Verify access to new keys
		*else, verify access to all keys
	*Generate new role policy
	*Verify aux policy is identical to existing
		*If not, change to match
	*Update assume role values. (will overwrite existing)

*/
func (pg *IAMPolicyGenerator) Put(
	request *PutRoleRequest,
	auth Authorizer,
	secrets SecretsManager,
) (response *PutRoleResponse, err error) {

	// The only required portion of a request is the name
	if request.RoleName == "" {
		err = InvalidInputError{prepend: "policy.PUT", msg: "*PutRoleRequest.RoleName cannot be empty"}
		return
	}

	// get current policy ARN + doc
	policyArn, existingPolicyDoc, err := pg.GetAttachedRolePolicy(request.RoleName)
	if err != nil {
		return
	}

	namespaces, err := ValidateNamespaces(request.SecretKeys, secrets) //need full namespace for policy document creation
	if err != nil {
		return
	}

	// If policy doesn't exist, verify user has access to all secrets
	if policyArn == "" {
		err = auth.AuthorizeSecretKeys(request.SecretKeys)
		if err != nil {
			return
		}
	} else {
		// Find SecretKeys
		existingSecretKeys := ExtractPolicySecretKeys(existingPolicyDoc)

		// If no statement in policy doc contains a SecretTableSid, ExtractPolicySecretKeys
		// will return a 0 len array. Error out
		if len(existingSecretKeys) == 0 {
			err = InvalidRolePolicyError{
				msg:  fmt.Sprintf("No SecretTableSid found in policy statement for %s", policyArn),
				code: http.StatusConflict,
			}
			return
		}

		// If nothing has changed, no need to authorize
		verifyKeys := compareSecretKeys(existingSecretKeys, request.SecretKeys)
		if len(verifyKeys) != 0 {
			err = auth.AuthorizeSecretKeys(verifyKeys)
			if err != nil {
				return
			}
		}

	}

	// RoleOwner may not exist. If it doesn't exist, but values were passed for it and user isn't admin, fail
	var roleOwnerExists = true

	// verification of roleOwners happens early (before any changes to resources are made),
	// but updating roleOwners comes last. updateRoleOwners will update to new value if true
	var updateRoleOwners = false

	// if owners changed, verify user has access to owners
	origOwners, err := pg.GetRoleOwners(request.RoleName)
	if err != nil {
		return
	}

	// origOwners == nil when roleOwners doesn't exist
	if origOwners == nil {
		roleOwnerExists = false
	}

	if roleOwnerExists {
		// if roleOwnerExists, never allow users to delete all owners (create unmanageable role)
		if len(request.Owners) == 0 {
			err = InvalidInputError{prepend: "policy.Put", msg: "Cannot have no owner for a role"}
			return
		}
		var newOwners *Owners
		newOwners = NewOwners(request.Owners)

		// Need to verify access if:
		// - If new values exist in request.Owners
		// - If new values don't contain all old values (some deleted)
		if origOwners.DoesNotContain(request.Owners) || !newOwners.ContainsAll(origOwners.LDAPGroups) {

			err = auth.AuthorizeLDAPGroups(origOwners.LDAPGroups)
			if err != nil {
				return
			}
			updateRoleOwners = true

		}
	} else if len(request.Owners) != 0 {
		// if owners in request but no RoleOwners exist:
		// 	-check if user is admin.
		//     -If admin, create roleOwner with request.Owners
		// 	   -else, fail

		if auth.IsAdmin() {
			origOwners, err = pg.CreateRoleOwners(request.RoleName, request.Owners, auth)
			if err != nil {
				return
			}
			// don't need to update role owner, will already be up to date
			updateRoleOwners = false
		} else {
			err = LegacyRoleWithoutRoleOwner
			return
		}

	}

	// Verify if allowedArns has changed
	//  - If it has changed, verify user is an owner
	var arnsNeedVerification bool
	arnsNeedVerification, err = pg.arnsNeedVerification(request)
	if err != nil {
		return
	}

	// if arnsNeedVerification and roleOwnerExists, verify owner
	// IF legacy role, (ie roleOwnerDoesn'tExist), don't break workflow and allow edit without verification
	if arnsNeedVerification && roleOwnerExists {
		err = auth.AuthorizeLDAPGroups(origOwners.LDAPGroups)
		if err != nil {
			return
		}
	}

	// Generate new rolePolicy
	policyDoc := pg.PolicyDocument(request.SecretKeys, namespaces, request.WriteAccess, pg.GenerateRoleARN(request.RoleName))
	policyDocBytes, err := json.Marshal(policyDoc)
	if err != nil {
		return
	}

	// If policyArn doesn't exist, create it
	if policyArn == "" {
		createPolicyInput := &iam.CreatePolicyInput{
			Path:           aws.String(pg.PathPrefix(IAMPolicyPathSuffix)),
			PolicyDocument: aws.String(string(policyDocBytes)),
			PolicyName:     aws.String(request.RoleName),
			Description:    aws.String(IAMPolicyDescription),
		}

		var createPolicyResponse *iam.CreatePolicyOutput
		createPolicyResponse, err = pg.IAM.CreatePolicy(createPolicyInput)
		if err != nil {
			return
		}

		_, err = pg.AttachRolePolicy(*createPolicyResponse.Policy.Arn, request.RoleName)
		if err != nil {
			return
		}

	} else {
		/*
			If policy exists:
				*Get a list of old policy versions
				*Create new version:
					*Applying new policy to it
					*using new policy version as default
				*Delete old policy version
		*/
		var policyVersions *iam.ListPolicyVersionsOutput
		policyVersions, err = pg.DeleteOldestPolicyVersion(policyArn)
		if err != nil {
			return
		}

		// Create new policy
		_, err = pg.CreateNewPolicyVersion(policyArn, policyDocBytes, true)
		if err != nil {
			return
		}

		// Delete old policies
		err = pg.DeletePolicyVersions(policyArn, policyVersions)
		if err != nil {
			return
		}

	}

	// Verify AuxPolicy is identical
	desiredAuxPolicyArn := pg.auxPolicyArn(request.WriteAccess)
	err = pg.attachDesiredAuxPolicyToRole(desiredAuxPolicyArn, request.RoleName)
	if err != nil {
		pg.Logger.Errorf("Error attaching aux policy: %s", err.Error())
		return
	}

	// update assume role targets
	assumeRolePolicyDocument := pg.AssumeRolePolicyDocument(request.AllowedArns)
	assumeRoleDoc, err := json.Marshal(assumeRolePolicyDocument)
	if err != nil {
		return
	}

	_, err = pg.IAM.UpdateAssumeRolePolicy(&iam.UpdateAssumeRolePolicyInput{
		PolicyDocument: aws.String(string(assumeRoleDoc)),
		RoleName:       aws.String(request.RoleName),
	})
	if err != nil {
		return
	}

	// Need roleArn
	getRoleOutput, err := pg.IAM.GetRole(&iam.GetRoleInput{RoleName: aws.String(request.RoleName)})
	if err != nil {
		return
	}

	// if any values contained in owners, overwrite existing owners
	// Will fail if owner doesn't exist
	var owners = origOwners

	if updateRoleOwners {
		owners, err = pg.UpdateRoleOwners(request.RoleName, request.Owners, auth)
		if err != nil {
			return
		}
	}
	if owners == nil {
		owners = &Owners{
			InvalidState: true,
		}
	}

	response = &PutRoleResponse{
		RoleArn:          aws.StringValue(getRoleOutput.Role.Arn),
		RoleName:         request.RoleName,
		Policy:           policyDoc,
		AssumeRolePolicy: pg.ExternalAssumeRolePolicyDocument(aws.StringValue(getRoleOutput.Role.Arn)),
		AllowedArns:      request.AllowedArns,
		WriteAccess:      request.WriteAccess,
		Owners:           owners,
	}
	return
}

func (pg *IAMPolicyGenerator) arnsNeedVerification(request *PutRoleRequest) (bool, error) {
	// Verify if allowedArns has changed
	//  -If it has changed, verify user is an owner
	allowedARNs, err := pg.GetAllowedARNs(request.RoleName)
	if err != nil {
		return false, err
	}

	// original is the initial state
	var originalAllowedArnsSet = make(map[string]struct{})
	for _, arn := range allowedARNs {
		originalAllowedArnsSet[arn] = struct{}{}
	}

	// values in request are the newAllowedArns
	var newAllowedArnsSet = make(map[string]struct{})
	for _, arn := range request.AllowedArns {
		newAllowedArnsSet[arn] = struct{}{}
	}

	// if in existing but not in new, needs to be an owner
	for key := range originalAllowedArnsSet {
		if _, ok := newAllowedArnsSet[key]; !ok {
			return true, nil
		}
	}

	for key := range newAllowedArnsSet {
		if _, ok := originalAllowedArnsSet[key]; !ok {
			return true, nil
		}
	}

	return false, nil
}

// compareSecretKeys compares 2 keys slices and returns the difference
func compareSecretKeys(existingSecretKeys []string, requestSecretKeys []string) []string {
	// optimize search on oldKeys (maybe many?)
	var oldKeys = make(map[string]struct{})
	for _, oldKey := range existingSecretKeys {
		oldKeys[oldKey] = struct{}{}
	}
	var newKeys = make(map[string]struct{})
	for _, newKey := range requestSecretKeys {
		newKeys[newKey] = struct{}{}
	}

	// set of keys to check
	var checkKeys = make(map[string]struct{})

	// find added keys
	for _, newKey := range requestSecretKeys {
		// if a newKey not in oldKeys, add to checkKeys
		if _, ok := oldKeys[newKey]; !ok {
			checkKeys[newKey] = struct{}{}
		}
	}

	for _, existingKey := range existingSecretKeys {
		// if an existingKey has been deleted from newKeys, add to removedKeys
		if _, ok := newKeys[existingKey]; !ok {
			checkKeys[existingKey] = struct{}{}
		}
	}

	// transform map to slice
	var verifyKeys []string

	for key := range checkKeys {
		verifyKeys = append(verifyKeys, key)
	}

	return verifyKeys
}

// attachDesiredAuxPolicyToRole attaches the desiredAuxPolicyArn and
// detaches the existing auxPolicyArn to the role
func (pg *IAMPolicyGenerator) attachDesiredAuxPolicyToRole(desiredAuxPolicyArn string, roleName string) error {

	auxPolicyArn, _, err := pg.GetAttachedRoleAuxPolicy(roleName)
	if err != nil {
		return fmt.Errorf("Error getting aux policy for %s. Error: %s", roleName, err.Error())
	}

	if auxPolicyArn == desiredAuxPolicyArn {
		return nil
	}

	// if auxPolicy has changed, detach old and attach new
	_, err = pg.DetachRolePolicy(auxPolicyArn, roleName)
	if err != nil {
		return fmt.Errorf("Error detaching aux policy %s to role: %s. Error: %s", auxPolicyArn, roleName, err.Error())
	}

	_, err = pg.AttachRolePolicy(desiredAuxPolicyArn, roleName)
	if err != nil {
		return fmt.Errorf("Error attaching aux policy %s to role %s. Error: %s", desiredAuxPolicyArn, roleName, err.Error())
	}
	return nil
}
