package civogo

import (
	"bytes"
	"encoding/json"
	"fmt"
	"strings"
	"time"
)

// KubernetesInstance represents a single node/master within a Kubernetes cluster
type KubernetesInstance struct {
	ID              string    `json:"id"`
	Hostname        string    `json:"hostname,omitempty"`
	Size            string    `json:"size,omitempty"`
	Region          string    `json:"region,omitempty"`
	SourceType      string    `json:"source_type,omitempty"`
	SourceID        string    `json:"source_id,omitempty"`
	InitialUser     string    `json:"initial_user,omitempty"`
	InitialPassword string    `json:"initial_password,omitempty"`
	Status          string    `json:"status,omitempty"`
	FirewallID      string    `json:"firewall_id,omitempty"`
	PublicIP        string    `json:"public_ip,omitempty"`
	CPUCores        int       `json:"cpu_cores,omitempty"`
	RAMMegabytes    int       `json:"ram_mb,omitempty"`
	DiskGigabytes   int       `json:"disk_gb,omitempty"`
	Tags            []string  `json:"tags,omitempty"`
	CreatedAt       time.Time `json:"created_at,omitempty"`
	CivoStatsdToken string    `json:"civostatsd_token,omitempty"`
}

// KubernetesPool represents a single pool within a Kubernetes cluster
type KubernetesPool struct {
	ID            string               `json:"id"`
	Count         int                  `json:"count,omitempty"`
	Size          string               `json:"size,omitempty"`
	InstanceNames []string             `json:"instance_names,omitempty"`
	Instances     []KubernetesInstance `json:"instances,omitempty"`
}

// KubernetesInstalledApplication is an application within our marketplace available for
// installation
type KubernetesInstalledApplication struct {
	Application   string                              `json:"application,omitempty"`
	Name          string                              `json:"name,omitempty"`
	Version       string                              `json:"version,omitempty"`
	Dependencies  []string                            `json:"dependencies,omitempty"`
	Maintainer    string                              `json:"maintainer,omitempty"`
	Description   string                              `json:"description,omitempty"`
	PostInstall   string                              `json:"post_install,omitempty"`
	Installed     bool                                `json:"installed,omitempty"`
	URL           string                              `json:"url,omitempty"`
	Category      string                              `json:"category,omitempty"`
	UpdatedAt     time.Time                           `json:"updated_at,omitempty"`
	ImageURL      string                              `json:"image_url,omitempty"`
	Plan          string                              `json:"plan,omitempty"`
	Configuration map[string]ApplicationConfiguration `json:"configuration,omitempty"`
}

// ApplicationConfiguration is a configuration for installed application
type ApplicationConfiguration map[string]string

// KubernetesCluster is a Kubernetes item inside the cluster
type KubernetesCluster struct {
	ID                    string                           `json:"id"`
	Name                  string                           `json:"name,omitempty"`
	GeneratedName         string                           `json:"generated_name,omitempty"`
	Version               string                           `json:"version,omitempty"`
	Status                string                           `json:"status,omitempty"`
	Ready                 bool                             `json:"ready,omitempty"`
	ClusterType           string                           `json:"cluster_type,omitempty"`
	NumTargetNode         int                              `json:"num_target_nodes,omitempty"`
	TargetNodeSize        string                           `json:"target_nodes_size,omitempty"`
	BuiltAt               time.Time                        `json:"built_at,omitempty"`
	KubeConfig            string                           `json:"kubeconfig,omitempty"`
	KubernetesVersion     string                           `json:"kubernetes_version,omitempty"`
	APIEndPoint           string                           `json:"api_endpoint,omitempty"`
	MasterIP              string                           `json:"master_ip,omitempty"`
	DNSEntry              string                           `json:"dns_entry,omitempty"`
	UpgradeAvailableTo    string                           `json:"upgrade_available_to,omitempty"`
	Legacy                bool                             `json:"legacy,omitempty"`
	NetworkID             string                           `json:"network_id,omitempty"`
	NameSpace             string                           `json:"namespace,omitempty"`
	Tags                  []string                         `json:"tags,omitempty"`
	CreatedAt             time.Time                        `json:"created_at,omitempty"`
	Instances             []KubernetesInstance             `json:"instances,omitempty"`
	Pools                 []KubernetesPool                 `json:"pools,omitempty"`
	RequiredPools         []RequiredPools                  `json:"required_pools,omitempty"`
	InstalledApplications []KubernetesInstalledApplication `json:"installed_applications,omitempty"`
	FirewallID            string                           `json:"firewall_id,omitempty"`
	CNIPlugin             string                           `json:"cni_plugin,omitempty"`
	CCMInstalled          string                           `json:"ccm_installed,omitempty"`
}

// RequiredPools returns the required pools for a given Kubernetes cluster
type RequiredPools struct {
	ID    string `json:"id"`
	Size  string `json:"size"`
	Count int    `json:"count"`
}

