package google

import (
	"fmt"
	"log"

	"time"

	"github.com/hashicorp/errwrap"
	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
	"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
	compute "google.golang.org/api/compute/v0.beta"
)

func resourceComputeSecurityPolicy() *schema.Resource {
	return &schema.Resource{
		Create: resourceComputeSecurityPolicyCreate,
		Read:   resourceComputeSecurityPolicyRead,
		Update: resourceComputeSecurityPolicyUpdate,
		Delete: resourceComputeSecurityPolicyDelete,
		Importer: &schema.ResourceImporter{
			State: resourceSecurityPolicyStateImporter,
		},

		Timeouts: &schema.ResourceTimeout{
			Create: schema.DefaultTimeout(4 * time.Minute),
			Update: schema.DefaultTimeout(4 * time.Minute),
			Delete: schema.DefaultTimeout(4 * time.Minute),
		},

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

			"description": {
				Type:     schema.TypeString,
				Optional: true,
			},

			"project": {
				Type:     schema.TypeString,
				Optional: true,
				Computed: true,
				ForceNew: true,
			},

			"rule": {
				Type:     schema.TypeSet,
				Optional: true,
				Computed: true, // If no rules are set, a default rule is added
				Elem: &schema.Resource{
					Schema: map[string]*schema.Schema{
						"action": {
							Type:         schema.TypeString,
							Required:     true,
							ValidateFunc: validation.StringInSlice([]string{"allow", "deny(403)", "deny(404)", "deny(502)"}, false),
						},

						"priority": {
							Type:     schema.TypeInt,
							Required: true,
						},

						"match": {
							Type:     schema.TypeList,
							Required: true,
							MaxItems: 1,
							Elem: &schema.Resource{
								Schema: map[string]*schema.Schema{
									"config": {
										Type:     schema.TypeList,
										Optional: true,
										MaxItems: 1,
										Elem: &schema.Resource{
											Schema: map[string]*schema.Schema{
												"src_ip_ranges": {
													Type:     schema.TypeSet,
													Required: true,
													MinItems: 1,
													MaxItems: 5,
													Elem:     &schema.Schema{Type: schema.TypeString},
												},
											},
										},
									},

									"versioned_expr": {
										Type:         schema.TypeString,
										Optional:     true,
										ValidateFunc: validation.StringInSlice([]string{"SRC_IPS_V1"}, false),
									},
								},
							},
						},

						"description": {
							Type:     schema.TypeString,
							Optional: true,
						},

						"preview": {
							Type:     schema.TypeBool,
							Optional: true,
						},
					},
				},
			},

			"fingerprint": {
				Type:     schema.TypeString,
				Computed: true,
			},

			"self_link": {
				Type:     schema.TypeString,
				Computed: true,
			},
		},
	}
}

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

	project, err := getProject(d, config)
	if err != nil {
		return err
	}

	sp := d.Get("name").(string)
	securityPolicy := &compute.SecurityPolicy{
		Name:        sp,
		Description: d.Get("description").(string),
	}
	if v, ok := d.GetOk("rule"); ok {
		securityPolicy.Rules = expandSecurityPolicyRules(v.(*schema.Set).List())
	}

	log.Printf("[DEBUG] SecurityPolicy insert request: %#v", securityPolicy)

	op, err := config.clientComputeBeta.SecurityPolicies.Insert(project, securityPolicy).Do()

	if err != nil {
		return errwrap.Wrapf("Error creating SecurityPolicy: {{err}}", err)
	}

	id, err := replaceVars(d, config, "projects/{{project}}/global/securityPolicies/{{name}}")
	if err != nil {
		return fmt.Errorf("Error constructing id: %s", err)
	}
	d.SetId(id)

	err = computeOperationWaitTime(config, op, project, fmt.Sprintf("Creating SecurityPolicy %q", sp), int(d.Timeout(schema.TimeoutCreate).Minutes()))
	if err != nil {
		return err
	}

	return resourceComputeSecurityPolicyRead(d, meta)
}

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

	project, err := getProject(d, config)
	if err != nil {
		return err
	}

	sp := d.Get("name").(string)
	securityPolicy, err := config.clientComputeBeta.SecurityPolicies.Get(project, sp).Do()
	if err != nil {
		return handleNotFoundError(err, d, fmt.Sprintf("SecurityPolicy %q", d.Id()))
	}

	d.Set("name", securityPolicy.Name)
	d.Set("description", securityPolicy.Description)
	if err := d.Set("rule", flattenSecurityPolicyRules(securityPolicy.Rules)); err != nil {
		return err
	}
	d.Set("fingerprint", securityPolicy.Fingerprint)
	d.Set("project", project)
	d.Set("self_link", ConvertSelfLinkToV1(securityPolicy.SelfLink))

	return nil
}

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

	project, err := getProject(d, config)
	if err != nil {
		return err
	}

	sp := d.Get("name").(string)

	if d.HasChange("description") {
		securityPolicy := &compute.SecurityPolicy{
			Description:     d.Get("description").(string),
			Fingerprint:     d.Get("fingerprint").(string),
			ForceSendFields: []string{"Description"},
		}
		op, err := config.clientComputeBeta.SecurityPolicies.Patch(project, sp, securityPolicy).Do()

		if err != nil {
			return errwrap.Wrapf(fmt.Sprintf("Error updating SecurityPolicy %q: {{err}}", sp), err)
		}

		err = computeOperationWaitTime(config, op, project, fmt.Sprintf("Updating SecurityPolicy %q", sp), int(d.Timeout(schema.TimeoutCreate).Minutes()))
		if err != nil {
			return err
		}
	}

	if d.HasChange("rule") {
		o, n := d.GetChange("rule")
		oSet := o.(*schema.Set)
		nSet := n.(*schema.Set)

		oPriorities := map[int64]bool{}
		nPriorities := map[int64]bool{}
		for _, rule := range oSet.List() {
			oPriorities[int64(rule.(map[string]interface{})["priority"].(int))] = true
		}

		for _, rule := range nSet.List() {
			priority := int64(rule.(map[string]interface{})["priority"].(int))
			nPriorities[priority] = true
			if !oPriorities[priority] {
				// If the rule is in new and its priority does not exist in old, then add it.
				op, err := config.clientComputeBeta.SecurityPolicies.AddRule(project, sp, expandSecurityPolicyRule(rule)).Do()

				if err != nil {
					return errwrap.Wrapf(fmt.Sprintf("Error updating SecurityPolicy %q: {{err}}", sp), err)
				}

				err = computeOperationWaitTime(config, op, project, fmt.Sprintf("Updating SecurityPolicy %q", sp), int(d.Timeout(schema.TimeoutCreate).Minutes()))
				if err != nil {
					return err
				}
			} else if !oSet.Contains(rule) {
				// If the rule is in new, and its priority is in old, but its hash is different than the one in old, update it.
				op, err := config.clientComputeBeta.SecurityPolicies.PatchRule(project, sp, expandSecurityPolicyRule(rule)).Priority(priority).Do()

				if err != nil {
					return errwrap.Wrapf(fmt.Sprintf("Error updating SecurityPolicy %q: {{err}}", sp), err)
				}

				err = computeOperationWaitTime(config, op, project, fmt.Sprintf("Updating SecurityPolicy %q", sp), int(d.Timeout(schema.TimeoutCreate).Minutes()))
				if err != nil {
					return err
				}
			}
		}

		for _, rule := range oSet.List() {
			priority := int64(rule.(map[string]interface{})["priority"].(int))
			if !nPriorities[priority] {
				// If the rule's priority is in old but not new, remove it.
				op, err := config.clientComputeBeta.SecurityPolicies.RemoveRule(project, sp).Priority(priority).Do()

				if err != nil {
					return errwrap.Wrapf(fmt.Sprintf("Error updating SecurityPolicy %q: {{err}}", sp), err)
				}

				err = computeOperationWaitTime(config, op, project, fmt.Sprintf("Updating SecurityPolicy %q", sp), int(d.Timeout(schema.TimeoutCreate).Minutes()))
				if err != nil {
					return err
				}
			}
		}
	}

	return resourceComputeSecurityPolicyRead(d, meta)
}

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

	project, err := getProject(d, config)
	if err != nil {
		return err
	}

	// Delete the SecurityPolicy
	op, err := config.clientComputeBeta.SecurityPolicies.Delete(project, d.Get("name").(string)).Do()
	if err != nil {
		return errwrap.Wrapf("Error deleting SecurityPolicy: {{err}}", err)
	}

	err = computeOperationWaitTime(config, op, project, "Deleting SecurityPolicy", int(d.Timeout(schema.TimeoutDelete).Minutes()))
	if err != nil {
		return err
	}

	d.SetId("")
	return nil
}

