package client

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/go-resty/resty/v2"
	"github.com/icholy/digest"
)

const (
	defaultNodeProfile = "2C8G"
	defaultRegion      = "us-west-2"
	defaultProvider    = "AWS"
)

// dedicatedClient is a client for managing dedicated clusters.
type dedicatedClient struct {
	tikvProfile    string
	tidbProfile    string
	tiflashProfile string
	host           string
	project        string
	provider       string
	region         string
	dbPasswd       string
	restcli        *resty.Client
}

var _ Client = (*dedicatedClient)(nil)

// NewDedicatedClient creates a new dedicated client.
func NewDedicatedClient(host, project, pubKey, privKey string) *dedicatedClient {
	dbPasswd := generateRandString(16)
	return &dedicatedClient{
		tikvProfile:    defaultNodeProfile,
		tidbProfile:    defaultNodeProfile,
		tiflashProfile: defaultNodeProfile,
		host:           host,
		project:        project,
		provider:       defaultProvider,
		region:         defaultRegion,
		dbPasswd:       dbPasswd,
		restcli: resty.New().SetTransport(&digest.Transport{
			Username: pubKey,
			Password: privKey,
		}),
	}
}

func (c *dedicatedClient) Create(name string) (id string, err error) {
	payload := CreateClusterReq{
		Name:          name,
		ClusterType:   "DEDICATED",
		CloudProvider: c.provider,
		Region:        c.region,
		Config: ClusterConfig{
			RootPassword: c.dbPasswd,
			Port:         4000,
			Components: Components{
				TiDB: &ComponentTiDB{
					NodeSize:     c.tidbProfile,
					NodeQuantity: 1,
				},
				TiKV: &ComponentTiKV{
					NodeSize:       c.tikvProfile,
					StorageSizeGib: 10,
					NodeQuantity:   3,
				},
			},
			IPAccessList: []IPAccess{
				{
					CIDR:        "0.0.0.0/0",
					Description: "Allow Access from Anywhere.",
				},
			},
		},
	}

	var result CreateClusterResp
	_, err = c.doPOST(fmt.Sprintf("%s/api/v1beta/projects/%s/clusters", c.host, c.project), payload, &result)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%d", result.ClusterID), nil
}

func (c *dedicatedClient) Delete(id string) error {
	_, err := c.doDELETE(fmt.Sprintf("%s/api/v1beta/projects/%s/clusters/%s", c.host, c.project, id), nil, nil)
	if err != nil {
		return err
	}
	return nil
}

func (c *dedicatedClient) Get() (*Clusters, error) {
	var result = &Clusters{}
	_, err := c.doGET(fmt.Sprintf("%s/api/v1beta/projects/%s/clusters?page_size=100000", c.host, c.project), nil, result)
	if err != nil {
		return nil, err
	}
	return result, nil
}

type CreateClusterReq struct {
	Name          string        `json:"name"`
	ClusterType   string        `json:"cluster_type"`
	CloudProvider string        `json:"cloud_provider"`
	Region        string        `json:"region"`
	Config        ClusterConfig `json:"config"`
}

type CreateClusterResp struct {
	ClusterID uint64 `json:"id,string"`
	Message   string `json:"message"`
}

type ClusterConfig struct {
	RootPassword string     `json:"root_password"`
	Port         int32      `json:"port"`
	Components   Components `json:"components"`
	IPAccessList []IPAccess `json:"ip_access_list"`
}

type Components struct {
	TiDB    *ComponentTiDB    `json:"tidb,omitempty"`
	TiKV    *ComponentTiKV    `json:"tikv,omitempty"`
	TiFlash *ComponentTiFlash `json:"tiflash,omitempty"`
}

type IPAccess struct {
	CIDR        string `json:"cidr"`
	Description string `json:"description"`
}

type ComponentTiDB struct {
	NodeSize       string `json:"node_size"`
	StorageSizeGib int    `json:"storage_size_gib,omitempty"`
	NodeQuantity   int    `json:"node_quantity"`
}

type ComponentTiKV struct {
	NodeSize       string `json:"node_size"`
	StorageSizeGib int    `json:"storage_size_gib"`
	NodeQuantity   int    `json:"node_quantity"`
}

type ComponentTiFlash struct {
	NodeSize       string `json:"node_size"`
	StorageSizeGib int32  `json:"storage_size_gib"`
	NodeQuantity   int32  `json:"node_quantity"`
}

// doRequest wraps resty request, it's a generic method to spawn a HTTP request
func (c *dedicatedClient) doRequest(method, url string, payload, output interface{}) (*resty.Response, error) {
	request := c.restcli.R()

	// if payload is not nil, we'll put it on body
	if payload != nil {
		if _, err := json.Marshal(payload); err != nil {
			return nil, err
		}
		request.SetBody(payload)
	}

	// execute the request
	resp, err := request.Execute(method, url)
	if err != nil {
		return nil, err
	}

	// if the request return a non-200 response, wrap it with error
	if resp.StatusCode() != http.StatusOK {
		return resp, fmt.Errorf("failed with status %d and resp %s", resp.StatusCode(), resp)
	}

	// if we need to unmarshal the response into a struct, we pass it here, otherwise pass nil in the argument
	if output != nil {
		return resp, json.Unmarshal(resp.Body(), output)
	}

	return resp, nil
}

func (c *dedicatedClient) doGET(url string, payload, output interface{}) (*resty.Response, error) {
	return c.doRequest(resty.MethodGet, url, payload, output)
}

func (c *dedicatedClient) doPOST(url string, payload, output interface{}) (*resty.Response, error) {
	return c.doRequest(resty.MethodPost, url, payload, output)
}

func (c *dedicatedClient) doDELETE(url string, payload, output interface{}) (*resty.Response, error) {
	return c.doRequest(resty.MethodDelete, url, payload, output)
}

// nolint: unused
func (c *dedicatedClient) doPATCH(url string, payload, output interface{}) (*resty.Response, error) {
	return c.doRequest(resty.MethodPatch, url, payload, output)
}