// PaginatedKubernetesClusters is a Kubernetes k3s cluster
type PaginatedKubernetesClusters struct {
	Page    int                 `json:"page"`
	PerPage int                 `json:"per_page"`
	Pages   int                 `json:"pages"`
	Items   []KubernetesCluster `json:"items"`
}

// KubernetesClusterConfig is used to create a new cluster
type KubernetesClusterConfig struct {
	Name              string                        `json:"name,omitempty"`
	Region            string                        `json:"region,omitempty"`
	ClusterType       string                        `json:"cluster_type,omitempty"`
	NumTargetNodes    int                           `json:"num_target_nodes,omitempty"`
	TargetNodesSize   string                        `json:"target_nodes_size,omitempty"`
	KubernetesVersion string                        `json:"kubernetes_version,omitempty"`
	NodeDestroy       string                        `json:"node_destroy,omitempty"`
	NetworkID         string                        `json:"network_id,omitempty"`
	Tags              string                        `json:"tags,omitempty"`
	Pools             []KubernetesClusterPoolConfig `json:"pools,omitempty"`
	Applications      string                        `json:"applications,omitempty"`
	InstanceFirewall  string                        `json:"instance_firewall,omitempty"`
	FirewallRule      string                        `json:"firewall_rule,omitempty"`
	CNIPlugin         string                        `json:"cni_plugin,omitempty"`
}

// KubernetesClusterPoolConfig is used to create a new cluster pool
type KubernetesClusterPoolConfig struct {
	ID    string `json:"id,omitempty"`
	Count int    `json:"count,omitempty"`
	Size  string `json:"size,omitempty"`
}

// KubernetesPlanConfiguration is a value within a configuration for
// an application's plan
type KubernetesPlanConfiguration struct {
	Value string `json:"value"`
}

// KubernetesMarketplacePlan is a plan for
type KubernetesMarketplacePlan struct {
	Label         string                                 `json:"label"`
	Configuration map[string]KubernetesPlanConfiguration `json:"configuration"`
}

// KubernetesMarketplaceApplication is an application within our marketplace
// available for installation
type KubernetesMarketplaceApplication struct {
	Name         string                      `json:"name"`
	Title        string                      `json:"title,omitempty"`
	Version      string                      `json:"version"`
	Default      bool                        `json:"default,omitempty"`
	Dependencies []string                    `json:"dependencies,omitempty"`
	Maintainer   string                      `json:"maintainer"`
	Description  string                      `json:"description"`
	PostInstall  string                      `json:"post_install"`
	URL          string                      `json:"url"`
	Category     string                      `json:"category"`
	Plans        []KubernetesMarketplacePlan `json:"plans"`
}

// KubernetesVersion represents an available version of k3s to install
type KubernetesVersion struct {
	Label       string `json:"label"`
	Version     string `json:"version"`
	Type        string `json:"type"`
	Default     bool   `json:"default,omitempty"`
	ClusterType string `json:"clusterType,omitempty"`
}

// ListKubernetesClusters returns all cluster of kubernetes in the account
func (c *Client) ListKubernetesClusters() (*PaginatedKubernetesClusters, error) {
	resp, err := c.SendGetRequest("/v2/kubernetes/clusters")
	if err != nil {
		return nil, decodeError(err)
	}

	kubernetes := &PaginatedKubernetesClusters{}
	if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&kubernetes); err != nil {
		return nil, err
	}

	return kubernetes, nil
}

// FindKubernetesCluster finds a Kubernetes cluster by either part of the ID or part of the name
func (c *Client) FindKubernetesCluster(search string) (*KubernetesCluster, error) {
	clusters, err := c.ListKubernetesClusters()
	if err != nil {
		return nil, decodeError(err)
	}

	exactMatch := false
	partialMatchesCount := 0
	result := KubernetesCluster{}

	for _, value := range clusters.Items {
		if strings.EqualFold(value.Name, search) || value.ID == search {
			exactMatch = true
			result = value
		} else if strings.Contains(strings.ToUpper(value.Name), strings.ToUpper(search)) || strings.Contains(value.ID, search) {
			if !exactMatch {
				result = value
				partialMatchesCount++
			}
		}
	}

	if exactMatch || partialMatchesCount == 1 {
		return &result, nil
	} else if partialMatchesCount > 1 {
		err := fmt.Errorf("unable to find %s because there were multiple matches", search)
		return nil, MultipleMatchesError.wrap(err)
	} else {
		err := fmt.Errorf("unable to find %s, zero matches", search)
		return nil, ZeroMatchesError.wrap(err)
	}
}

