/*
Copyright 2024.

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 providers

import (
	"context"
	"fmt"
	"strings"

	"github.com/alibabacloud-go/cs-20151215/v7/client"
	openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils"
	"github.com/alibabacloud-go/tea/dara"
	"github.com/alibabacloud-go/tea/tea"
	"github.com/aliyun/credentials-go/credentials"

	o11yv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
)

// Cluster detail map keys used by clusterDetailBodyToMap and related functions.
const (
	clusterDetailKeyCurrentVersion = "current_version"
	clusterDetailKeyState          = "state"
	clusterDetailKeyName           = "name"
	clusterDetailKeyClusterID      = "cluster_id"
)

// ACKCloudProvider implements CloudProviderInterface for Alibaba Cloud ACK.
type ACKCloudProvider struct {
	client *client.Client
	region string
}

func NewACKCloudProvider(region string) (*ACKCloudProvider, error) {
	cred, err := credentials.NewCredential(nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create Aliyun credential: %w", err)
	}

	r := region
	endpoint := fmt.Sprintf("cs.%s.aliyuncs.com", region)
	cfg := &openapiutil.Config{
		Credential:         cred,
		RegionId:           &r,
		Endpoint:           &endpoint,
		Protocol:           tea.String("HTTPS"),
		SignatureAlgorithm: tea.String("v2"),
	}
	cli, err := client.NewClient(cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to create ACK client: %w", err)
	}
	return &ACKCloudProvider{client: cli, region: region}, nil
}

func (a *ACKCloudProvider) UpgradeControlPlane(ctx context.Context, clusterName, targetVersion string) error {
	req := &client.UpgradeClusterRequest{
		NextVersion: tea.String(targetVersion),
	}
	_, err := a.client.UpgradeClusterWithContext(ctx, tea.String(clusterName), req, nil, &dara.RuntimeOptions{})
	if err != nil {
		return fmt.Errorf("failed to upgrade ACK control plane: %w", err)
	}
	return nil
}

func (a *ACKCloudProvider) UpgradeClusterAddons(ctx context.Context, clusterName string, addons []o11yv1.AddonUpgrade) error {
	if len(addons) == 0 {
		return nil
	}
	body := make([]*client.UpgradeClusterAddonsRequestBody, 0, len(addons))
	for _, ad := range addons {
		reqBody := &client.UpgradeClusterAddonsRequestBody{
			ComponentName: tea.String(ad.Name),
			NextVersion:   tea.String(ad.Version),
		}
		if ad.ConfigurationValues != "" {
			reqBody.Config = tea.String(ad.ConfigurationValues)
		}
		if ad.ResolveConflicts != "" {
			policy := strings.ToLower(ad.ResolveConflicts)
			if policy == "overwrite" || policy == "canary" {
				reqBody.Policy = tea.String(policy)
			}
		}
		body = append(body, reqBody)
	}
	req := &client.UpgradeClusterAddonsRequest{Body: body}
	_, err := a.client.UpgradeClusterAddonsWithContext(ctx, tea.String(clusterName), req, nil, &dara.RuntimeOptions{})
	if err != nil {
		return fmt.Errorf("failed to upgrade ACK cluster addons: %w", err)
	}
	return nil
}

// ListClusterAddons returns all installed addons in the ACK cluster.
func (a *ACKCloudProvider) ListClusterAddons(ctx context.Context, clusterName string) ([]AddonStatusInfo, error) {
	resp, err := a.client.ListClusterAddonInstancesWithContext(ctx, tea.String(clusterName), nil, &dara.RuntimeOptions{})
	if err != nil {
		return nil, fmt.Errorf("list ACK addons: %w", err)
	}
	if resp.Body == nil || resp.Body.Addons == nil {
		return []AddonStatusInfo{}, nil
	}
	out := make([]AddonStatusInfo, 0, len(resp.Body.Addons))
	for _, ad := range resp.Body.Addons {
		if ad == nil || ad.Name == nil {
			continue
		}
		version := ""
		if ad.Version != nil {
			version = *ad.Version
		}
		status := mapACKAddonStateToStatus(*ad.State)
		out = append(out, AddonStatusInfo{Name: *ad.Name, Version: version, Status: status})
	}
	return out, nil
}

func (a *ACKCloudProvider) GetClusterAddonsStatus(ctx context.Context, clusterName string, addons []o11yv1.AddonUpgrade) ([]AddonStatusInfo, error) {
	out := make([]AddonStatusInfo, 0, len(addons))
	resp, err := a.client.ListClusterAddonInstancesWithContext(ctx, tea.String(clusterName), nil, &dara.RuntimeOptions{})
	if err != nil {
		return nil, fmt.Errorf("list ACK addons: %w", err)
	}
	if resp.Body == nil || resp.Body.Addons == nil {
		return nil, fmt.Errorf("empty ACK addons response")
	}
	for _, ad := range addons {
		info := AddonStatusInfo{Name: ad.Name}
		for _, respAd := range resp.Body.Addons {
			if respAd == nil || respAd.Name == nil {
				continue
			}
			if *respAd.Name == ad.Name {
				if respAd.Version != nil {
					info.Version = *respAd.Version
				}
				info.Status = mapACKAddonStateToStatus(*respAd.State)
				break
			}
		}
		out = append(out, info)
	}
	return out, nil
}

// mapACKAddonStateToStatus maps ACK addon state to processor-compatible status.
// ACK states: initial, active, unhealthy, upgrading, updating, deleting, deleted
func mapACKAddonStateToStatus(state string) string {
	switch strings.ToLower(state) {
	case "active":
		return "ACTIVE"
	case "upgrading", "updating", "initial":
		return "UPDATING"
	case "unhealthy":
		return "UPDATE_FAILED"
	case "deleting", "deleted":
		return "DELETE_FAILED"
	default:
		return state
	}
}

func (a *ACKCloudProvider) GetNodeGroupsByLabels(ctx context.Context, clusterName string, labelSelector map[string]string) ([]NodeGroup, error) {
	resp, err := a.client.DescribeClusterNodePoolsWithContext(ctx, tea.String(clusterName), &client.DescribeClusterNodePoolsRequest{}, nil, &dara.RuntimeOptions{})
	if err != nil {
		return nil, fmt.Errorf("failed to list ACK nodepools: %w", err)
	}

	body := resp.Body
	if body == nil || body.Nodepools == nil {
		return nil, nil
	}

	var out []NodeGroup
	for _, np := range body.Nodepools {
		ng := a.sdkNodepoolToNodeGroup(np)
		matches := true
		for k, v := range labelSelector {
			if ng.Labels[k] != v {
				matches = false
				break
			}
		}
		if matches {
			out = append(out, ng)
		}
	}
	return out, nil
}

func (a *ACKCloudProvider) GetNodeGroupByName(ctx context.Context, clusterName string, nodeGroupName string) (*NodeGroup, error) {
	resp, err := a.client.DescribeClusterNodePoolDetailWithContext(ctx, tea.String(clusterName), tea.String(nodeGroupName), nil, &dara.RuntimeOptions{})
	if err != nil {
		return nil, fmt.Errorf("failed to describe ACK nodepool %q: %w", nodeGroupName, err)
	}

	body := resp.Body
	if body == nil {
		return nil, fmt.Errorf("empty ACK nodepool detail response")
	}

	ng := a.sdkNodePoolDetailToNodeGroup(body)
	if ng.ID == "" && ng.Name == "" {
		return nil, fmt.Errorf("unexpected ACK nodepool response shape")
	}
	return &ng, nil
}

func (a *ACKCloudProvider) CreateNodeGroup(ctx context.Context, clusterName string, spec NodeGroup) (string, error) {
	return "", fmt.Errorf("not implemented")
}

func (a *ACKCloudProvider) UpdateNodeGroupMinSize(ctx context.Context, clusterName, nodeGroupID string, minSize int) error {
	return fmt.Errorf("not implemented")
}

func (a *ACKCloudProvider) UpgradeNodeGroup(ctx context.Context, clusterName, nodeGroupID, targetVersion string, opts *o11yv1.NodeGroupUpgradeOptions) (string, error) {
	req := &client.UpgradeClusterNodepoolRequest{
		KubernetesVersion: tea.String(targetVersion),
	}
	if opts != nil && opts.TargetRuntimeVersion != "" && opts.RuntimeType != "" {
		req.RuntimeVersion = tea.String(opts.TargetRuntimeVersion)
		req.RuntimeType = tea.String(opts.RuntimeType)
	}
	_, err := a.client.UpgradeClusterNodepoolWithContext(ctx, tea.String(clusterName), tea.String(nodeGroupID), req, nil, &dara.RuntimeOptions{})
	if err != nil {
		return "", fmt.Errorf("failed to upgrade ACK nodepool %q: %w", nodeGroupID, err)
	}
	return "", nil
}

func (_ *ACKCloudProvider) GetNodeGroupUpgradeOperationStatus(_ context.Context, _, _, _ string) (bool, error) {
	return true, nil
}

func (a *ACKCloudProvider) DeleteNodeGroup(ctx context.Context, clusterName, nodeGroupID string) error {
	return fmt.Errorf("not implemented")
}

func (a *ACKCloudProvider) DescribeCluster(ctx context.Context, clusterName string) (any, error) {
	resp, err := a.client.DescribeClusterDetailWithContext(ctx, tea.String(clusterName), nil, &dara.RuntimeOptions{})
	if err != nil {
		return nil, fmt.Errorf("failed to describe ACK cluster %q: %w", clusterName, err)
	}

	body := resp.Body
	if body == nil {
		return nil, fmt.Errorf("empty ACK cluster detail response")
	}

	return clusterDetailBodyToMap(body), nil
}

func (a *ACKCloudProvider) GetNodeGroupStatus(ctx context.Context, clusterName, nodeGroupID string) (NodeGroupStatus, error) {
	ng, err := a.GetNodeGroupByName(ctx, clusterName, nodeGroupID)
	if err != nil {
		return "", err
	}
	return ng.Status, nil
}

func (a *ACKCloudProvider) GetNodeGroupLabelSelector(_ context.Context, _, nodeGroupID string) (map[string]string, error) {
	return map[string]string{
		"node.alibabacloud.com/nodepool-id": nodeGroupID,
	}, nil
}

func (a *ACKCloudProvider) GetControlPlaneVersion(ctx context.Context, clusterName string) (string, error) {
	anyCluster, err := a.DescribeCluster(ctx, clusterName)
	if err != nil {
		return "", err
	}
	mp, ok := anyCluster.(map[string]any)
	if !ok {
		return "", fmt.Errorf("unexpected ACK cluster response type")
	}
	if v := strFromAny(mp[clusterDetailKeyCurrentVersion]); v != "" {
		return v, nil
	}
	return "", fmt.Errorf("failed to find control plane version in ACK cluster response")
}

// GetControlPlaneStatus returns the ACK cluster control plane status (ACTIVE or UPDATING).
func (a *ACKCloudProvider) GetControlPlaneStatus(ctx context.Context, clusterName string) (ControlPlaneStatus, error) {
	anyCluster, err := a.DescribeCluster(ctx, clusterName)
	if err != nil {
		return "", err
	}
	mp, ok := anyCluster.(map[string]any)
	if !ok {
		return "", fmt.Errorf("unexpected ACK cluster response type")
	}
	raw := strings.ToLower(strFromAny(mp[clusterDetailKeyState]))
	if raw == "updating" {
		return ControlPlaneStatusUpdating, nil
	}
	if raw == "running" {
		return ControlPlaneStatusActive, nil
	}
	return ControlPlaneStatusOther, nil
}

// GetInstanceTypeByTemplateVersion is not applicable to ACK (no EC2-style launch templates). Returns empty string, nil.
func (_ *ACKCloudProvider) GetInstanceTypeByTemplateVersion(_ context.Context, _, _, _ string) (string, error) {
	return "", nil
}

func clusterDetailBodyToMap(body *client.DescribeClusterDetailResponseBody) map[string]any {
	m := make(map[string]any)
	if body == nil {
		return m
	}
	if body.CurrentVersion != nil {
		m[clusterDetailKeyCurrentVersion] = *body.CurrentVersion
	}
	if body.State != nil {
		m[clusterDetailKeyState] = *body.State
	}
	if body.Name != nil {
		m[clusterDetailKeyName] = *body.Name
	}
	if body.ClusterId != nil {
		m[clusterDetailKeyClusterID] = *body.ClusterId
	}
	return m
}

func (a *ACKCloudProvider) sdkNodepoolToNodeGroup(np *client.DescribeClusterNodePoolsResponseBodyNodepools) NodeGroup {
	ng := NodeGroup{Labels: map[string]string{}}

	if np.NodepoolInfo != nil {
		if np.NodepoolInfo.NodepoolId != nil {
			ng.ID = *np.NodepoolInfo.NodepoolId
		}
		if np.NodepoolInfo.Name != nil {
			ng.Name = *np.NodepoolInfo.Name
		}
	}

	if np.KubernetesConfig != nil && np.KubernetesConfig.Labels != nil {
		for _, t := range np.KubernetesConfig.Labels {
			if t != nil && t.Key != nil && t.Value != nil {
				ng.Labels[*t.Key] = *t.Value
			}
		}
	}

	if np.Status != nil {
		if np.Status.TotalNodes != nil {
			ng.NodeCount = int(*np.Status.TotalNodes)
		}
		if np.Status.State != nil {
			raw := strings.ToLower(*np.Status.State)
			ng.Status = mapStatusToNodeGroupStatus(raw)
		}
	}

	if np.KubernetesConfig != nil && np.KubernetesConfig.RuntimeVersion != nil {
		ng.RuntimeVersion = *np.KubernetesConfig.RuntimeVersion
	}

	// Extract Kubernetes version from kubelet component (e.g. "1.33.3-aliyun.1")
	for _, c := range np.NodeComponents {
		if c != nil && c.Name != nil && *c.Name == "kubelet" && c.Version != nil {
			ng.Version = *c.Version
			break
		}
	}

	if ng.Name == "" {
		ng.Name = ng.ID
	}
	return ng
}

func (a *ACKCloudProvider) sdkNodePoolDetailToNodeGroup(body *client.DescribeClusterNodePoolDetailResponseBody) NodeGroup {
	ng := NodeGroup{Labels: map[string]string{}}

	if body.NodepoolInfo != nil {
		if body.NodepoolInfo.NodepoolId != nil {
			ng.ID = *body.NodepoolInfo.NodepoolId
		}
		if body.NodepoolInfo.Name != nil {
			ng.Name = *body.NodepoolInfo.Name
		}
	}

	if body.KubernetesConfig != nil && body.KubernetesConfig.Labels != nil {
		for _, t := range body.KubernetesConfig.Labels {
			if t != nil && t.Key != nil && t.Value != nil {
				ng.Labels[*t.Key] = *t.Value
			}
		}
	}

	if body.KubernetesConfig != nil && body.KubernetesConfig.RuntimeVersion != nil {
		ng.RuntimeVersion = *body.KubernetesConfig.RuntimeVersion
	}

	if body.Status != nil {
		if body.Status.TotalNodes != nil {
			ng.NodeCount = int(*body.Status.TotalNodes)
		}
		if body.Status.State != nil {
			raw := strings.ToLower(*body.Status.State)
			ng.Status = mapStatusToNodeGroupStatus(raw)
		}
	}

	// Extract Kubernetes version from kubelet component (e.g. "1.33.3-aliyun.1")
	for _, c := range body.NodeComponents {
		if c != nil && c.Name != nil && *c.Name == "kubelet" && c.Version != nil {
			ng.Version = *c.Version
			break
		}
	}

	if ng.Name == "" {
		ng.Name = ng.ID
	}
	return ng
}

func mapStatusToNodeGroupStatus(raw string) NodeGroupStatus {
	switch raw {
	case "active", "running", "succeeded":
		return NodeGroupStatusActive
	case "creating", "provisioning", "initial":
		return NodeGroupStatusCreating
	case "updating", "upgrading", "reconciling":
		return NodeGroupStatusUpdating
	case "deleting":
		return NodeGroupStatusDeleting
	case "failed", "error":
		return NodeGroupStatusCreateFailed
	default:
		if raw != "" {
			return NodeGroupStatus(raw)
		}
		return ""
	}
}

func strFromAny(v any) string {
	switch t := v.(type) {
	case nil:
		return ""
	case string:
		return t
	case *string:
		if t == nil {
			return ""
		}
		return *t
	default:
		return fmt.Sprintf("%v", t)
	}
}
