/*
Copyright 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 ibmcloud

import (
	"context"
	"fmt"
	"strings"

	"github.com/IBM-Cloud/cloud-provider-ibm/pkg/client" 
	"github.com/awslabs/operatorpkg/status"
	"github.com/samber/lo"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"sigs.k8s.io/karpenter/pkg/apis/v1alpha1"
	v1 "sigs.k8s.io/karpenter/pkg/apis/v1"
	"sigs.k8s.io/karpenter/pkg/cloudprovider"
	"sigs.k8s.io/karpenter/pkg/scheduling"
)

func NewCloudProvider(ctx context.Context, kubeClient client.Client, ibmClient *client.IBMClient, instanceTypes []*cloudprovider.InstanceType) *CloudProvider {
	return &CloudProvider{
		kubeClient:    kubeClient,
		ibmClient:     ibmClient,
		instanceTypes: instanceTypes,
	}
}

type CloudProvider struct {
	kubeClient    client.Client
	ibmClient     *client.IBMClient // IBM Cloud client
	instanceTypes []*cloudprovider.InstanceType
}

func (c CloudProvider) Create(ctx context.Context, nodeClaim *v1.NodeClaim) (*v1.NodeClaim, error) {
	// Create the Node using IBM Cloud APIs
	node, err := c.toNode(nodeClaim)
	if err != nil {
		return nil, fmt.Errorf("translating nodeclaim to node, %w", err)
	}
	if err := c.kubeClient.Create(ctx, node); err != nil {
		return nil, fmt.Errorf("creating node, %w", err)
	}
	return c.toNodeClaim(node)
}

func (c CloudProvider) DisruptionReasons() []v1.DisruptionReason {
	return []v1.DisruptionReason{v1alpha1.DisruptionReasonExampleReason} // adjust if IBM Cloud has its specific reasons
}

func (c CloudProvider) Delete(ctx context.Context, nodeClaim *v1.NodeClaim) error {
	providerID := nodeClaim.Status.ProviderID
	if err := c.ibmClient.DeleteInstance(providerID); err != nil {
		if errors.IsNotFound(err) {
			return fmt.Errorf("deleting node, %w", cloudprovider.NewNodeClaimNotFoundError(err))
		}
		return fmt.Errorf("deleting node, %w", err)
	}
	return nil
}

func (c CloudProvider) Get(ctx context.Context, providerID string) (*v1.NodeClaim, error) {
	nodeID := strings.Replace(providerID, "ibmcloud://", "", -1) // adjust the provider ID prefix for IBM Cloud
	node, err := c.ibmClient.GetNode(nodeID)
	if err != nil {
		if errors.IsNotFound(err) {
			return nil, fmt.Errorf("finding node, %w", cloudprovider.NewNodeClaimNotFoundError(err))
		}
		return nil, fmt.Errorf("finding node, %w", err)
	}
	if node.DeletionTimestamp != nil {
		return nil, cloudprovider.NewNodeClaimNotFoundError(fmt.Errorf("nodeclaim not found"))
	}
	return c.toNodeClaim(node)
}

func (c CloudProvider) List(ctx context.Context) ([]*v1.NodeClaim, error) {
	nodeList, err := c.ibmClient.ListNodes()
	if err != nil {
		return nil, fmt.Errorf("listing nodes, %w", err)
	}
	var nodeClaims []*v1.NodeClaim
	for _, node := range nodeList {
		if !strings.HasPrefix(node.ProviderID, "ibmcloud://") {
			continue
		}
		nc, err := c.toNodeClaim(node)
		if err != nil {
			return nil, fmt.Errorf("converting nodeclaim, %w", err)
		}
		nodeClaims = append(nodeClaims, nc)
	}
	return nodeClaims, nil
}

func (c CloudProvider) GetInstanceTypes(ctx context.Context, nodePool *v1.NodePool) ([]*cloudprovider.InstanceType, error) {
	return c.instanceTypes, nil
}

func (c CloudProvider) IsDrifted(ctx context.Context, nodeClaim *v1.NodeClaim) (cloudprovider.DriftReason, error) {
	// implement IBM Cloud specific drift detection
	return "", nil
}

func (c CloudProvider) Name() string {
	return "ibmcloud"
}

func (c CloudProvider) GetSupportedNodeClasses() []status.Object {
	return []status.Object{&v1alpha1.IBMNodeClass{}}
}

func (c CloudProvider) getInstanceType(instanceTypeName string) (*cloudprovider.InstanceType, error) {
	it, found := lo.Find(c.instanceTypes, func(it *cloudprovider.InstanceType) bool {
		return it.Name == instanceTypeName
	})
	if !found {
		return nil, fmt.Errorf("unable to find instance type %q", instanceTypeName)
	}
	return it, nil
}

func (c CloudProvider) toNode(nodeClaim *v1.NodeClaim) (*corev1.Node, error) {
	// generate a new node name
	nodeName := fmt.Sprintf("ibmcloud-node-%s", nodeClaim.Name)

	// get the instance type information
	instanceType, err := c.getInstanceType(nodeClaim.Spec.InstanceType)
	if err != nil {
		return nil, fmt.Errorf("instance type %s not found", nodeClaim.Spec.InstanceType)
	}

	return &corev1.Node{
		ObjectMeta: metav1.ObjectMeta{
			Name:        nodeName,
			Labels:      addIBMCloudInstanceLabels(nodeClaim.Labels, instanceType, nodeClaim),
			Annotations: addIBMCloudAnnotations(nodeClaim.Annotations),
		},
		Spec: corev1.NodeSpec{
			ProviderID: "ibmcloud://" + nodeName,
			Taints:     nodeClaim.Spec.Taints,
		},
		Status: corev1.NodeStatus{
			Capacity:    instanceType.Capacity,
			Allocatable: instanceType.Allocatable(),
		},
	}, nil
}

func addIBMCloudInstanceLabels(labels map[string]string, instanceType *cloudprovider.InstanceType, nodeClaim *v1.NodeClaim) map[string]string {
	ret := make(map[string]string, len(labels))
	for k, v := range labels {
		ret[k] = v
	}
	ret[corev1.LabelInstanceTypeStable] = instanceType.Name
	return ret
}

func addIBMCloudAnnotations(annotations map[string]string) map[string]string {
	ret := make(map[string]string, len(annotations)+1)
	for k, v := range annotations {
		ret[k] = v
	}
	ret["ibmcloud-specific-annotation"] = "true"
	return ret
}

func (c CloudProvider) toNodeClaim(node *corev1.Node) (*v1.NodeClaim, error) {
	return &v1.NodeClaim{
		ObjectMeta: metav1.ObjectMeta{
			Name:        node.Name,
			Labels:      node.Labels,
			Annotations: addIBMCloudAnnotations(node.Annotations),
		},
		Spec: v1.NodeClaimSpec{},
		Status: v1.NodeClaimStatus{
			NodeName:    node.Name,
			ProviderID:  node.Spec.ProviderID,
			Capacity:    node.Status.Capacity,
			Allocatable: node.Status.Allocatable,
		},
	}, nil
}