package google

import (
	"fmt"
	"strings"

	"github.com/hashicorp/terraform/helper/schema"
	"github.com/hashicorp/terraform/helper/validation"
	"google.golang.org/api/storage/v1"
)

func resourceStorageObjectAcl() *schema.Resource {
	return &schema.Resource{
		Create:        resourceStorageObjectAclCreate,
		Read:          resourceStorageObjectAclRead,
		Update:        resourceStorageObjectAclUpdate,
		Delete:        resourceStorageObjectAclDelete,
		CustomizeDiff: resourceStorageObjectAclDiff,

		Schema: map[string]*schema.Schema{
			"bucket": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"object": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"predefined_acl": {
				Type:          schema.TypeString,
				Optional:      true,
				ConflictsWith: []string{"role_entity"},
				ValidateFunc:  validation.StringInSlice([]string{"private", "bucketOwnerRead", "bucketOwnerFullControl", "projectPrivate", "authenticatedRead", "publicRead", ""}, false),
			},

			"role_entity": {
				Type:     schema.TypeSet,
				Optional: true,
				Computed: true,
				Elem: &schema.Schema{
					Type:         schema.TypeString,
					ValidateFunc: validateRoleEntityPair,
				},
				ConflictsWith: []string{"predefined_acl"},
			},
		},
	}
}

// We can't edit the object owner (at risk of 403 errors), and users will always see a diff if they
// don't explicitly specify that it has OWNER permissions.
// Suppressing it means their configs won't be *strictly* correct as they will be missing the object
// owner having OWNER. It's impossible to remove that permission though, so this custom diff
// makes configs with or without that line indistinguishable.
func resourceStorageObjectAclDiff(diff *schema.ResourceDiff, meta interface{}) error {
	config := meta.(*Config)
	bucket := diff.Get("bucket").(string)
	object := diff.Get("object").(string)

	sObject, err := config.clientStorage.Objects.Get(bucket, object).Projection("full").Do()
	if err != nil {
		// Failing here is OK! Generally, it means we are at Create although it could mean the resource is gone.
		// Create won't show the object owner being given
		return nil
	}

	objectOwner := sObject.Owner.Entity
	ownerRole := fmt.Sprintf("%s:%s", "OWNER", objectOwner)
	oldRoleEntity, newRoleEntity := diff.GetChange("role_entity")

	// We can fail at plan time if the object owner/creator is being set to
	// a reader
	for _, v := range newRoleEntity.(*schema.Set).List() {
		res := getValidatedRoleEntityPair(v.(string))

		if res.Entity == objectOwner && res.Role != "OWNER" {
			return fmt.Errorf("New state tried setting object owner entity (%s) to non-'OWNER' role", objectOwner)
		}
	}

	// Diffs won't match in Plan and Apply pre-create if we naively add the RE
	// every time. So instead, we check to see if the old state (upstream/gcp
	// because we will have just done a refresh) contains it first.
	if oldRoleEntity.(*schema.Set).Contains(ownerRole) &&
		!newRoleEntity.(*schema.Set).Contains(ownerRole) {
		newRoleEntity.(*schema.Set).Add(ownerRole)
		return diff.SetNew("role_entity", newRoleEntity)
	}

	return nil
}

func getObjectAclId(object string) string {
	return object + "-acl"
}

