package rancher

import (
	"fmt"
	"log"
	"time"

	"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
	"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
	rancherClient "github.com/rancher/go-rancher/v2"
)

var (
	defaultProjectTemplates = map[string]string{
		"mesos":      "",
		"kubernetes": "",
		"windows":    "",
		"swarm":      "",
		"cattle":     "",
	}
)

func resourceRancherEnvironment() *schema.Resource {
	return &schema.Resource{
		Create: resourceRancherEnvironmentCreate,
		Read:   resourceRancherEnvironmentRead,
		Update: resourceRancherEnvironmentUpdate,
		Delete: resourceRancherEnvironmentDelete,
		Importer: &schema.ResourceImporter{
			State: schema.ImportStatePassthrough,
		},

		Schema: map[string]*schema.Schema{
			"name": {
				Type:     schema.TypeString,
				Required: true,
			},
			"orchestration": {
				Type:          schema.TypeString,
				Optional:      true,
				ValidateFunc:  validation.StringInSlice([]string{"cattle", "kubernetes", "mesos", "swarm", "windows"}, true),
				Computed:      true,
				ConflictsWith: []string{"project_template_id"},
				ForceNew:      true,
			},
			"project_template_id": {
				Type:          schema.TypeString,
				Optional:      true,
				Computed:      true,
				ConflictsWith: []string{"orchestration"},
				ForceNew:      true,
			},
			"description": {
				Type:     schema.TypeString,
				Optional: true,
			},
			"member": {
				Type:     schema.TypeSet,
				Optional: true,
				Computed: true,
				Elem: &schema.Resource{
					Schema: map[string]*schema.Schema{
						"external_id_type": {
							Type:     schema.TypeString,
							Required: true,
						},
						"external_id": {
							Type:     schema.TypeString,
							Required: true,
						},
						"role": {
							Type:     schema.TypeString,
							Required: true,
						},
					},
				},
			},
		},
	}
}

func resourceRancherEnvironmentCreate(d *schema.ResourceData, meta interface{}) error {
	log.Printf("[INFO] Creating Environment: %s", d.Id())
	populateProjectTemplateIDs(meta.(*Config))

	client, err := meta.(*Config).GlobalClient()
	if err != nil {
		return err
	}

	name := d.Get("name").(string)
	description := d.Get("description").(string)
	orchestration := d.Get("orchestration").(string)
	projectTemplateID := d.Get("project_template_id").(string)

	projectTemplateID, err = getProjectTemplateID(orchestration, projectTemplateID)
	if err != nil {
		return err
	}

	data := map[string]interface{}{
		"name":              &name,
		"description":       &description,
		"projectTemplateId": &projectTemplateID,
	}

	var newEnv rancherClient.Project
	if err := client.Create("project", data, &newEnv); err != nil {
		return err
	}

	stateConf := &resource.StateChangeConf{
		Pending:    []string{"active", "activating", "registering", "removed", "removing"},
		Target:     []string{"active"},
		Refresh:    EnvironmentStateRefreshFunc(client, newEnv.Id),
		Timeout:    10 * time.Minute,
		Delay:      1 * time.Second,
		MinTimeout: 3 * time.Second,
	}
	_, waitErr := stateConf.WaitForState()
	if waitErr != nil {
		return fmt.Errorf(
			"Error waiting for environment (%s) to be created: %s", newEnv.Id, waitErr)
	}

	d.SetId(newEnv.Id)
	log.Printf("[INFO] Environment ID: %s", d.Id())

	return projectMembersCreateOrUpdate(d, meta)
}

func resourceRancherEnvironmentRead(d *schema.ResourceData, meta interface{}) error {
	log.Printf("[INFO] Refreshing Environment: %s", d.Id())
	client, err := meta.(*Config).GlobalClient()
	if err != nil {
		return err
	}

	env, err := client.Project.ById(d.Id())
	if err != nil {
		return err
	}

	if env == nil {
		log.Printf("[INFO] Environment %s not found", d.Id())
		d.SetId("")
		return nil
	}

	if removed(env.State) {
		log.Printf("[INFO] Environment %s was removed on %v", d.Id(), env.Removed)
		d.SetId("")
		return nil
	}

	log.Printf("[INFO] Environment Name: %s", env.Name)

	d.Set("description", env.Description)
	d.Set("name", env.Name)
	d.Set("orchestration", getActiveOrchestration(env))
	d.Set("project_template_id", env.ProjectTemplateId)

	envClient, err := meta.(*Config).EnvironmentClient(d.Id())
	if err != nil {
		return err
	}

	members, _ := envClient.ProjectMember.List(NewListOpts())
	normalizedMembers := normalizeMembers(members.Data)
	if len(normalizedMembers) > 0 {
		d.Set("member", normalizedMembers)
	}
	return nil
}

