package cluster

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	awseks "github.com/aws/aws-sdk-go-v2/service/eks"
	ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
	"github.com/kris-nova/logger"

	"github.com/weaveworks/eksctl/pkg/actions/addon"
	"github.com/weaveworks/eksctl/pkg/actions/capability"
	"github.com/weaveworks/eksctl/pkg/actions/nodegroup"
	"github.com/weaveworks/eksctl/pkg/actions/podidentityassociation"
	api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
	"github.com/weaveworks/eksctl/pkg/cfn/manager"
	"github.com/weaveworks/eksctl/pkg/ctl/cmdutils"
	"github.com/weaveworks/eksctl/pkg/eks"
	iamoidc "github.com/weaveworks/eksctl/pkg/iam/oidc"
	"github.com/weaveworks/eksctl/pkg/kubernetes"
	"github.com/weaveworks/eksctl/pkg/utils/tasks"
	"github.com/weaveworks/eksctl/pkg/vpc"
)

type OwnedCluster struct {
	cfg                 *api.ClusterConfig
	ctl                 *eks.ClusterProvider
	clusterStack        *manager.Stack
	stackManager        manager.StackManager
	newClientSet        func() (kubernetes.Interface, error)
	newNodeGroupDrainer func(clientSet kubernetes.Interface) NodeGroupDrainer
	autoModeDeleter     AutoModeDeleter
}

type AutoModeDeleter interface {
	DeleteIfRequired(ctx context.Context) error
}

func NewOwnedCluster(cfg *api.ClusterConfig, ctl *eks.ClusterProvider, clusterStack *manager.Stack, stackManager manager.StackManager, autoModeDeleter AutoModeDeleter) *OwnedCluster {
	return &OwnedCluster{
		cfg:             cfg,
		ctl:             ctl,
		clusterStack:    clusterStack,
		autoModeDeleter: autoModeDeleter,
		stackManager:    stackManager,
		newClientSet: func() (kubernetes.Interface, error) {
			return ctl.NewStdClientSet(cfg)
		},
		newNodeGroupDrainer: func(clientSet kubernetes.Interface) NodeGroupDrainer {
			return &nodegroup.Drainer{
				ClientSet: clientSet,
			}
		},
	}
}

func (c *OwnedCluster) Upgrade(ctx context.Context, dryRun bool) error {
	if err := vpc.UseFromClusterStack(ctx, c.ctl.AWSProvider, c.clusterStack, c.cfg, true); err != nil {
		return fmt.Errorf("getting VPC configuration for cluster %q: %w", c.cfg.Metadata.Name, err)
	}

	versionUpdateRequired, err := upgrade(ctx, c.cfg, c.ctl, dryRun)
	if err != nil {
		return err
	}

	remoteNetworkUpdateRequired, err := c.updateRemoteNetworkConfig(ctx, dryRun)
	if err != nil {
		return err
	}

	stackUpdateRequired, err := c.stackManager.AppendNewClusterStackResource(ctx, false, dryRun)
	if err != nil {
		return err
	}

	if err := eks.ValidateExistingNodeGroupsForCompatibility(ctx, c.cfg, c.stackManager); err != nil {
		logger.Critical("failed checking nodegroups", err.Error())
	}

	cmdutils.LogPlanModeWarning(dryRun && (stackUpdateRequired || versionUpdateRequired || remoteNetworkUpdateRequired))
	return nil
}

func (c *OwnedCluster) updateRemoteNetworkConfig(ctx context.Context, dryRun bool) (bool, error) {
	if c.cfg.RemoteNetworkConfig == nil {
		return false, nil
	}

	rnc := c.cfg.RemoteNetworkConfig
	req := &ekstypes.RemoteNetworkConfigRequest{}
	if rnc.RemoteNodeNetworks != nil {
		req.RemoteNodeNetworks = make([]ekstypes.RemoteNodeNetwork, len(rnc.RemoteNodeNetworks))
		for i, rn := range rnc.RemoteNodeNetworks {
			req.RemoteNodeNetworks[i] = ekstypes.RemoteNodeNetwork{Cidrs: rn.CIDRs}
		}
	}
	if rnc.RemotePodNetworks != nil {
		req.RemotePodNetworks = make([]ekstypes.RemotePodNetwork, len(rnc.RemotePodNetworks))
		for i, rp := range rnc.RemotePodNetworks {
			req.RemotePodNetworks[i] = ekstypes.RemotePodNetwork{Cidrs: rp.CIDRs}
		}
	}

	cmdutils.LogIntendedAction(dryRun, "update remote network config for cluster %q", c.cfg.Metadata.Name)
	if !dryRun {
		if err := c.ctl.UpdateClusterConfig(ctx, &awseks.UpdateClusterConfigInput{
			Name:                aws.String(c.cfg.Metadata.Name),
			RemoteNetworkConfig: req,
		}); err != nil {
			if strings.Contains(err.Error(), "No changes detected") {
				logger.Info("remote network config is already up-to-date on cluster %q", c.cfg.Metadata.Name)
				return false, nil
			}
			return false, fmt.Errorf("updating remote network config: %w", err)
		}
		logger.Success("remote network config updated successfully for cluster %q", c.cfg.Metadata.Name)
	}
	return true, nil
}

