package bitbucket

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"path/filepath"
	"regexp"
	"time"

	"github.com/hashicorp/terraform/helper/resource"
	"github.com/hashicorp/terraform/helper/schema"
)

type Plugin struct {
	Key              string `json:"key,omitempty"`
	Enabled          bool   `json:"enabled,omitempty"`
	EnabledByDefault bool   `json:"enabledByDefault,omitempty"`
	Version          string `json:"version,omitempty"`
	Description      string `json:"description,omitempty"`
	Name             string `json:"name,omitempty"`
	UserInstalled    bool   `json:"userInstalled,omitempty"`
	Optional         bool   `json:"optional,omitempty"`
	Vendor           struct {
		Name            string `json:"name,omitempty"`
		MarketplaceLink string `json:"marketplaceLink,omitempty"`
		Link            string `json:"link,omitempty"`
	} `json:"vendor,omitempty"`
}

type PluginLicense struct {
	Valid                        bool     `json:"valid,omitempty"`
	Evaluation                   bool     `json:"evaluation,omitempty"`
	NearlyExpired                bool     `json:"nearlyExpired,omitempty"`
	MaintenanceExpiryDate        jsonTime `json:"maintenanceExpiryDate,omitempty"`
	MaintenanceExpired           bool     `json:"maintenanceExpired,omitempty"`
	LicenseType                  string   `json:"licenseType,omitempty"`
	ExpiryDate                   jsonTime `json:"expiryDate,omitempty"`
	RawLicense                   string   `json:"rawLicense,omitempty"`
	Renewable                    bool     `json:"renewable,omitempty"`
	OrganizationName             string   `json:"organizationName,omitempty"`
	ContactEmail                 string   `json:"contactEmail,omitempty"`
	Enterprise                   bool     `json:"enterprise,omitempty"`
	DataCenter                   bool     `json:"dataCenter,omitempty"`
	Subscription                 bool     `json:"subscription,omitempty"`
	Active                       bool     `json:"active,omitempty"`
	AutoRenewal                  bool     `json:"autoRenewal,omitempty"`
	Upgradable                   bool     `json:"upgradable,omitempty"`
	Crossgradeable               bool     `json:"crossgradeable,omitempty"`
	PurchasePastServerCutoffDate bool     `json:"purchasePastServerCutoffDate,omitempty"`
	SupportEntitlementNumber     string   `json:"supportEntitlementNumber,omitempty"`
}

type PluginMarketplaceVersion struct {
	Version string `json:"name,omitempty"`
	Links   struct {
		Self struct {
			Href string `json:"href,omitempty"`
		} `json:"self,omitempty"`
	} `json:"_links,omitempty"`
	Embedded struct {
		Artifact struct {
			Links struct {
				Self struct {
					Href string `json:"href,omitempty"`
				} `json:"self,omitempty"`
				Binary struct {
					Href string `json:"href,omitempty"`
				} `json:"binary,omitempty"`
			} `json:"_links,omitempty"`
		} `json:"artifact,omitempty"`
	} `json:"_embedded,omitempty"`
}

func (p *PluginMarketplaceVersion) Key() string {
	re, _ := regexp.Compile("/rest/2/addons/([a-zA-Z0-9.-]*)/versions/build/.*")
	values := re.FindStringSubmatch(p.Links.Self.Href)
	if len(values) > 0 {
		return values[1]
	}
	return ""
}

func (p *PluginMarketplaceVersion) Filename() string {
	ext := filepath.Ext(p.Embedded.Artifact.Links.Self.Href)
	return fmt.Sprintf("%s-%s%s", p.Key(), p.Version, ext)
}

func resourcePlugin() *schema.Resource {
	return &schema.Resource{
		Create: resourcePluginCreate,
		Update: resourcePluginUpdate,
		Read:   resourcePluginRead,
		Exists: resourcePluginExists,
		Delete: resourcePluginDelete,
		Importer: &schema.ResourceImporter{
			State: schema.ImportStatePassthrough,
		},

		Schema: map[string]*schema.Schema{
			"key": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},
			"version": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},
			"enabled": {
				Type:     schema.TypeBool,
				Optional: true,
				Default:  true,
			},
			"license": {
				Type:     schema.TypeString,
				Optional: true,
			},
			"enabled_by_default": {
				Type:     schema.TypeBool,
				Computed: true,
			},
			"description": {
				Type:     schema.TypeString,
				Computed: true,
			},
			"name": {
				Type:     schema.TypeString,
				Computed: true,
			},
			"user_installed": {
				Type:     schema.TypeBool,
				Computed: true,
			},
			"optional": {
				Type:     schema.TypeBool,
				Computed: true,
			},
			"vendor": {
				Type:     schema.TypeMap,
				Computed: true,
				Elem: &schema.Resource{
					Schema: map[string]*schema.Schema{
						"name": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"link": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"marketplace_link": {
							Type:     schema.TypeString,
							Computed: true,
						},
					},
				},
			},
			"applied_license": {
				Type:     schema.TypeList,
				Computed: true,
				MaxItems: 1,
				Elem: &schema.Resource{
					Schema: map[string]*schema.Schema{
						"valid": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"evaluation": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"nearly_expired": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"maintenance_expiry_date": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"maintenance_expired": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"license_type": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"expiry_date": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"raw_license": {
							Type:      schema.TypeString,
							Computed:  true,
							Sensitive: true,
						},
						"renewable": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"organization_name": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"contact_email": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"enterprise": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"data_center": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"subscription": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"active": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"auto_renewal": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"upgradable": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"crossgradeable": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"purchase_past_server_cutoff_date": {
							Type:     schema.TypeBool,
							Computed: true,
						},
						"support_entitlement_number": {
							Type:     schema.TypeString,
							Computed: true,
						},
					},
				},
			},
		},
	}
}

