/*
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 hetzner

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/hetznercloud/hcloud-go/hcloud"
	v1 "k8s.io/api/core/v1"
	"k8s.io/klog/v2"
	"k8s.io/kops/dnsprovider/pkg/dnsprovider"
	"k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/pkg/cloudinstances"
	"k8s.io/kops/upup/pkg/fi"
)

const (
	TagKubernetesClusterName         = "kops.k8s.io/cluster"
	TagKubernetesFirewallRole        = "kops.k8s.io/firewall-role"
	TagKubernetesInstanceGroup       = "kops.k8s.io/instance-group"
	TagKubernetesInstanceRole        = "kops.k8s.io/instance-role"
	TagKubernetesInstanceUserData    = "kops.k8s.io/instance-userdata"
	TagKubernetesInstanceNeedsUpdate = "kops.k8s.io/needs-update"
	TagKubernetesVolumeRole          = "kops.k8s.io/volume-role"
	TagKubernetesNodeLabelPrefix     = "node-label.kops.k8s.io."
)

// HetznerCloud exposes all the interfaces required to operate on Hetzner Cloud resources
type HetznerCloud interface {
	fi.Cloud
	ActionClient() hcloud.ActionClient
	SSHKeyClient() hcloud.SSHKeyClient
	NetworkClient() hcloud.NetworkClient
	LoadBalancerClient() hcloud.LoadBalancerClient
	FirewallClient() hcloud.FirewallClient
	ServerClient() hcloud.ServerClient
	VolumeClient() hcloud.VolumeClient
	GetSSHKeys(clusterName string) ([]*hcloud.SSHKey, error)
	GetNetworks(clusterName string) ([]*hcloud.Network, error)
	GetFirewalls(clusterName string) ([]*hcloud.Firewall, error)
	GetLoadBalancers(clusterName string) ([]*hcloud.LoadBalancer, error)
	GetServers(clusterName string) ([]*hcloud.Server, error)
	GetVolumes(clusterName string) ([]*hcloud.Volume, error)
}

// static compile time check to validate HetznerCloud's fi.Cloud Interface.
var _ fi.Cloud = &hetznerCloudImplementation{}

// hetznerCloudImplementation holds the godo client object to interact with Hetzner resources.
type hetznerCloudImplementation struct {
	Client *hcloud.Client

	dns dnsprovider.Interface

	region string
}

// NewHetznerCloud returns a Cloud, using the env var HCLOUD_TOKEN
func NewHetznerCloud(region string) (HetznerCloud, error) {
	accessToken := os.Getenv("HCLOUD_TOKEN")
	if accessToken == "" {
		return nil, errors.New("HCLOUD_TOKEN is required")
	}

	opts := []hcloud.ClientOption{
		hcloud.WithToken(accessToken),
	}
	client := hcloud.NewClient(opts...)

	return &hetznerCloudImplementation{
		Client: client,
		dns:    nil,
		region: region,
	}, nil
}

// ActionClient returns an implementation of hetzner.ActionClient
func (c *hetznerCloudImplementation) ActionClient() hcloud.ActionClient {
	return c.Client.Action
}

// SSHKeyClient returns an implementation of hetzner.SSHKeyClient
func (c *hetznerCloudImplementation) SSHKeyClient() hcloud.SSHKeyClient {
	return c.Client.SSHKey
}

// NetworkClient returns an implementation of hetzner.NetworkClient
func (c *hetznerCloudImplementation) NetworkClient() hcloud.NetworkClient {
	return c.Client.Network
}

// FirewallClient returns an implementation of hetzner.FirewallClient
func (c *hetznerCloudImplementation) FirewallClient() hcloud.FirewallClient {
	return c.Client.Firewall
}

// LoadBalancerClient returns an implementation of hetzner.LoadBalancerClient
func (c *hetznerCloudImplementation) LoadBalancerClient() hcloud.LoadBalancerClient {
	return c.Client.LoadBalancer
}

// ServerClient returns an implementation of hetzner.ServerClient
func (c *hetznerCloudImplementation) ServerClient() hcloud.ServerClient {
	return c.Client.Server
}

// VolumeClient returns an implementation of hetzner.VolumeClient
func (c *hetznerCloudImplementation) VolumeClient() hcloud.VolumeClient {
	return c.Client.Volume
}

func (c *hetznerCloudImplementation) GetSSHKeys(clusterName string) ([]*hcloud.SSHKey, error) {
	client := c.SSHKeyClient()

	labelSelector := TagKubernetesClusterName + "=" + clusterName
	listOptions := hcloud.ListOpts{
		PerPage:       50,
		LabelSelector: labelSelector,
	}
	sshKeyListOpts := hcloud.SSHKeyListOpts{ListOpts: listOptions}

	matches, err := client.AllWithOpts(context.TODO(), sshKeyListOpts)
	if err != nil {
		return nil, fmt.Errorf("failed to get SSH keys matching label selector %q: %w", labelSelector, err)
	}

	return matches, nil
}

func (c *hetznerCloudImplementation) GetNetworks(clusterName string) ([]*hcloud.Network, error) {
	client := c.NetworkClient()

	labelSelector := TagKubernetesClusterName + "=" + clusterName
	listOptions := hcloud.ListOpts{
		PerPage:       50,
		LabelSelector: labelSelector,
	}
	networkListOptions := hcloud.NetworkListOpts{ListOpts: listOptions}

	matches, err := client.AllWithOpts(context.TODO(), networkListOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to get networks matching label selector %q: %w", labelSelector, err)
	}

	return matches, nil
}

func (c *hetznerCloudImplementation) GetFirewalls(clusterName string) ([]*hcloud.Firewall, error) {
	client := c.FirewallClient()

	labelSelector := TagKubernetesClusterName + "=" + clusterName
	listOptions := hcloud.ListOpts{
		PerPage:       50,
		LabelSelector: labelSelector,
	}
	firewallListOptions := hcloud.FirewallListOpts{ListOpts: listOptions}

	matches, err := client.AllWithOpts(context.TODO(), firewallListOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to get firewalls matching label selector %q: %w", labelSelector, err)
	}

	return matches, nil
}

func (c *hetznerCloudImplementation) GetLoadBalancers(clusterName string) ([]*hcloud.LoadBalancer, error) {
	client := c.LoadBalancerClient()

	labelSelector := TagKubernetesClusterName + "=" + clusterName
	listOptions := hcloud.ListOpts{
		PerPage:       50,
		LabelSelector: labelSelector,
	}
	loadBalancerListOptions := hcloud.LoadBalancerListOpts{ListOpts: listOptions}

	matches, err := client.AllWithOpts(context.TODO(), loadBalancerListOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to get load balancers matching label selector %q: %w", labelSelector, err)
	}

	return matches, nil
}

func (c *hetznerCloudImplementation) GetServers(clusterName string) ([]*hcloud.Server, error) {
	client := c.ServerClient()

	labelSelector := TagKubernetesClusterName + "=" + clusterName
	listOptions := hcloud.ListOpts{
		PerPage:       50,
		LabelSelector: labelSelector,
	}
	sortOptions := []string{
		"created:desc",
	}
	serverListOptions := hcloud.ServerListOpts{ListOpts: listOptions, Sort: sortOptions}

	matches, err := client.AllWithOpts(context.TODO(), serverListOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to get servers matching label selector %q: %w", labelSelector, err)
	}

	return matches, nil
}

func (c *hetznerCloudImplementation) GetVolumes(clusterName string) ([]*hcloud.Volume, error) {
	client := c.VolumeClient()

	labelSelector := TagKubernetesClusterName + "=" + clusterName
	listOptions := hcloud.ListOpts{
		PerPage:       50,
		LabelSelector: labelSelector,
	}
	volumeListOptions := hcloud.VolumeListOpts{ListOpts: listOptions}

	matches, err := client.AllWithOpts(context.TODO(), volumeListOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to get volumes matching label selector %q: %w", labelSelector, err)
	}

	return matches, nil
}

func (c *hetznerCloudImplementation) DNS() (dnsprovider.Interface, error) {
	// Hetzner LB has a stable internal IP and can use that instead of creating a record for api.internal.
	return nil, nil
}

func (c *hetznerCloudImplementation) DeleteInstance(instance *cloudinstances.CloudInstance) error {
	serverID, err := strconv.Atoi(instance.ID)
	if err != nil {
		return fmt.Errorf("failed to convert server ID %q to int: %w", instance.ID, err)
	}

	err = deleteServer(c, serverID)

	return err
}

// deleteServer shuts down and deletes the given server
func deleteServer(c *hetznerCloudImplementation, id int) error {
	client := c.ServerClient()
	ctx := context.TODO()

	server := &hcloud.Server{ID: id}
	_, _, err := client.Shutdown(ctx, server)
	if err != nil {
		return fmt.Errorf("failed to stop server %q: %w", strconv.Itoa(id), err)
	}

	for i := 1; i <= 30; i++ {
		server, _, err := client.GetByID(ctx, id)
		if err != nil || server == nil {
			return fmt.Errorf("failed to get info for server %q: %w", strconv.Itoa(id), err)
		}

		if server.Status == hcloud.ServerStatusOff {
			break
		}

		time.Sleep(time.Second * 5)
	}

	_, err = client.Delete(ctx, server)
	if err != nil {
		return fmt.Errorf("failed to delete server %q: %w", strconv.Itoa(id), err)
	}

	return nil
}

func (c *hetznerCloudImplementation) DetachInstance(instance *cloudinstances.CloudInstance) error {
	// Hetzner Cloud API doesn't provide the option of using cloud groups
	return nil
}

// ProviderID returns the kOps API identifier for Hetzner Cloud
func (c *hetznerCloudImplementation) ProviderID() kops.CloudProviderID {
	return kops.CloudProviderHetzner
}

// Region returns the Hetzner Cloud region
func (c *hetznerCloudImplementation) Region() string {
	return c.region
}

func (c *hetznerCloudImplementation) GetCloudGroups(cluster *kops.Cluster, instanceGroups []*kops.InstanceGroup, warnUnmatched bool, nodes []v1.Node) (map[string]*cloudinstances.CloudInstanceGroup, error) {
	nodeMap := cloudinstances.GetNodeMap(nodes, cluster)

	serverGroups, err := findServerGroups(c, cluster.Name)
	if err != nil {
		return nil, fmt.Errorf("failed to find server groups: %v", err)
	}

	cloudInstanceGroups := make(map[string]*cloudinstances.CloudInstanceGroup)
	for name, serverGroup := range serverGroups {
		var instanceGroup *kops.InstanceGroup
		for _, ig := range instanceGroups {
			groupName := fmt.Sprintf("%s-%s", cluster.Name, ig.Name)
			if name == groupName {
				instanceGroup = ig
				break
			}
		}
		if instanceGroup == nil {
			if warnUnmatched {
				klog.Warningf("Server group %q has no corresponding instance group", name)
			}
			continue
		}

		cloudInstanceGroups[instanceGroup.Name], err = buildCloudInstanceGroup(instanceGroup, serverGroup, nodeMap)
		if err != nil {
			return nil, fmt.Errorf("failed to build cloud instance group for instance group %q: %w", instanceGroup.Name, err)
		}
	}

	return cloudInstanceGroups, nil
}

// findServerGroups finds all server groups belonging to the cluster
func findServerGroups(c *hetznerCloudImplementation, clusterName string) (map[string][]*hcloud.Server, error) {
	servers, err := c.GetServers(clusterName)
	if err != nil {
		return nil, fmt.Errorf("failed to list servers: %w", err)
	}

	serverGroups := make(map[string][]*hcloud.Server)
	for _, server := range servers {
		instanceGroupNameLabel, ok := server.Labels[TagKubernetesInstanceGroup]
		if !ok {
			klog.Warningf("failed to find instance group name for server %s(%d)", server.Name, server.ID)
			continue
		}

		instanceGroupName := fmt.Sprintf("%s-%s", clusterName, instanceGroupNameLabel)
		serverGroups[instanceGroupName] = append(serverGroups[instanceGroupName], server)
	}

	return serverGroups, nil
}

func buildCloudInstanceGroup(ig *kops.InstanceGroup, sg []*hcloud.Server, nodeMap map[string]*v1.Node) (*cloudinstances.CloudInstanceGroup, error) {
	cloudInstanceGroup := &cloudinstances.CloudInstanceGroup{
		HumanName:     ig.Name,
		InstanceGroup: ig,
		Raw:           sg,
		MinSize:       int(fi.ValueOf(ig.Spec.MinSize)),
		TargetSize:    int(fi.ValueOf(ig.Spec.MinSize)),
		MaxSize:       int(fi.ValueOf(ig.Spec.MaxSize)),
	}

	for _, server := range sg {
		status := cloudinstances.CloudInstanceStatusUpToDate
		if _, ok := server.Labels[TagKubernetesInstanceNeedsUpdate]; ok {
			status = cloudinstances.CloudInstanceStatusNeedsUpdate
		}

		id := strconv.Itoa(server.ID)
		cloudInstance, err := cloudInstanceGroup.NewCloudInstance(id, status, nodeMap[id])
		if err != nil {
			return nil, fmt.Errorf("failed to create cloud group instance for server %s(%d): %w", server.Name, server.ID, err)
		}

		// Add additional instance info
		cloudInstance.State = cloudinstances.State(server.Status)
		if role, ok := server.Labels[TagKubernetesInstanceRole]; ok {
			cloudInstance.Roles = append(cloudInstance.Roles, role)
		}
		if server.ServerType != nil {
			cloudInstance.MachineType = server.ServerType.Name
		}
		if len(server.PrivateNet) > 0 {
			cloudInstance.PrivateIP = server.PrivateNet[0].IP.String()
		}
	}

	return cloudInstanceGroup, nil
}

func (c *hetznerCloudImplementation) DeleteGroup(g *cloudinstances.CloudInstanceGroup) error {
	for _, cloudInstance := range append(g.NeedUpdate, g.Ready...) {
		serverID, err := strconv.Atoi(cloudInstance.ID)
		if err != nil {
			return fmt.Errorf("failed to convert server ID %q to int: %w", cloudInstance.ID, err)
		}

		err = deleteServer(c, serverID)
		if err != nil {
			return err
		}
	}

	return nil
}

func (c *hetznerCloudImplementation) DeregisterInstance(i *cloudinstances.CloudInstance) error {
	// Hetzner Cloud API doesn't provide the option to drain and remove an instance from the associated load balancers
	return nil
}

// FindVPCInfo is not implemented
func (c *hetznerCloudImplementation) FindVPCInfo(id string) (*fi.VPCInfo, error) {
	// TODO(hakman): Implement me
	return nil, errors.New("hetzner cloud provider does not implement FindVPCInfo at this time")
}

// FindClusterStatus was used before etcd-manager to check the etcd cluster status and prevent unsupported changes.
func (c *hetznerCloudImplementation) FindClusterStatus(cluster *kops.Cluster) (*kops.ClusterStatus, error) {
	return nil, nil
}

func (c *hetznerCloudImplementation) GetApiIngressStatus(cluster *kops.Cluster) ([]fi.ApiIngressStatus, error) {
	lbName := "api." + cluster.Name

	client := c.LoadBalancerClient()
	lb, _, err := client.GetByName(context.TODO(), lbName)
	if err != nil {
		return nil, fmt.Errorf("failed to get info for load balancer %q: %w", lbName, err)
	}
	if lb == nil {
		return nil, nil
	}

	if !lb.PublicNet.Enabled {
		return nil, fmt.Errorf("load balancer %s(%d) is not public", lb.Name, lb.ID)
	}

	ingresses := []fi.ApiIngressStatus{
		{
			IP: lb.PublicNet.IPv4.IP.String(),
		},
	}

	return ingresses, nil
}
