/*
Copyright 2022 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package civocloud

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

	corev1 "k8s.io/api/core/v1"
)

// 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"`
	Labels           map[string]string    `json:"labels,omitempty"`
	Taints           []corev1.Taint       `json:"taints,omitempty"`
	PublicIPNodePool bool                 `json:"public_ip_node_pool,omitempty"`
	Region           string               `json:"region,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"`
	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"`
	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 {
	Version string `json:"version"`
	Type    string `json:"type"`
	Default bool   `json:"default,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)
	}
}
