//go:generate struct-markdown
package common

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/url"
	"os"
	"path/filepath"
	"runtime"

	"github.com/hashicorp/packer/builder/ucloud/version"
	"github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate"
	"github.com/ucloud/ucloud-sdk-go/external"
	"github.com/ucloud/ucloud-sdk-go/private/protocol/http"
	"github.com/ucloud/ucloud-sdk-go/services/uaccount"
	"github.com/ucloud/ucloud-sdk-go/services/ufile"
	"github.com/ucloud/ucloud-sdk-go/services/uhost"
	"github.com/ucloud/ucloud-sdk-go/services/unet"
	"github.com/ucloud/ucloud-sdk-go/services/vpc"
	"github.com/ucloud/ucloud-sdk-go/ucloud"
	"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
	"github.com/ucloud/ucloud-sdk-go/ucloud/log"
)

type AccessConfig struct {
	// This is the UCloud public key. It must be provided unless `profile` is set,
	// but it can also be sourced from the `UCLOUD_PUBLIC_KEY` environment variable.
	PublicKey string `mapstructure:"public_key" required:"true"`
	// This is the UCloud private key. It must be provided unless `profile` is set,
	// but it can also be sourced from the `UCLOUD_PRIVATE_KEY` environment variable.
	PrivateKey string `mapstructure:"private_key" required:"true"`
	// This is the UCloud region. It must be provided, but it can also be sourced from
	// the `UCLOUD_REGION` environment variables.
	Region string `mapstructure:"region" required:"true"`
	// This is the UCloud project id. It must be provided, but it can also be sourced
	// from the `UCLOUD_PROJECT_ID` environment variables.
	ProjectId string `mapstructure:"project_id" required:"true"`
	// This is the base url. (Default: `https://api.ucloud.cn`).
	BaseUrl string `mapstructure:"base_url" required:"false"`
	// This is the UCloud profile name as set in the shared credentials file, it can
	// also be sourced from the `UCLOUD_PROFILE` environment variables.
	Profile string `mapstructure:"profile" required:"false"`
	// This is the path to the shared credentials file, it can also be sourced from
	// the `UCLOUD_SHARED_CREDENTIAL_FILE` environment variables. If this is not set
	// and a profile is specified, `~/.ucloud/credential.json` will be used.
	SharedCredentialsFile string `mapstructure:"shared_credentials_file" required:"false"`

	client *UCloudClient
}

type cloudShellCredential struct {
	Cookie    string `json:"cookie"`
	Profile   string `json:"profile"`
	CSRFToken string `json:"csrf_token"`
}

func (c *AccessConfig) Client() (*UCloudClient, error) {
	if c.client != nil {
		return c.client, nil
	}

	cfg := ucloud.NewConfig()
	cfg.Region = c.Region
	cfg.ProjectId = c.ProjectId
	if c.BaseUrl != "" {
		cfg.BaseUrl = c.BaseUrl
	}
	cfg.LogLevel = log.PanicLevel
	cfg.UserAgent = fmt.Sprintf("Packer-UCloud/%s", version.UcloudPluginVersion.FormattedVersion())
	// set default max retry count
	cfg.MaxRetries = 3

	cred := auth.NewCredential()
	var cloudShellCredHandler ucloud.HttpRequestHandler
	if len(c.Profile) > 0 {
		// load public/private key from shared credential file
		credV, err := external.LoadUCloudCredentialFile(c.SharedCredentialsFile, c.Profile)
		if err != nil {
			return nil, fmt.Errorf("cannot load shared %q credential file, %s", c.Profile, err)
		}
		cred = *credV
	} else if len(c.PublicKey) > 0 && len(c.PrivateKey) > 0 {
		cred.PublicKey = c.PublicKey
		cred.PrivateKey = c.PrivateKey
	} else if v := os.Getenv("CLOUD_SHELL"); v == "true" {
		csCred := make([]cloudShellCredential, 0)
		// load credential from default cloud shell credential path
		if err := loadJSONFile(defaultCloudShellCredPath(), &csCred); err != nil {
			return nil, fmt.Errorf("must set credential about public_key and private_key, %s", err)
		}
		// get default cloud shell credential
		defaultCsCred := &cloudShellCredential{}
		for i := 0; i < len(csCred); i++ {
			if csCred[i].Profile == "default" {
				defaultCsCred = &csCred[i]
				break
			}
		}
		if defaultCsCred == nil || len(defaultCsCred.Cookie) == 0 || len(defaultCsCred.CSRFToken) == 0 {
			return nil, fmt.Errorf("must set credential about public_key and private_key, default credential is null")
		}

		// set cloud shell client handler
		cloudShellCredHandler = func(c *ucloud.Client, req *http.HttpRequest) (*http.HttpRequest, error) {
			if err := req.SetHeader("Cookie", defaultCsCred.Cookie); err != nil {
				return nil, err
			}
			if err := req.SetHeader("Csrf-Token", defaultCsCred.CSRFToken); err != nil {
				return nil, err
			}
			return req, nil
		}
	} else {
		return nil, fmt.Errorf("must set credential about public_key and private_key")
	}

	c.client = &UCloudClient{}
	c.client.UHostConn = uhost.NewClient(&cfg, &cred)
	c.client.UNetConn = unet.NewClient(&cfg, &cred)
	c.client.VPCConn = vpc.NewClient(&cfg, &cred)
	c.client.UAccountConn = uaccount.NewClient(&cfg, &cred)
	c.client.UFileConn = ufile.NewClient(&cfg, &cred)

	if cloudShellCredHandler != nil {
		if err := c.client.UHostConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
			return nil, err
		}
		if err := c.client.UNetConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
			return nil, err
		}
		if err := c.client.VPCConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
			return nil, err
		}
		if err := c.client.UAccountConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
			return nil, err
		}
		if err := c.client.UFileConn.AddHttpRequestHandler(cloudShellCredHandler); err != nil {
			return nil, err
		}
	}

	return c.client, nil
}