// NewKubernetesClusters create a new cluster of kubernetes
func (c *Client) NewKubernetesClusters(kc *KubernetesClusterConfig) (*KubernetesCluster, error) {
	kc.Region = c.Region
	body, err := c.SendPostRequest("/v2/kubernetes/clusters", kc)
	if err != nil {
		return nil, decodeError(err)
	}

	kubernetes := &KubernetesCluster{}
	if err := json.NewDecoder(bytes.NewReader(body)).Decode(kubernetes); err != nil {
		return nil, err
	}

	return kubernetes, nil
}

// GetKubernetesCluster returns a single kubernetes cluster by its full ID
func (c *Client) GetKubernetesCluster(id string) (*KubernetesCluster, error) {
	resp, err := c.SendGetRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s", id))
	if err != nil {
		return nil, decodeError(err)
	}

	kubernetes := &KubernetesCluster{}
	if err = json.NewDecoder(bytes.NewReader(resp)).Decode(kubernetes); err != nil {
		return nil, err
	}
	return kubernetes, nil
}

// UpdateKubernetesCluster update a single kubernetes cluster by its full ID
func (c *Client) UpdateKubernetesCluster(id string, i *KubernetesClusterConfig) (*KubernetesCluster, error) {
	i.Region = c.Region
	resp, err := c.SendPutRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s", id), i)
	if err != nil {
		return nil, decodeError(err)
	}

	kubernetes := &KubernetesCluster{}
	if err = json.NewDecoder(bytes.NewReader(resp)).Decode(kubernetes); err != nil {
		return nil, err
	}
	return kubernetes, nil
}

// ListKubernetesMarketplaceApplications returns all application inside marketplace
func (c *Client) ListKubernetesMarketplaceApplications() ([]KubernetesMarketplaceApplication, error) {
	resp, err := c.SendGetRequest("/v2/kubernetes/applications")
	if err != nil {
		return nil, decodeError(err)
	}

	kubernetes := make([]KubernetesMarketplaceApplication, 0)
	if err = json.NewDecoder(bytes.NewReader(resp)).Decode(&kubernetes); err != nil {
		return nil, err
	}

	return kubernetes, nil
}

// DeleteKubernetesCluster deletes a cluster
func (c *Client) DeleteKubernetesCluster(id string) (*SimpleResponse, error) {
	resp, err := c.SendDeleteRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s", id))
	if err != nil {
		return nil, decodeError(err)
	}

	return c.DecodeSimpleResponse(resp)
}

// RecycleKubernetesCluster create a new cluster of kubernetes
func (c *Client) RecycleKubernetesCluster(id string, hostname string) (*SimpleResponse, error) {
	body, err := c.SendPostRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s/recycle", id), map[string]string{
		"hostname": hostname,
		"region":   c.Region,
	})
	if err != nil {
		return nil, decodeError(err)
	}

	return c.DecodeSimpleResponse(body)
}

// ListAvailableKubernetesVersions returns all version of kubernetes available
func (c *Client) ListAvailableKubernetesVersions() ([]KubernetesVersion, error) {
	resp, err := c.SendGetRequest("/v2/kubernetes/versions")
	if err != nil {
		return nil, decodeError(err)
	}

	kubernetes := make([]KubernetesVersion, 0)
	if err = json.NewDecoder(bytes.NewReader(resp)).Decode(&kubernetes); err != nil {
		return nil, err
	}

	return kubernetes, nil
}

// ListKubernetesClusterInstances returns all cluster instances
func (c *Client) ListKubernetesClusterInstances(id string) ([]Instance, error) {
	resp, err := c.SendGetRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s/instances", id))
	if err != nil {
		return nil, decodeError(err)
	}

	instances := make([]Instance, 0)
	if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&instances); err != nil {
		return nil, err
	}

	return instances, nil
}

// FindKubernetesClusterInstance finds a Kubernetes cluster instance by either part of the ID or part of the name
func (c *Client) FindKubernetesClusterInstance(clusterID, search string) (*Instance, error) {
	instances, err := c.ListKubernetesClusterInstances(clusterID)
	if err != nil {
		return nil, decodeError(err)
	}

	exactMatch := false
	partialMatchesCount := 0
	result := Instance{}

	for _, value := range instances {
		if strings.EqualFold(value.Hostname, search) || value.ID == search {
			exactMatch = true
			result = value
		} else if strings.Contains(strings.ToUpper(value.Hostname), strings.ToUpper(search)) || strings.Contains(value.ID, search) {
			if !exactMatch {
				result = value
				partialMatchesCount++
			}
		}
	}

	if exactMatch || partialMatchesCount == 1 {
		return &result, nil
	} else if partialMatchesCount > 1 {
		err := fmt.Errorf("unable to find %s because there were multiple matches", search)
		return nil, MultipleMatchesError.wrap(err)
	} else {
		err := fmt.Errorf("unable to find %s, zero matches", search)
		return nil, ZeroMatchesError.wrap(err)
	}
}
