// Copyright © 2020 Banzai Cloud
//
// 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 pke

import (
	"context"

	"emperror.dev/errors"

	"github.com/banzaicloud/pipeline/internal/cluster"
)

// +testify:mock

// Service provides an interface to PKE AWS clusters.
type Service interface {
	// DeleteNodePool deletes an existing node pool.
	DeleteNodePool(ctx context.Context, clusterID uint, nodePoolName string) (isDeleted bool, err error)
	// UpdateCluster updates a cluster.
	//
	// This method accepts a partial body representation.
	UpdateCluster(ctx context.Context, clusterID uint, clusterUpdate ClusterUpdate) error

	// UpdateNodePool updates an existing node pool in a cluster.
	//
	// This method accepts a partial body representation.
	UpdateNodePool(ctx context.Context, clusterID uint, nodePoolName string, nodePoolUpdate NodePoolUpdate) (string, error)

	// ListNodePools lists node pools from a cluster.
	ListNodePools(ctx context.Context, clusterID uint) ([]NodePool, error)
}

// ClusterUpdate describes a cluster update request.
//
// A cluster update contains a partial representation of the cluster resource,
// updating only the changed values.
type ClusterUpdate struct {
	Version string `mapstructure:"version"`
}

// NodePoolUpdate describes a node pool update request.
//
// A node pool update contains a partial representation of the node pool resource,
// updating only the changed values.
type NodePoolUpdate struct {
	Image string `mapstructure:"image"`

	Version string `mapstructure:"version"`

	Options NodePoolUpdateOptions `mapstructure:"options"`
}

type NodePoolUpdateOptions struct {
	// Maximum number of extra nodes that can be created during the update.
	MaxSurge int `mapstructure:"maxSurge"`

	// Maximum number of nodes that can be updated simultaneously.
	MaxBatchSize int `mapstructure:"maxBatchSize"`

	// Maximum number of nodes that can be unavailable during the update.
	MaxUnavailable int `mapstructure:"maxUnavailable"`

	// Kubernetes node drain specific options.
	Drain NodePoolUpdateDrainOptions `mapstructure:"drain"`
}

type NodePoolUpdateDrainOptions struct {
	Timeout int `mapstructure:"timeout"`

	FailOnError bool `mapstructure:"failOnError"`

	PodSelector string `mapstructure:"podSelector"`
}

// NodePool encapsulates information about a cluster node pool.
type NodePool struct {
	Name         string            `mapstructure:"name"`
	Labels       map[string]string `mapstructure:"labels"`
	Size         int               `mapstructure:"size"`
	Autoscaling  Autoscaling       `mapstructure:"autoscaling"`
	VolumeSize   int               `mapstructure:"volumeSize"`
	InstanceType string            `mapstructure:"instanceType"`
	Image        string            `mapstructure:"image"`
	SpotPrice    string            `mapstructure:"spotPrice"`
	SubnetID     string            `mapstructure:"subnetId"`
}

// Autoscaling describes the EC2 node pool's autoscaling settings.
type Autoscaling struct {
	Enabled bool `mapstructure:"enabled"`
	MinSize int  `mapstructure:"minSize"`
	MaxSize int  `mapstructure:"maxSize"`
}

// NewService returns a new Service instance.
func NewService(
	genericClusters Store,
	nodePoolManager NodePoolManager,
	nodePoolStore NodePoolStore,
	enterprise bool,
) Service {
	return service{
		enterprise:      enterprise,
		genericClusters: genericClusters,
		nodePoolManager: nodePoolManager,
		nodePoolStore:   nodePoolStore,
	}
}

type service struct {
	enterprise      bool
	genericClusters Store
	clusterManager  ClusterManager
	nodePoolManager NodePoolManager
	nodePoolStore   NodePoolStore
}

// +testify:mock:testOnly=true

// NodePoolManager is responsible for managing node pools.
type NodePoolManager interface {
	// DeleteNodePool deletes an existing node pool in a cluster.
	DeleteNodePool(
		ctx context.Context,
		c cluster.Cluster,
		existingNodePool ExistingNodePool,
		shouldUpdateClusterStatus bool,
	) (err error)

	// UpdateNodePool updates an existing node pool in a cluster.
	UpdateNodePool(
		ctx context.Context,
		c cluster.Cluster,
		nodePoolName string,
		nodePoolUpdate NodePoolUpdate,
	) (string, error)
}

// ClusterManager is responsible for managing clusters.
type ClusterManager interface {
	// UpdateCluster updates an existing cluster.
	UpdateCluster(ctx context.Context, c cluster.Cluster, clusterUpdate ClusterUpdate) error
}

func (s service) DeleteNodePool(ctx context.Context, clusterID uint, nodePoolName string) (isDeleted bool, err error) {
	c, err := s.genericClusters.GetCluster(ctx, clusterID)
	if err != nil {
		return false, err
	}

	existingNodePools, err := s.nodePoolStore.ListNodePools(ctx, c.OrganizationID, c.ID, c.Name)
	if err != nil {
		return false, err
	}

	existingNodePool, isExisting := existingNodePools[nodePoolName]
	if !isExisting {
		return true, nil
	}

	err = s.genericClusters.SetStatus(ctx, clusterID, cluster.Updating, "deleting node pool")
	if err != nil {
		return false, err
	}

	err = s.nodePoolManager.DeleteNodePool(ctx, c, existingNodePool, true)
	if err != nil {
		return false, err
	}

	return false, nil
}

func (s service) UpdateCluster(
	ctx context.Context,
	clusterID uint,
	clusterUpdate ClusterUpdate,
) error {
	c, err := s.genericClusters.GetCluster(ctx, clusterID)
	if err != nil {
		return err
	}

	err = s.genericClusters.SetStatus(ctx, clusterID, cluster.Updating, "updating cluster")
	if err != nil {
		return err
	}

	return s.clusterManager.UpdateCluster(ctx, c, clusterUpdate)
}

func (s service) UpdateNodePool(
	ctx context.Context,
	clusterID uint,
	nodePoolName string,
	nodePoolUpdate NodePoolUpdate,
) (string, error) {
	c, err := s.genericClusters.GetCluster(ctx, clusterID)
	if err != nil {
		return "", err
	}

	err = s.genericClusters.SetStatus(ctx, clusterID, cluster.Updating, "updating node pool")
	if err != nil {
		return "", err
	}

	return s.nodePoolManager.UpdateNodePool(ctx, c, nodePoolName, nodePoolUpdate)
}

// ListNodePools lists node pools from a cluster.
func (s service) ListNodePools(ctx context.Context, clusterID uint) ([]NodePool, error) {
	_, err := s.genericClusters.GetCluster(ctx, clusterID)
	if err != nil {
		return nil, errors.WrapWithDetails(err, "retrieving cluster failed", "clusterID", clusterID)
	}

	panic("not implemented")
}

// +testify:mock:testOnly=true

// Store provides an interface to the generic Cluster model persistence.
type Store interface {
	// GetCluster returns a generic Cluster.
	// Returns an error with the NotFound behavior when the cluster cannot be found.
	GetCluster(ctx context.Context, id uint) (cluster.Cluster, error)

	// SetStatus sets the cluster status.
	SetStatus(ctx context.Context, id uint, status string, statusMessage string) error
}