func expandSecurityPolicyRules(configured []interface{}) []*compute.SecurityPolicyRule {
	rules := make([]*compute.SecurityPolicyRule, 0, len(configured))
	for _, raw := range configured {
		rules = append(rules, expandSecurityPolicyRule(raw))
	}
	return rules
}

func expandSecurityPolicyRule(raw interface{}) *compute.SecurityPolicyRule {
	data := raw.(map[string]interface{})
	return &compute.SecurityPolicyRule{
		Description:     data["description"].(string),
		Priority:        int64(data["priority"].(int)),
		Action:          data["action"].(string),
		Preview:         data["preview"].(bool),
		Match:           expandSecurityPolicyMatch(data["match"].([]interface{})),
		ForceSendFields: []string{"Description", "Preview"},
	}
}

func expandSecurityPolicyMatch(configured []interface{}) *compute.SecurityPolicyRuleMatcher {
	if len(configured) == 0 || configured[0] == nil {
		return nil
	}

	data := configured[0].(map[string]interface{})
	return &compute.SecurityPolicyRuleMatcher{
		VersionedExpr: data["versioned_expr"].(string),
		Config:        expandSecurityPolicyMatchConfig(data["config"].([]interface{})),
	}
}

func expandSecurityPolicyMatchConfig(configured []interface{}) *compute.SecurityPolicyRuleMatcherConfig {
	if len(configured) == 0 || configured[0] == nil {
		return nil
	}

	data := configured[0].(map[string]interface{})
	return &compute.SecurityPolicyRuleMatcherConfig{
		SrcIpRanges: convertStringArr(data["src_ip_ranges"].(*schema.Set).List()),
	}
}