func (c *OwnedCluster) Delete(ctx context.Context, _, podEvictionWaitPeriod time.Duration, wait, force, disableNodegroupEviction bool, parallel int) error {
	clusterOperable, err := c.ctl.CanOperate(c.cfg)
	if err != nil {
		logger.Debug("failed to check if cluster is operable: %v", err)
	}

	// moving this here was fine because inside `NewTasksToDeleteClusterWithNodeGroups` we did it anyway.
	allStacks, err := c.stackManager.ListNodeGroupStacksWithStatuses(ctx)
	if err != nil {
		return err
	}

	var clientSet kubernetes.Interface
	if clusterOperable {
		var err error
		clientSet, err = c.newClientSet()
		if err != nil {
			if force {
				logger.Warning("error occurred during deletion: %v", err)
			} else {
				return err
			}
		}

		drainer := c.newNodeGroupDrainer(clientSet)
		if err := drainAllNodeGroups(ctx, c.cfg, c.ctl, clientSet, allStacks, disableNodegroupEviction, parallel, drainer, func(clusterConfig *api.ClusterConfig, ctl *eks.ClusterProvider, clientSet kubernetes.Interface) {
			attemptVpcCniDeletion(ctx, clusterConfig, ctl, clientSet)
		}, podEvictionWaitPeriod); err != nil {
			if !force {
				return err
			}

			logger.Warning("an error occurred during nodegroups draining, force=true so proceeding with deletion: %q", err.Error())
		}
	}

	if err := deleteSharedResources(ctx, c.cfg, c.ctl, c.stackManager, clusterOperable, clientSet); err != nil {
		if force {
			logger.Warning("error occurred during deletion: %v", err)
		} else {
			return err
		}
	}

	newOIDCManager := func() (*iamoidc.OpenIDConnectManager, error) {
		return c.ctl.NewOpenIDConnectManager(ctx, c.cfg)
	}
	newTasksToDeleteAddonIAM := addon.NewRemover(c.stackManager).DeleteAddonIAMTasks

	newTasksToDeleteCapability := func() (*tasks.TaskTree, error) {
		capabilityRemover := capability.NewRemover(c.cfg.Metadata.Name, c.stackManager)
		return capabilityRemover.DeleteTasks(ctx, nil) // Delete all capabilities.
	}

	newTasksToDeletePodIdentityRoles := func() (*tasks.TaskTree, error) {
		if !clusterOperable {
			return &tasks.TaskTree{}, nil
		}
		clientSet, err = c.newClientSet()
		if err != nil {
			if force {
				logger.Warning("error occurred while deleting IAM Role stacks for pod identity associations: %v; force=true so proceeding with cluster deletion", err)
				return &tasks.TaskTree{}, nil
			}
			return nil, err
		}
		return podidentityassociation.NewDeleter(c.cfg.Metadata.Name, c.stackManager, c.ctl.AWSProvider.EKS(), clientSet).
			DeleteTasks(ctx, []podidentityassociation.Identifier{})
	}

	tasks, err := c.stackManager.NewTasksToDeleteClusterWithNodeGroups(ctx, c.clusterStack, allStacks, clusterOperable, newOIDCManager, newTasksToDeleteAddonIAM, newTasksToDeleteCapability, newTasksToDeletePodIdentityRoles, c.ctl.Status.ClusterInfo.Cluster, kubernetes.NewCachedClientSet(clientSet), wait, force, func(errs chan error, _ string) error {
		logger.Info("trying to cleanup dangling network interfaces")
		stack, err := c.stackManager.DescribeClusterStack(ctx)
		if err != nil {
			return fmt.Errorf("error describing cluster stack: %w", err)
		}
		if err := c.ctl.LoadClusterVPC(ctx, c.cfg, stack, true); err != nil {
			return fmt.Errorf("getting VPC configuration for cluster %q: %w", c.cfg.Metadata.Name, err)
		}

		go func() {
			errs <- vpc.CleanupNetworkInterfaces(ctx, c.ctl.AWSProvider.EC2(), c.cfg)
			close(errs)
		}()
		return nil
	})

	if err != nil {
		return err
	}

	if tasks.Len() == 0 {
		logger.Warning("no cluster resources were found for %q", c.cfg.Metadata.Name)
		return nil
	}

	logger.Info(tasks.Describe())
	if errs := tasks.DoAllSync(); len(errs) > 0 {
		return handleErrors(errs, "cluster with nodegroup(s)")
	}

	if err := c.deleteKarpenterStackIfExists(ctx); err != nil {
		return err
	}

	if err := checkForUndeletedStacks(ctx, c.stackManager); err != nil {
		return err
	}
	if err := c.autoModeDeleter.DeleteIfRequired(ctx); err != nil {
		return err
	}

	logger.Success("all cluster resources were deleted")

	return nil
}

func (c *OwnedCluster) deleteKarpenterStackIfExists(ctx context.Context) error {
	stack, err := c.stackManager.GetKarpenterStack(ctx)
	if err != nil {
		return err
	}

	if stack != nil {
		logger.Info("deleting karpenter stack")
		return c.stackManager.DeleteStackSync(ctx, stack)
	}

	return nil
}