func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
	var errs []error
	if err := c.Config(); err != nil {
		errs = append(errs, err)
	}

	if c.Region == "" {
		c.Region = os.Getenv("UCLOUD_REGION")
	}

	if c.Region == "" {
		errs = append(errs, fmt.Errorf("%q must be set", "region"))
	}

	if c.ProjectId == "" {
		c.ProjectId = os.Getenv("UCLOUD_PROJECT_ID")
	}

	if c.ProjectId == "" {
		errs = append(errs, fmt.Errorf("%q must be set", "projectId"))
	}

	if c.BaseUrl != "" {
		if _, err := url.Parse(c.BaseUrl); err != nil {
			errs = append(errs, fmt.Errorf("%q is invalid, should be an valid ucloud base_url, got %q, parse error: %s", "base_url", c.BaseUrl, err))
		}
	}

	if c.Profile == "" {
		c.Profile = os.Getenv("UCLOUD_PROFILE")
	}

	if c.SharedCredentialsFile == "" {
		c.SharedCredentialsFile = os.Getenv("UCLOUD_SHARED_CREDENTIAL_FILE")
	}

	if len(errs) > 0 {
		return errs
	}

	return nil
}

func (c *AccessConfig) Config() error {
	if c.PublicKey == "" {
		c.PublicKey = os.Getenv("UCLOUD_PUBLIC_KEY")
	}
	if c.PrivateKey == "" {
		c.PrivateKey = os.Getenv("UCLOUD_PRIVATE_KEY")
	}

	if c.Profile == "" {
		c.Profile = os.Getenv("UCLOUD_PROFILE")
	}

	if c.SharedCredentialsFile == "" {
		c.SharedCredentialsFile = os.Getenv("UCLOUD_SHARED_CREDENTIAL_FILE")
	}

	if (c.PublicKey == "" || c.PrivateKey == "") && c.Profile == "" && os.Getenv("CLOUD_SHELL") != "true" {
		return fmt.Errorf("%q, %q must be set in template file or environment variables", "public_key", "private_key")
	}
	return nil

}

func (c *AccessConfig) ValidateProjectId(projectId string) error {
	supportedProjectIds, err := c.getSupportedProjectIds()
	if err != nil {
		return err
	}

	for _, supportedProjectId := range supportedProjectIds {
		if projectId == supportedProjectId {
			return nil
		}
	}

	return fmt.Errorf("%q is invalid, should be an valid ucloud project_id, got %q", "project_id", projectId)
}

func (c *AccessConfig) ValidateRegion(region string) error {
	supportedRegions, err := c.getSupportedRegions()
	if err != nil {
		return err
	}

	for _, supportedRegion := range supportedRegions {
		if region == supportedRegion {
			return nil
		}
	}

	return fmt.Errorf("%q is invalid, should be an valid ucloud region, got %q", "region", region)
}

func (c *AccessConfig) ValidateZone(region, zone string) error {
	supportedZones, err := c.getSupportedZones(region)
	if err != nil {
		return err
	}

	for _, supportedZone := range supportedZones {
		if zone == supportedZone {
			return nil
		}
	}

	return fmt.Errorf("%q is invalid, should be an valid ucloud zone, got %q", "availability_zone", zone)
}

func (c *AccessConfig) getSupportedProjectIds() ([]string, error) {
	client, err := c.Client()
	if err != nil {
		return nil, err
	}
	conn := client.UAccountConn
	req := conn.NewGetProjectListRequest()
	resp, err := conn.GetProjectList(req)
	if err != nil {
		return nil, err
	}

	validProjectIds := make([]string, len(resp.ProjectSet))
	for _, val := range resp.ProjectSet {
		if !IsStringIn(val.ProjectId, validProjectIds) {
			validProjectIds = append(validProjectIds, val.ProjectId)
		}
	}

	return validProjectIds, nil
}

func (c *AccessConfig) getSupportedRegions() ([]string, error) {
	client, err := c.Client()
	if err != nil {
		return nil, err
	}

	conn := client.UAccountConn
	req := conn.NewGetRegionRequest()
	resp, err := conn.GetRegion(req)
	if err != nil {
		return nil, err
	}

	validRegions := make([]string, len(resp.Regions))
	for _, val := range resp.Regions {
		if !IsStringIn(val.Region, validRegions) {
			validRegions = append(validRegions, val.Region)
		}
	}

	return validRegions, nil
}

func (c *AccessConfig) getSupportedZones(region string) ([]string, error) {
	client, err := c.Client()
	if err != nil {
		return nil, err
	}

	conn := client.UAccountConn
	req := conn.NewGetRegionRequest()
	resp, err := conn.GetRegion(req)
	if err != nil {
		return nil, err
	}

	validZones := make([]string, len(resp.Regions))
	for _, val := range resp.Regions {
		if val.Region == region && !IsStringIn(val.Zone, validZones) {
			validZones = append(validZones, val.Zone)
		}

	}

	return validZones, nil
}

func defaultCloudShellCredPath() string {
	return filepath.Join(userHomeDir(), ".ucloud", "credential.json")
}

func loadJSONFile(path string, p interface{}) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}

	c, err := ioutil.ReadAll(f)
	if err != nil {
		return err
	}

	err = json.Unmarshal(c, p)
	if err != nil {
		return err
	}
	return nil
}

func userHomeDir() string {
	if runtime.GOOS == "windows" {
		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
		if home == "" {
			home = os.Getenv("USERPROFILE")
		}
		return home
	}
	return os.Getenv("HOME")
}