func resourceRancherEnvironmentUpdate(d *schema.ResourceData, meta interface{}) error {
	populateProjectTemplateIDs(meta.(*Config))

	client, err := meta.(*Config).GlobalClient()
	if err != nil {
		return err
	}

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

	data := map[string]interface{}{
		"name":        &name,
		"description": &description,
	}

	var newEnv rancherClient.Project
	env, err := client.Project.ById(d.Id())
	if err != nil {
		return err
	}

	if err := client.Update("project", &env.Resource, data, &newEnv); err != nil {
		return err
	}

	return projectMembersCreateOrUpdate(d, meta)
}

func resourceRancherEnvironmentDelete(d *schema.ResourceData, meta interface{}) error {
	log.Printf("[INFO] Deleting Environment: %s", d.Id())
	id := d.Id()
	client, err := meta.(*Config).GlobalClient()
	if err != nil {
		return err
	}

	env, err := client.Project.ById(id)
	if err != nil {
		return err
	}

	if err := client.Project.Delete(env); err != nil {
		return fmt.Errorf("Error deleting Environment: %s", err)
	}

	log.Printf("[DEBUG] Waiting for environment (%s) to be removed", id)

	stateConf := &resource.StateChangeConf{
		Pending:    []string{"active", "removed", "removing"},
		Target:     []string{"removed"},
		Refresh:    EnvironmentStateRefreshFunc(client, id),
		Timeout:    10 * time.Minute,
		Delay:      1 * time.Second,
		MinTimeout: 3 * time.Second,
	}

	_, waitErr := stateConf.WaitForState()
	if waitErr != nil {
		return fmt.Errorf(
			"Error waiting for environment (%s) to be removed: %s", id, waitErr)
	}

	d.SetId("")
	return nil
}

func projectMembersCreateOrUpdate(d *schema.ResourceData, meta interface{}) error {
	client, err := meta.(*Config).GlobalClient()
	if err != nil {
		return err
	}

	env, err := client.Project.ById(d.Id())
	if err != nil {
		return err
	}

	// Create or Update members
	envClient, err := meta.(*Config).EnvironmentClient(d.Id())
	if err != nil {
		return err
	}
	members := d.Get("member").(*schema.Set).List()
	log.Printf("[INFO] Create or Update Project Members: %v", makeProjectMembers(members))
	if members != nil && len(members) > 0 {
		_, err = envClient.Project.ActionSetmembers(env, &rancherClient.SetProjectMembersInput{
			Members: makeProjectMembers(members),
		})
		if err != nil {
			return err
		}
	}
	return resourceRancherEnvironmentRead(d, meta)
}

func getProjectTemplateID(orchestration, templateID string) (string, error) {
	id := templateID
	if templateID == "" && orchestration == "" {
		return "", fmt.Errorf("Need either 'orchestration' or 'project_template_id'")
	}

	if templateID == "" && orchestration != "" {
		ok := false
		id, ok = defaultProjectTemplates[orchestration]
		if !ok {
			return "", fmt.Errorf("Invalid orchestration: %s", orchestration)
		}
	}

	return id, nil
}

func normalizeMembers(in []rancherClient.ProjectMember) (out []interface{}) {
	for _, m := range in {
		mm := map[string]string{
			"external_id_type": m.ExternalIdType,
			"external_id":      m.ExternalId,
			"role":             m.Role,
		}
		out = append(out, mm)
	}
	return
}

func makeProjectMembers(in []interface{}) (out []rancherClient.ProjectMember) {
	for _, m := range in {
		mMap := m.(map[string]interface{})
		mm := rancherClient.ProjectMember{
			ExternalIdType: mMap["external_id_type"].(string),
			ExternalId:     mMap["external_id"].(string),
			Role:           mMap["role"].(string),
		}
		out = append(out, mm)
	}
	return
}

// EnvironmentStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// a Rancher Environment.
func EnvironmentStateRefreshFunc(client *rancherClient.RancherClient, environmentID string) resource.StateRefreshFunc {
	return func() (interface{}, string, error) {
		env, err := client.Project.ById(environmentID)

		if err != nil {
			return nil, "", err
		}

		// Env not returned, or State not set...
		if env == nil || env.State == "" {
			// This makes it so user level API keys can be used instead of just admin
			env = &rancherClient.Project{
				State: "removed",
			}
		}

		return env, env.State, nil
	}
}
