package policy

import (
	"fmt"
	"strings"

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

// Group represents an IAM Group with all information needed by the ui
type Group struct {
	Group           *iam.Group
	Users           []*iam.User
	ManagedPolicies []IAMPolicy
	RoleName        string
	Owner           *Owners
}

// CreateGroupRequest is what it says it is -_-
type CreateGroupRequest struct {
	Auth     Authorizer // Optional
	RoleName string     // Required
}

// CreateGroup creats an IAM group and attaches the rolepolicy of the same name
// 	-Verifies request (Auth not required)
//  -Pulls RolePolicy of associated roleARN
//  -Creates Group
// 		-IF roleName doesn't validate against sandstorm regex, add 'sandstorm-' infront
//
//  NOTICE: without an Authorizer, there is no validation that the namespace is valid.
func (pg *IAMPolicyGenerator) CreateGroup(req CreateGroupRequest) (*Group, error) {
	if req.RoleName == "" {
		return nil, InvalidInputError{prepend: "policy.CreateGroup", msg: "RoleName can't be empty"}
	}

	// TODO:Verify that the environments are the same, so a testing role can't accidently
	// create a prod group

	// Verify existence of RolePolicy
	attachedRolePolices, err := pg.ListAttachedRolePolicies(req.RoleName)
	if err != nil {
		return nil, err
	}

	// If req.Authorizer present, use it
	var owner *Owners
	if req.Auth != nil {
		// get RoleOwner
		owner, err = pg.GetRoleOwners(req.RoleName)
		if err != nil {
			return nil, err
		}
		if owner == nil {
			return nil, RoleOwnerDoesNotExist
		}
		err = req.Auth.AuthorizeLDAPGroups(owner.LDAPGroups)
		if err != nil {
			return nil, err
		}
	} else {
		pg.Logger.Warnln("policy.CreateGroup is continuing without formal authorization")
	}

	// SYS-9811 Group name and role name should have the same name
	groupName := req.RoleName

	input := iam.CreateGroupInput{
		GroupName: aws.String(groupName),
		Path:      aws.String(pg.IAMPathPrefix), // /sandstorm/{env}/
	}
	output, err := pg.IAM.CreateGroup(&input)
	if err != nil {
		return nil, err
	}

	// attach rolePolicies to group
	var groupPolicyInput iam.AttachGroupPolicyInput

	for _, rolePolicy := range attachedRolePolices {
		groupPolicyInput = iam.AttachGroupPolicyInput{
			GroupName: aws.String(groupName),
			PolicyArn: aws.String(rolePolicy.PolicyArn),
		}
		_, err = pg.IAM.AttachGroupPolicy(&groupPolicyInput)
		if err != nil {
			return nil, err
		}
	}

	// get policyName from arn
	// Should be the same as GroupName/RoleName, but rather be clear

	group := Group{
		Group:           output.Group,
		ManagedPolicies: attachedRolePolices,
		RoleName:        req.RoleName,
		Owner:           owner,
	}
	return &group, nil

}

// GetGroup returns information on a group, all users within the group, and all managed polices
// attached to a group.
// TODO: parallelize
func (pg *IAMPolicyGenerator) GetGroup(groupName string) (*Group, error) {

	iamGroup, err := pg.getGroup(groupName)
	if err != nil {
		return nil, err
	}

	groupPolicies, err := pg.listAttachedGroupPolicies(groupName)
	if err != nil {
		return nil, err
	}

	var managedPolicies []IAMPolicy
	var newPolicy IAMPolicy
	var doc IAMPolicyDocument

	// get RoleName from custom policy (since templated roles/policys share name)
	// if doesn't exist (legacy role/policy), ignore
	var roleName string

	for _, policy := range groupPolicies.AttachedPolicies {
		// TODO: Figure out why I have this here...
		if policy.PolicyArn == nil {
			break
		}

		doc, err = pg.GetPolicyDocument(*policy.PolicyArn)
		if err != nil {
			return nil, err
		}

		newPolicy = IAMPolicy{
			PolicyName: *policy.PolicyName,
			PolicyArn:  *policy.PolicyArn,
		}
		newPolicy.Policy = doc

		if *policy.PolicyArn != pg.RWAuxPolicyArn && *policy.PolicyArn != pg.AuxPolicyArn {
			roleName = *policy.PolicyName
		}

		managedPolicies = append(managedPolicies, newPolicy)

	}

	var owner *Owners
	if roleName != "" {
		owner, err = pg.GetRoleOwners(roleName)
		if err != nil {
			return nil, err
		}
	} else {
		owner = &Owners{InvalidState: true}
	}

	group := &Group{
		Group:           iamGroup.Group,
		Users:           iamGroup.Users,
		ManagedPolicies: managedPolicies,
		RoleName:        roleName,
		Owner:           owner,
	}

	return group, err
}

// CheckGroupExistence verifies that an IAM group exists. Lighter call than GetGroup
// NOTE: may fail for other reasons (the error isn't checked), The idea being if there
// is a failure here, you have bigger problems
func (pg *IAMPolicyGenerator) CheckGroupExistence(groupName string) bool {
	input := &iam.GetGroupInput{
		GroupName: aws.String(groupName),
	}

	_, err := pg.IAM.GetGroup(input)
	if err != nil {
		return false
	}

	return true
}

// helper to get all group policies attached to group
func (pg *IAMPolicyGenerator) listAttachedGroupPolicies(groupName string) (*iam.ListAttachedGroupPoliciesOutput, error) {
	listAttachedPolicesInput := &iam.ListAttachedGroupPoliciesInput{
		GroupName: aws.String(groupName),
	}

	var attachedPolicies []*iam.AttachedPolicy
	listAttachedPolicesOutput := &iam.ListAttachedGroupPoliciesOutput{}
	var err error

	for {
		listAttachedPolicesOutput, err = pg.IAM.ListAttachedGroupPolicies(listAttachedPolicesInput)
		if err != nil {
			return nil, err
		}

		attachedPolicies = append(attachedPolicies, listAttachedPolicesOutput.AttachedPolicies...)

		if listAttachedPolicesOutput.IsTruncated != nil && *listAttachedPolicesOutput.IsTruncated {
			listAttachedPolicesInput.Marker = listAttachedPolicesOutput.Marker
		} else {
			break
		}
	}
	listAttachedPolicesOutput.AttachedPolicies = attachedPolicies
	return listAttachedPolicesOutput, err
}

// ListGroups returns all Groups with "/sandstorm/{env}" path prefix
func (pg *IAMPolicyGenerator) ListGroups() ([]*iam.Group, error) {

	input := &iam.ListGroupsInput{
		PathPrefix: aws.String(pg.IAMPathPrefix), // /sandstorm/{env}/
	}

	output := &iam.ListGroupsOutput{}
	var groups, validGroups []*iam.Group
	var err error
	for {
		output, err = pg.IAM.ListGroups(input)
		if err != nil {
			return nil, err
		}

		validGroups = output.Groups
		groups = append(groups, validGroups...)
		// attachedPolicies = append(attachedPolicies, output.AttachedPolicies...)

		if output.IsTruncated != nil && *output.IsTruncated {
			input.Marker = output.Marker
		} else {
			break
		}
	}

	return groups, nil
}

// getGroup is a helper that returns all users in a group from IAM
func (pg *IAMPolicyGenerator) getGroup(groupName string) (*iam.GetGroupOutput, error) {

	input := &iam.GetGroupInput{
		GroupName: aws.String(groupName),
	}
	var users []*iam.User
	output := &iam.GetGroupOutput{}
	var err error
	for {
		output, err = pg.IAM.GetGroup(input)
		if err != nil {
			return nil, err
		}

		users = append(users, output.Users...)
		if output.IsTruncated != nil && *output.IsTruncated {
			input.Marker = output.Marker
		} else {
			break
		}
	}
	output.Users = users
	return output, err
}

// DeleteGroup delets a group from iam.
// -uses Authorizer to verify auth
// -removes users from group
// -detaches policies from group
// -deletes group
func (pg *IAMPolicyGenerator) DeleteGroup(groupName string, auth Authorizer) error {

	output, err := pg.GetGroup(groupName)
	if err != nil {
		return err
	}
	// if path invalid, fail
	if output.Group.Path == nil || *output.Group.Path == "" {
		return LegacyGroupDeletionFailure{
			msg:  "group has no defined pathPrefix, and is likely a legacy group. Cannot be managed from this interface",
			code: 409,
		}
	}

	var cleanPath string

	// strips the pre and post `/` from path
	cleanPath = strings.TrimPrefix(*output.Group.Path, "/")
	cleanPath = strings.TrimSuffix(cleanPath, "/")
	pathParts := strings.Split(cleanPath, "/")

	if pathParts[0] != "sandstorm" {
		return LegacyGroupDeletionFailure{
			msg:  fmt.Sprintf("pathPrefix in odd state. Expected: `sandstorm/{env} Actual: %s", *output.Group.Path),
			code: 409,
		}
	}

	// auth user
	var owner *Owners
	if auth != nil {
		/*
			Gets RoleName from  managed policyName for custom policy

			There is an assumtion here that there are to be two managed
			policies in a templated role, the aux policy and the custom
			policy. The Aux policy is general and given to everyone for
			basic sandstorm functionality, while the custom policy contains
			the secrets and namespaces needed for sandstorm key retrieval.

			We assume here that:
				-These two policies are the only two applied to the group
					(which inherited them from the role)
				-The custom policy's name is the same as role policies name
		*/

		// Verify client User is part of an owner group
		owner, err = pg.GetRoleOwners(output.RoleName)
		if err != nil {
			return err
		}
		if owner == nil {
			return RoleOwnerDoesNotExist
		}
		err = auth.AuthorizeLDAPGroups(owner.LDAPGroups)
		if err != nil {
			return err
		}
	}

	// TODO: verify the group env and service env are identical

	// ------FROM HERE START CHANGING RESOURCES------

	// remove users from group
	var removeUserInput = &iam.RemoveUserFromGroupInput{
		GroupName: aws.String(groupName),
	}

	for _, users := range output.Users {
		removeUserInput.UserName = users.UserName

		_, err := pg.IAM.RemoveUserFromGroup(removeUserInput)
		if err != nil {
			return err
		}
	}

	// Must detach all policies to be allowed to delete group
	groupPolicies, err := pg.listAttachedGroupPolicies(groupName)
	if err != nil {
		return err
	}

	var detachPolicyInput = &iam.DetachGroupPolicyInput{
		GroupName: aws.String(groupName),
	}

	attachedGroupAuxPolicyArn, err := pg.GetAttachedGroupAuxPolicyArn(groupName)
	if err != nil {
		return err
	}

	// attempt to detach and delete all policies attached to a group
	// IF policy is attached elsewhere (ie to a role). will continue without
	// 		erroring out
	for _, policy := range groupPolicies.AttachedPolicies {
		if policy.PolicyArn == nil || *policy.PolicyArn == "" {
			return LegacyGroupDeletionFailure{
				msg:  fmt.Sprintf("PolicyArn of policy %v not found; Attached Policy in weird state. Let #secdev know.", policy.PolicyName),
				code: 409,
			}
		}
		policyArn := policy.PolicyArn
		detachPolicyInput.PolicyArn = policyArn
		_, err = pg.IAM.DetachGroupPolicy(detachPolicyInput)
		if err != nil {
			pg.Logger.Errorf("policy: error detaching group policy: %s", err.Error())
			return err
		}

		// Do not try deleting aux policy even if there is nothing attached to it.
		if *policyArn == *attachedGroupAuxPolicyArn {
			continue
		}

		// Policy is already detached from the group.
		attachedEntities, err := pg.ListEntitiesAttachedToPolicies(*policyArn)
		if err != nil {
			// We would not be able to delete the policy, log the error and continue.
			pg.Logger.Errorf("error getting attached entities: %s", err.Error())
			err = nil
		}
		policyAttachedToOtherEntities := isPolicyAttachedToAnyOtherEntities(attachedEntities)

		if !policyAttachedToOtherEntities {

			//Delete all the policy version except default version.
			err = pg.DeleteAllPolicyVersions(*policyArn)
			if err != nil {
				pg.Logger.Errorf("delete group policy: %s", err.Error())
				err = nil
			}

			// delete the policy,
			_, err = pg.IAM.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: policy.PolicyArn})
			if err != nil {
				if awsErr, ok := err.(awserr.RequestFailure); ok && awsErr.Message() == "Cannot delete a policy attached to entities." {

					// ignore error and continue
					err = nil

				} else {
					pg.Logger.Errorf("policy: error deleting role policy: %s", err.Error())
					return err
				}
			}
		}
	}

	deleteGroupInput := &iam.DeleteGroupInput{
		GroupName: aws.String(groupName),
	}
	_, err = pg.IAM.DeleteGroup(deleteGroupInput)
	if err != nil {
		return err
	}

	// if role doesn't exist, delete roleOwner
	if owner != nil && !pg.CheckRoleExistence(groupName) {
		err = pg.DeleteRoleOwners(output.RoleName, auth)
		if err != nil {
			return err
		}
	}
	return err
}