func resourcePluginCreate(d *schema.ResourceData, m interface{}) error {
	provider := m.(*BitbucketServerProvider)

	key := d.Get("key").(string)
	version := d.Get("version").(string)

	marketplacePluginVersion, err := readMarketplacePluginVersion(key, version, provider)
	if err != nil {
		return err
	}

	// first get a token for interacting with the UPM
	resp, err := provider.BitbucketClient.Get("/rest/plugins/1.0/?os_authType=basic")
	if err != nil {
		return err
	}
	upmToken := resp.Header.Get("upm-token")

	pluginUri := marketplacePluginVersion.Embedded.Artifact.Links.Binary.Href

	// now we can use the token to install plugin to Bitbucket
	_, err = provider.BitbucketClient.InstallPluginWithUri("/rest/plugins/1.0/?token="+upmToken, pluginUri, d.Get("key").(string))
	if err != nil {
		return err
	}

	d.SetId(key)

	err = resource.Retry(time.Minute*2,
		func() *resource.RetryError {
			exists, err := resourcePluginExists(d, m)
			if exists == false || err != nil {
				return resource.RetryableError(fmt.Errorf("Waiting for plugin installation to finish..."))
			} else {
				return nil
			}
		})
	if err != nil {
		return err
	}

	// need to also run an update loop to set enabled flags and license details
	err = resource.Retry(time.Minute*2,
		func() *resource.RetryError {
			err := resourcePluginUpdate(d, m)
			if err != nil {
				return resource.RetryableError(fmt.Errorf("Waiting for plugin updates to finish..."))
			} else {
				return nil
			}
		})
	if err != nil {
		return err
	}

	return nil
}

func resourcePluginUpdate(d *schema.ResourceData, m interface{}) error {
	client := m.(*BitbucketServerProvider).BitbucketClient

	key := d.Get("key").(string)

	if d.IsNewResource() || d.HasChange("enabled") {
		var plugin Plugin
		req, err := client.Do("GET", fmt.Sprintf("/rest/plugins/1.0/%s-key?os_authType=basic", key), nil, "application/vnd.atl.plugins.plugin+json")
		if err != nil {
			return nil
		}

		body, readErr := ioutil.ReadAll(req.Body)
		if readErr != nil {
			return readErr
		}

		decodeErr := json.Unmarshal(body, &plugin)
		if decodeErr != nil {
			return decodeErr
		}

		plugin.Enabled = d.Get("enabled").(bool)
		bytedata, err := json.Marshal(plugin)
		_, err = client.Do("PUT", fmt.Sprintf("/rest/plugins/1.0/%s-key?os_authType=basic", key), bytes.NewBuffer(bytedata), "application/vnd.atl.plugins.plugin+json")
		if err != nil {
			return err
		}
	}

	if d.IsNewResource() || d.HasChange("license") {
		license := d.Get("license").(string)
		if license != "" {
			licenseJson := map[string]string{"rawLicense": license}
			bytedata, err := json.Marshal(licenseJson)
			if err != nil {
				return err
			}

			req, err := client.Do("PUT", fmt.Sprintf("/rest/plugins/1.0/%s-key/license?os_authType=basic", key), bytes.NewBuffer(bytedata), "application/vnd.atl.plugins+json")

			// ignore 400 errors as this happens if the license is already applied
			if req == nil || (err != nil && req != nil && req.StatusCode != 400) {
				return err
			}
		} else {
			_, err := client.Do("DELETE", fmt.Sprintf("/rest/plugins/1.0/%s-key/license?os_authType=basic", key), nil, "application/vnd.atl.plugins+json")
			if err != nil {
				return err
			}
		}
	}

	return resourcePluginRead(d, m)
}

