package eks

import (
	"context"
	"fmt"

	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/internet/task"
)

type IPAddress struct {
	*task.BaseTask
	ipAddresses []*v1beta1.Address
}

func NewIPAddress(ctx *task.Context) *IPAddress {
	return &IPAddress{
		BaseTask:    &task.BaseTask{Context: ctx},
		ipAddresses: []*v1beta1.Address{},
	}
}

// Name implements Task interface.
// Returns the name of the task.
func (n *IPAddress) Name() string {
	return "IPAddress"
}

func (n *IPAddress) Create(ctx context.Context) deploy.Result {
	if n.IsHibernated() {
		return deploy.Done()
	}
	name := n.InternetName()
	tags := cloudv1.TagsFromLabels(n.Internet.Labels)
	zones := n.GetZones()

	for _, zone := range zones {
		tags := util.AddResourceName(tags, namer.PublicSubnetName(name, zone))
		ip, err := n.createIP(ctx, name, tags, zone, n.VPC)
		if err != nil {
			return deploy.Panic(err)
		}
		n.ipAddresses = append(n.ipAddresses, ip)
	}
	return deploy.Done()
}

// N.B. the mutate func does nothing for update, so special logic for migrated resource.
// otherwise chage creteIP to handle migrated resource.
func (n *IPAddress) createIP(ctx context.Context, name string, tags map[string]string, zone string, vpc *v1alpha1.VPC) (*v1beta1.Address, error) {
	ip := &v1beta1.Address{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.PublicSubnetName(name, zone),
			Labels: map[string]string{
				v1alpha1.InternetNameKey: n.InternetName(),
			},
		},
		Spec: v1beta1.AddressSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: n.ProviderConfigName()},
			},
			ForProvider: v1beta1.AddressParameters{
				Region: n.Internet.Spec.Region,
				Domain: pointer.String("vpc"),
				Tags:   util.GetEC2V1BetaTags(tags),
			},
		},
	}

	// TODO: why need to set owner reference to private subnet?
	private, err := findSubnetByZone(ctx, n.Local, vpc.Status.PrivateSubnets, zone)
	if err != nil {
		return nil, err
	}

	if err := controllerutil.SetControllerReference(private, ip, n.Local.Scheme()); err != nil {
		return nil, err
	}

	if _, err := controllerutil.CreateOrUpdate(ctx, n.Local, ip, func() error {
		return nil
	}); err != nil {
		return nil, err
	}

	return ip, nil
}

func (n *IPAddress) PostCreate(ctx context.Context) deploy.Result {
	if n.IsHibernated() {
		return deploy.Done()
	}
	records := []v1alpha1.IPAddress{}
	for _, ip := range n.ipAddresses {
		ipRes := &v1beta1.Address{}
		if err := n.Local.Get(ctx, types.NamespacedName{Name: ip.Name}, ipRes); err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}

		allocationID := meta.GetExternalName(ipRes)
		if allocationID == "" {
			return deploy.Panic(fmt.Errorf("failed to get IP %s allocationID", ip.Name)).WithRequeuAfter(defaultRequeuInterval)
		}
		records = append(records, v1alpha1.IPAddress{
			AllocationID: allocationID,
			Name:         ip.Name,
		})
	}
	n.Internet.Status.IPAddresses = records
	if err := n.InternetRW.UpdateStatus(ctx, n.Internet); err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	return deploy.Done()
}

func (n *IPAddress) Delete(ctx context.Context) deploy.Result {
	// FIXME(cwen0): use config for zones in delete task may cause problem
	zones := n.GetZones()
	name := n.InternetName()

	for _, zone := range zones {
		if err := n.deleteIP(ctx, name, zone); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

func (n *IPAddress) deleteIP(ctx context.Context, name string, zone string) error {
	ip := &v1beta1.Address{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.PublicSubnetName(name, zone),
		},
	}
	return k8s.DeleteObject(ctx, n.Local, ip)
}

func (n *IPAddress) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}