// AddUsersToGroupInput cleans up the function signature
type AddUsersToGroupInput struct {
	Users     []string
	GroupName string
	Auth      Authorizer
}

// AddUsersToGroup does just that
func (pg *IAMPolicyGenerator) AddUsersToGroup(input AddUsersToGroupInput) error {
	if input.Auth != nil {
		group, err := pg.GetGroup(input.GroupName)
		if err != nil {
			return err
		}
		owner, err := pg.GetRoleOwners(group.RoleName)
		if err != nil {
			return err
		}
		if owner == nil {
			return RoleOwnerDoesNotExist
		}
		err = input.Auth.AuthorizeLDAPGroups(owner.LDAPGroups)
		if err != nil {
			return err
		}
	}
	addUsersinput := &iam.AddUserToGroupInput{
		GroupName: aws.String(input.GroupName),
	}
	for _, userName := range input.Users {
		addUsersinput.UserName = aws.String(userName)
		_, err := pg.IAM.AddUserToGroup(addUsersinput)
		if err != nil {
			return err
		}
	}
	return nil
}

// RemoveUsersFromGroupInput is just that
type RemoveUsersFromGroupInput AddUsersToGroupInput

// RemoveUsersFromGroup will:
// 	-If auth!=nil, validates user is in an owner ldap group
// 	-Iterates over userNames to remove each from group
func (pg *IAMPolicyGenerator) RemoveUsersFromGroup(input RemoveUsersFromGroupInput) error {
	if input.Auth != nil {
		group, err := pg.GetGroup(input.GroupName)
		if err != nil {
			return err
		}
		owner, err := pg.GetRoleOwners(group.RoleName)
		if err != nil {
			return err
		}
		if owner == nil {
			return RoleOwnerDoesNotExist
		}
		err = input.Auth.AuthorizeLDAPGroups(owner.LDAPGroups)
		if err != nil {
			return err
		}
	}
	removeUsersinput := &iam.RemoveUserFromGroupInput{
		GroupName: aws.String(input.GroupName),
	}
	for _, userName := range input.Users {
		removeUsersinput.UserName = aws.String(userName)
		_, err := pg.IAM.RemoveUserFromGroup(removeUsersinput)
		if err != nil {
			return err
		}
	}
	return nil
}