func resourcePluginRead(d *schema.ResourceData, m interface{}) error {
	id := d.Id()
	if id != "" {
		_ = d.Set("key", id)
	}

	client := m.(*BitbucketServerProvider).BitbucketClient
	req, err := client.Get(fmt.Sprintf("/rest/plugins/1.0/%s-key", d.Get("key").(string)))
	if err != nil {
		return err
	}

	var plugin Plugin

	body, readErr := ioutil.ReadAll(req.Body)
	if readErr != nil {
		return readErr
	}

	decodeErr := json.Unmarshal(body, &plugin)
	if decodeErr != nil {
		return decodeErr
	}

	_ = d.Set("enabled", plugin.Enabled)
	_ = d.Set("enabled_by_default", plugin.EnabledByDefault)
	_ = d.Set("version", plugin.Version)
	_ = d.Set("description", plugin.Description)
	_ = d.Set("name", plugin.Name)
	_ = d.Set("user_installed", plugin.UserInstalled)
	_ = d.Set("optional", plugin.Optional)

	vendor := map[string]string{
		"name":             plugin.Vendor.Name,
		"link":             plugin.Vendor.Link,
		"marketplace_link": plugin.Vendor.MarketplaceLink,
	}
	_ = d.Set("vendor", vendor)

	// Hit the license API to get license details

	req, err = client.Get(fmt.Sprintf("/rest/plugins/1.0/%s-key/license", d.Get("key").(string)))
	if err != nil {
		return err
	}

	var pluginLicense PluginLicense

	body, readErr = ioutil.ReadAll(req.Body)
	if readErr != nil {
		return readErr
	}

	decodeErr = json.Unmarshal(body, &pluginLicense)
	if decodeErr != nil {
		return decodeErr
	}

	license := [1]map[string]interface{}{{
		"valid":                            pluginLicense.Valid,
		"evaluation":                       pluginLicense.Evaluation,
		"nearly_expired":                   pluginLicense.NearlyExpired,
		"maintenance_expiry_date":          pluginLicense.MaintenanceExpiryDate.String(),
		"maintenance_expired":              pluginLicense.MaintenanceExpired,
		"license_type":                     pluginLicense.LicenseType,
		"expiry_date":                      pluginLicense.ExpiryDate.String(),
		"raw_license":                      pluginLicense.RawLicense,
		"renewable":                        pluginLicense.Renewable,
		"organization_name":                pluginLicense.OrganizationName,
		"contact_email":                    pluginLicense.ContactEmail,
		"enterprise":                       pluginLicense.Enterprise,
		"data_center":                      pluginLicense.DataCenter,
		"subscription":                     pluginLicense.Subscription,
		"active":                           pluginLicense.Active,
		"auto_renewal":                     pluginLicense.AutoRenewal,
		"upgradable":                       pluginLicense.Upgradable,
		"crossgradeable":                   pluginLicense.Crossgradeable,
		"purchase_past_server_cutoff_date": pluginLicense.PurchasePastServerCutoffDate,
		"support_entitlement_number":       pluginLicense.SupportEntitlementNumber,
	}}
	_ = d.Set("applied_license", license)

	return nil
}

func resourcePluginExists(d *schema.ResourceData, m interface{}) (bool, error) {
	var key = ""
	id := d.Id()
	if id != "" {
		key = id
	} else {
		key = d.Get("key").(string)
	}

	client := m.(*BitbucketServerProvider).BitbucketClient
	req, err := client.Get(fmt.Sprintf("/rest/plugins/1.0/%s-key",
		key,
	))

	if err != nil {
		return false, fmt.Errorf("failed to get plugin %s from bitbucket: %+v", key, err)
	}

	if req.StatusCode == 200 {
		return true, nil
	} else {
		return false, nil
	}
}

func resourcePluginDelete(d *schema.ResourceData, m interface{}) error {
	client := m.(*BitbucketServerProvider).BitbucketClient
	_, err := client.Delete(fmt.Sprintf("/rest/plugins/1.0/%s-key",
		d.Get("key").(string),
	))

	return err
}

func readMarketplacePluginVersion(key string, version string, provider *BitbucketServerProvider) (*PluginMarketplaceVersion, error) {
	marketplaceRequest, err := provider.MarketplaceClient.Get(fmt.Sprintf("/rest/2/addons/%s/versions/name/%s", key, version))
	if err != nil {
		return nil, err
	}

	var marketplaceVersion PluginMarketplaceVersion

	body, readerr := ioutil.ReadAll(marketplaceRequest.Body)
	if readerr != nil {
		return nil, readerr
	}

	decodeerr := json.Unmarshal(body, &marketplaceVersion)
	if decodeerr != nil {
		return nil, decodeerr
	}

	return &marketplaceVersion, nil
}
