package consul

import (
	"encoding/json"
	"fmt"
	"time"

	consulapi "github.com/hashicorp/consul/api"
	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func resourceConsulACLAuthMethod() *schema.Resource {
	return &schema.Resource{
		Create: resourceConsulACLAuthMethodCreate,
		Read:   resourceConsulACLAuthMethodRead,
		Update: resourceConsulACLAuthMethodUpdate,
		Delete: resourceConsulACLAuthMethodDelete,

		Schema: map[string]*schema.Schema{
			"name": {
				Type:        schema.TypeString,
				Required:    true,
				ForceNew:    true,
				Description: "The name of the ACL auth method.",
			},

			"type": {
				Type:        schema.TypeString,
				Required:    true,
				ForceNew:    true,
				Description: "The type of the ACL auth method.",
			},

			"display_name": {
				Type:        schema.TypeString,
				Optional:    true,
				Description: "An optional name to use instead of the name attribute when displaying information about this auth method.",
			},

			"max_token_ttl": {
				Type:        schema.TypeString,
				Optional:    true,
				Default:     "0s",
				Description: "The maximum life of any token created by this auth method.",
				DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
					o, err := time.ParseDuration(old)
					if err != nil {
						return false
					}
					n, err := time.ParseDuration(new)
					if err != nil {
						return false
					}
					return o.Seconds() == n.Seconds()
				},
			},

			"token_locality": {
				Type:        schema.TypeString,
				Optional:    true,
				Description: "The kind of token that this auth method produces. This can be either 'local' or 'global'.",
			},

			"description": {
				Type:        schema.TypeString,
				Optional:    true,
				Description: "A free form human readable description of the auth method.",
			},

			"config": {
				Type:        schema.TypeMap,
				Optional:    true,
				Description: "The raw configuration for this ACL auth method.",
				Deprecated:  "The config attribute is deprecated, please use config_json instead.",
				Elem: &schema.Schema{
					Type: schema.TypeString,
				},
				ConflictsWith: []string{"config_json"},
			},

			"config_json": {
				Type:          schema.TypeString,
				Optional:      true,
				Description:   "The raw configuration for this ACL auth method.",
				ConflictsWith: []string{"config"},
				DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
					config := d.Get("config").(map[string]interface{})
					return len(config) != 0
				},
			},

			"namespace_rule": {
				Type:     schema.TypeList,
				Optional: true,
				Elem: &schema.Resource{
					Schema: map[string]*schema.Schema{
						"selector": {
							Type:     schema.TypeString,
							Optional: true,
						},
						"bind_namespace": {
							Type:     schema.TypeString,
							Required: true,
						},
					},
				},
			},

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

func resourceConsulACLAuthMethodCreate(d *schema.ResourceData, meta interface{}) error {
	ACL := getClient(meta).ACL()
	wOpts := &consulapi.WriteOptions{}

	authMethod, err := getAuthMethod(d, meta)
	if err != nil {
		return err
	}

	if _, _, err := ACL.AuthMethodCreate(authMethod, wOpts); err != nil {
		return fmt.Errorf("Failed to create auth method '%s': %v", authMethod.Name, err)
	}

	return resourceConsulACLAuthMethodRead(d, meta)
}

func resourceConsulACLAuthMethodRead(d *schema.ResourceData, meta interface{}) error {
	ACL := getClient(meta).ACL()
	namespace := getNamespace(d, meta)
	qOpts := &consulapi.QueryOptions{
		Namespace: namespace,
	}

	name := d.Get("name").(string)
	authMethod, _, err := ACL.AuthMethodRead(name, qOpts)
	if err != nil {
		return fmt.Errorf("Failed to read auth method '%s': %v", name, err)
	}
	if authMethod == nil {
		d.SetId("")
		return nil
	}

	d.SetId(fmt.Sprintf("auth-method-%s", authMethod.Name))

	if err = d.Set("type", authMethod.Type); err != nil {
		return fmt.Errorf("Failed to set 'type': %v", err)
	}

	if err = d.Set("description", authMethod.Description); err != nil {
		return fmt.Errorf("Failed to set 'description': %v", err)
	}

	configJson, err := json.Marshal(authMethod.Config)
	if err != nil {
		return fmt.Errorf("Failed to marshal 'config_json': %v", err)
	}
	if err = d.Set("config_json", string(configJson)); err != nil {
		return fmt.Errorf("Failed to set 'config_json': %v", err)
	}

	if err = d.Set("config", authMethod.Config); err != nil {
		// When a complex configuration is used we can fail to set config as it
		// will not support fields with maps or lists in them. In this case it
		// means that the user used the 'config_json' field, and since we
		// succeeded to set that and 'config' is deprecated, we can just use
		// an empty placeholder value and ignore the error.
		if c := d.Get("config_json").(string); c != "" {
			if err = d.Set("config", map[string]interface{}{}); err != nil {
				return fmt.Errorf("Failed to set 'config': %v", err)
			}
		} else {
			return fmt.Errorf("Failed to set 'config': %v", err)
		}
	}

	if err = d.Set("display_name", authMethod.DisplayName); err != nil {
		return fmt.Errorf("Failed to set 'display_name': %#v", err)
	}

	if err = d.Set("max_token_ttl", authMethod.MaxTokenTTL.String()); err != nil {
		return fmt.Errorf("Failed to set 'max_token_ttl': %#v", err)
	}

	if err = d.Set("token_locality", authMethod.TokenLocality); err != nil {
		return fmt.Errorf("Failed to set 'token_locality': %#v", err)
	}

	rules := make([]interface{}, 0)
	for _, rule := range authMethod.NamespaceRules {
		rules = append(rules, map[string]interface{}{
			"selector":       rule.Selector,
			"bind_namespace": rule.BindNamespace,
		})
	}
	if err = d.Set("namespace_rule", rules); err != nil {
		return fmt.Errorf("Failed to set 'namespace_rule': %v", err)
	}

	return nil
}

func resourceConsulACLAuthMethodUpdate(d *schema.ResourceData, meta interface{}) error {
	ACL := getClient(meta).ACL()
	wOpts := &consulapi.WriteOptions{}

	authMethod, err := getAuthMethod(d, meta)
	if err != nil {
		return err
	}

	if _, _, err := ACL.AuthMethodUpdate(authMethod, wOpts); err != nil {
		return fmt.Errorf("Failed to update the auth method '%s': %v", authMethod.Name, err)
	}

	return resourceConsulACLAuthMethodRead(d, meta)
}

func resourceConsulACLAuthMethodDelete(d *schema.ResourceData, meta interface{}) error {
	ACL := getClient(meta).ACL()
	namespace := getNamespace(d, meta)
	wOpts := &consulapi.WriteOptions{
		Namespace: namespace,
	}

	authMethodName := d.Get("name").(string)
	if _, err := ACL.AuthMethodDelete(authMethodName, wOpts); err != nil {
		return fmt.Errorf("Failed to delete auth method '%s': %v", authMethodName, err)
	}

	d.SetId("")
	return nil
}

func getAuthMethod(d *schema.ResourceData, meta interface{}) (*consulapi.ACLAuthMethod, error) {
	namespace := getNamespace(d, meta)

	var config map[string]interface{}
	if c := d.Get("config_json").(string); c != "" {
		err := json.Unmarshal([]byte(c), &config)
		if err != nil {
			return nil, fmt.Errorf("Failed to read 'config_json': %v", err)
		}
	} else {
		config = d.Get("config").(map[string]interface{})
	}

	authMethod := &consulapi.ACLAuthMethod{
		Name:          d.Get("name").(string),
		DisplayName:   d.Get("display_name").(string),
		TokenLocality: d.Get("token_locality").(string),
		Type:          d.Get("type").(string),
		Description:   d.Get("description").(string),
		Config:        config,
		Namespace:     namespace,
	}

	if mtt, ok := d.GetOk("max_token_ttl"); ok {
		maxTokenTTL, err := time.ParseDuration(mtt.(string))
		if err != nil {
			return nil, err
		}
		authMethod.MaxTokenTTL = maxTokenTTL
	}

	authMethod.NamespaceRules = make([]*consulapi.ACLAuthMethodNamespaceRule, 0)
	for _, r := range d.Get("namespace_rule").([]interface{}) {
		rule := r.(map[string]interface{})
		namespaceRule := &consulapi.ACLAuthMethodNamespaceRule{
			Selector:      rule["selector"].(string),
			BindNamespace: rule["bind_namespace"].(string),
		}
		authMethod.NamespaceRules = append(authMethod.NamespaceRules, namespaceRule)
	}

	return authMethod, nil
}