func resourceStorageObjectAclCreate(d *schema.ResourceData, meta interface{}) error {
	config := meta.(*Config)

	bucket := d.Get("bucket").(string)
	object := d.Get("object").(string)

	// If we're using a predefined acl we just use the canned api.
	if predefinedAcl, ok := d.GetOk("predefined_acl"); ok {
		res, err := config.clientStorage.Objects.Get(bucket, object).Do()
		if err != nil {
			return fmt.Errorf("Error reading object %s in %s: %v", object, bucket, err)
		}

		_, err = config.clientStorage.Objects.Update(bucket, object, res).PredefinedAcl(predefinedAcl.(string)).Do()
		if err != nil {
			return fmt.Errorf("Error updating object %s in %s: %v", object, bucket, err)
		}

		return resourceStorageObjectAclRead(d, meta)
	} else if reMap := d.Get("role_entity").(*schema.Set); reMap.Len() > 0 {
		sObject, err := config.clientStorage.Objects.Get(bucket, object).Projection("full").Do()
		if err != nil {
			return fmt.Errorf("Error reading object %s in %s: %v", object, bucket, err)
		}

		objectOwner := sObject.Owner.Entity
		roleEntitiesUpstream, err := getRoleEntitiesAsStringsFromApi(config, bucket, object)
		if err != nil {
			return fmt.Errorf("Error reading object %s in %s: %v", object, bucket, err)
		}

		create, update, remove, err := getRoleEntityChange(roleEntitiesUpstream, convertStringSet(reMap), objectOwner)
		if err != nil {
			return fmt.Errorf("Error reading object %s in %s. Invalid schema: %v", object, bucket, err)
		}

		err = performStorageObjectRoleEntityOperations(create, update, remove, config, bucket, object)
		if err != nil {
			return fmt.Errorf("Error creating object %s in %s: %v", object, bucket, err)
		}

		return resourceStorageObjectAclRead(d, meta)
	}

	return fmt.Errorf("Error, you must specify either \"predefined_acl\" or \"role_entity\"")
}

func resourceStorageObjectAclRead(d *schema.ResourceData, meta interface{}) error {
	config := meta.(*Config)

	bucket := d.Get("bucket").(string)
	object := d.Get("object").(string)

	roleEntities, err := getRoleEntitiesAsStringsFromApi(config, bucket, object)
	if err != nil {
		return handleNotFoundError(err, d, fmt.Sprintf("Storage Object ACL for Bucket %q", d.Get("bucket").(string)))
	}

	err = d.Set("role_entity", roleEntities)
	if err != nil {
		return err
	}

	d.SetId(getObjectAclId(object))
	return nil
}

func resourceStorageObjectAclUpdate(d *schema.ResourceData, meta interface{}) error {
	config := meta.(*Config)

	bucket := d.Get("bucket").(string)
	object := d.Get("object").(string)

	if _, ok := d.GetOk("predefined_acl"); d.HasChange("predefined_acl") && ok {
		res, err := config.clientStorage.Objects.Get(bucket, object).Do()
		if err != nil {
			return fmt.Errorf("Error reading object %s in %s: %v", object, bucket, err)
		}

		_, err = config.clientStorage.Objects.Update(bucket, object, res).PredefinedAcl(d.Get("predefined_acl").(string)).Do()
		if err != nil {
			return fmt.Errorf("Error updating object %s in %s: %v", object, bucket, err)
		}

		return resourceStorageObjectAclRead(d, meta)
	} else {
		sObject, err := config.clientStorage.Objects.Get(bucket, object).Projection("full").Do()
		if err != nil {
			return fmt.Errorf("Error reading object %s in %s: %v", object, bucket, err)
		}

		objectOwner := sObject.Owner.Entity

		o, n := d.GetChange("role_entity")
		create, update, remove, err := getRoleEntityChange(
			convertStringSet(o.(*schema.Set)),
			convertStringSet(n.(*schema.Set)),
			objectOwner)
		if err != nil {
			return fmt.Errorf("Error reading object %s in %s. Invalid schema: %v", object, bucket, err)
		}

		err = performStorageObjectRoleEntityOperations(create, update, remove, config, bucket, object)
		if err != nil {
			return fmt.Errorf("Error updating object %s in %s: %v", object, bucket, err)
		}

		return resourceStorageObjectAclRead(d, meta)
	}
}

func resourceStorageObjectAclDelete(d *schema.ResourceData, meta interface{}) error {
	config := meta.(*Config)

	bucket := d.Get("bucket").(string)
	object := d.Get("object").(string)

	res, err := config.clientStorage.Objects.Get(bucket, object).Do()
	if err != nil {
		return fmt.Errorf("Error reading object %s in %s: %v", object, bucket, err)
	}

	_, err = config.clientStorage.Objects.Update(bucket, object, res).PredefinedAcl("private").Do()
	if err != nil {
		return fmt.Errorf("Error updating object %s in %s: %v", object, bucket, err)
	}

	return nil
}