func flattenSecurityPolicyRules(rules []*compute.SecurityPolicyRule) []map[string]interface{} {
	rulesSchema := make([]map[string]interface{}, 0, len(rules))
	for _, rule := range rules {
		data := map[string]interface{}{
			"description": rule.Description,
			"priority":    rule.Priority,
			"action":      rule.Action,
			"preview":     rule.Preview,
			"match":       flattenMatch(rule.Match),
		}

		rulesSchema = append(rulesSchema, data)
	}
	return rulesSchema
}

func flattenMatch(match *compute.SecurityPolicyRuleMatcher) []map[string]interface{} {
	if match == nil {
		return nil
	}

	data := map[string]interface{}{
		"versioned_expr": match.VersionedExpr,
		"config":         flattenMatchConfig(match.Config),
	}

	return []map[string]interface{}{data}
}

func flattenMatchConfig(conf *compute.SecurityPolicyRuleMatcherConfig) []map[string]interface{} {
	if conf == nil {
		return nil
	}

	data := map[string]interface{}{
		"src_ip_ranges": schema.NewSet(schema.HashString, convertStringArrToInterface(conf.SrcIpRanges)),
	}

	return []map[string]interface{}{data}
}

func resourceSecurityPolicyStateImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
	config := meta.(*Config)
	if err := parseImportId([]string{"projects/(?P<project>[^/]+)/global/securityPolicies/(?P<name>[^/]+)", "(?P<project>[^/]+)/(?P<name>[^/]+)", "(?P<name>[^/]+)"}, d, config); err != nil {
		return nil, err
	}

	// Replace import id for the resource id
	id, err := replaceVars(d, config, "projects/{{project}}/global/securityPolicies/{{name}}")
	if err != nil {
		return nil, fmt.Errorf("Error constructing id: %s", err)
	}
	d.SetId(id)

	return []*schema.ResourceData{d}, nil
}