// VerifySandstormNamespace checks to see if string matches sandstorm namespace
func VerifySandstormNamespace(input string) bool {
	return sandstormRegex.MatchString(input)
}

// PatchGroup update aux policy attached to the group.
func (pg *IAMPolicyGenerator) PatchGroup(groupName string, writeAccess bool) error {
	// The only required portion of a request is the name
	if groupName == "" {
		err := InvalidInputError{prepend: "policy.patch", msg: "group name cannot be empty"}
		return err
	}

	currectAuxPolicyArn, err := pg.GetAttachedGroupAuxPolicyArn(groupName)
	if err != nil {
		return err
	}

	desiredAuxPolicyArn := pg.auxPolicyArn(writeAccess)
	if desiredAuxPolicyArn == *currectAuxPolicyArn {
		return nil
	}

	//aux policy attached to group name needs to be changed to desiredAuxPolicyArn
	// 1. detach the currectAuxPolicyArn
	// 2. attache the desiredAuxPolicyArn
	_, err = pg.DetachGroupPolicy(*currectAuxPolicyArn, groupName)
	if err != nil {
		return fmt.Errorf("Error detaching aux policy %s to group: %s. Error: %s", *currectAuxPolicyArn, groupName, err.Error())
	}

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

	return nil
}

func isPolicyAttachedToAnyOtherEntities(attachedEntities *AttachedEntities) bool {
	// Considering nil as empty.
	if attachedEntities == nil {
		return false
	}

	if len(attachedEntities.PolicyUsers) != 0 || len(attachedEntities.PolicyRoles) != 0 || len(attachedEntities.PolicyGroups) != 0 {
		return true
	}

	return false
}