func getRoleEntitiesAsStringsFromApi(config *Config, bucket string, object string) ([]string, error) {
	var roleEntities []string
	res, err := config.clientStorage.ObjectAccessControls.List(bucket, object).Do()
	if err != nil {
		return nil, err
	}

	for _, roleEntity := range res.Items {
		role := roleEntity.Role
		entity := roleEntity.Entity
		roleEntities = append(roleEntities, fmt.Sprintf("%s:%s", role, entity))
	}

	return roleEntities, nil
}

// Creates 3 lists of changes we need to make to go from one set of entities to another- which entities need to be created, update, and deleted
// Not resource specific
func getRoleEntityChange(old []string, new []string, owner string) (create, update, remove []*RoleEntity, err error) {
	newEntitiesUsed := make(map[string]struct{})
	for _, v := range new {
		res := getValidatedRoleEntityPair(v)
		if _, ok := newEntitiesUsed[res.Entity]; ok {
			return nil, nil, nil, fmt.Errorf("New state has duplicate Entity: %v", res.Entity)
		}

		newEntitiesUsed[res.Entity] = struct{}{}
	}

	oldEntitiesUsed := make(map[string]string)
	for _, v := range old {
		res := getValidatedRoleEntityPair(v)

		// Updating the owner will error out, so let's avoid it.
		if res.Entity == owner {
			continue
		}

		oldEntitiesUsed[res.Entity] = res.Role
	}

	for _, re := range new {
		res := getValidatedRoleEntityPair(re)

		// Updating the owner will error out, so let's never do it.
		if res.Entity == owner {
			continue
		}

		if v, ok := oldEntitiesUsed[res.Entity]; ok {
			if res.Role != v {
				update = append(update, res)
			}

			delete(oldEntitiesUsed, res.Entity)
		} else {
			create = append(create, res)
		}
	}

	for _, v := range old {
		res := getValidatedRoleEntityPair(v)

		if _, ok := oldEntitiesUsed[res.Entity]; ok {
			remove = append(remove, res)
		}
	}

	return create, update, remove, nil
}

// Takes in lists of changes to make to a Storage Object's ACL and makes those changes
func performStorageObjectRoleEntityOperations(create []*RoleEntity, update []*RoleEntity, remove []*RoleEntity, config *Config, bucket string, object string) error {
	for _, v := range create {
		objectAccessControl := &storage.ObjectAccessControl{
			Role:   v.Role,
			Entity: v.Entity,
		}
		_, err := config.clientStorage.ObjectAccessControls.Insert(bucket, object, objectAccessControl).Do()
		if err != nil {
			return fmt.Errorf("Error inserting ACL item %s for object %s in %s: %v", v.Entity, object, bucket, err)
		}
	}

	for _, v := range update {
		objectAccessControl := &storage.ObjectAccessControl{
			Role:   v.Role,
			Entity: v.Entity,
		}
		_, err := config.clientStorage.ObjectAccessControls.Update(bucket, object, v.Entity, objectAccessControl).Do()
		if err != nil {
			return fmt.Errorf("Error updating ACL item %s for object %s in %s: %v", v.Entity, object, bucket, err)
		}
	}

	for _, v := range remove {
		err := config.clientStorage.ObjectAccessControls.Delete(bucket, object, v.Entity).Do()
		if err != nil {
			return fmt.Errorf("Error deleting ACL item %s for object %s in %s: %v", v.Entity, object, bucket, err)
		}
	}

	return nil
}

func validateRoleEntityPair(v interface{}, k string) (ws []string, errors []error) {
	split := strings.Split(v.(string), ":")
	if len(split) != 2 {
		errors = append(errors, fmt.Errorf("Role entity pairs must be formatted as 'ROLE:entity': %s", v))
	}

	return
}

func getValidatedRoleEntityPair(roleEntity string) *RoleEntity {
	split := strings.Split(roleEntity, ":")
	return &RoleEntity{Role: split[0], Entity: split[1]}
}
