package vpnconfig

import (
	"context"
	"errors"
	"net"

	"sigs.k8s.io/controller-runtime/pkg/client"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"edge-infra.dev/pkg/edge/api/types"
	v1cluster "edge-infra.dev/pkg/edge/apis/cluster/v1alpha1"
	v1vpnconfig "edge-infra.dev/pkg/sds/remoteaccess/k8s/apis/vpnconfigs/v1"
	"edge-infra.dev/pkg/sds/remoteaccess/service"
	"edge-infra.dev/pkg/sds/remoteaccess/wireguard/vpn"
)

func Update(ctx context.Context, c client.Client, sm types.SecretManagerService, vpn *vpn.VPN, vpnConfig *v1vpnconfig.VPNConfig, cluster *v1cluster.Cluster) error {
	// Set relay and client wireguard instances in banner config
	if err := vpn.UpdateRelay(ctx, c); err != nil {
		return err
	}
	if err := vpn.UpdateClient(ctx, c); err != nil {
		return err
	}

	// retrieve external load balancer ip and reconcile if its not set
	lbIP, err := service.GetLoadBalancerExternalIP(ctx, c)
	if err != nil {
		return err
	} else if lbIP == nil {
		return errors.New("load balancer does not have an external IP")
	}

	// ensure subnet is configured from up-to-date CIDR from VPN ConfigMap
	if err := vpn.ConfigureSubnet(ctx, c); err != nil {
		return err
	}

	// ensure VPNConfig has valid IP address
	if err := updateVPNConfigIPAddress(vpnConfig, vpn); err != nil {
		return err
	}

	// update store config object
	if err := vpn.UpdateStore(ctx, c, vpnConfig, cluster); err != nil {
		return err
	}

	_, subnet, err := net.ParseCIDR(vpn.GetSubnetCIDR())
	if err != nil {
		return err
	}

	// update store wireguard objects
	if err := vpn.Store(vpnConfig.ClusterEdgeID()).UpdateWireguardSecret(
		ctx,
		c,
		subnet,
		vpn.Client().GetIPAddress(),
		lbIP,
		vpn.Relay().GetPublicKey(),
		sm,
		cluster,
	); err != nil {
		return err
	}

	// update store emissary CRDs
	if err := vpn.Store(vpnConfig.ClusterEdgeID()).UpdateEmissaryMapping(ctx, c, cluster.GetName()); err != nil {
		return err
	}

	// update client objects
	if err := vpn.Client().UpdateWireguardSecret(
		ctx,
		c,
		subnet,
		lbIP,
		vpn.Relay().GetPublicKey(),
		vpn.Stores(),
	); err != nil {
		return err
	}

	// update relay objects
	return vpn.Relay().UpdateWireguardSecret(
		ctx,
		c,
		subnet,
		vpn.Client().GetIPAddress(),
		vpn.Client().GetPublicKey(),
		vpn.Stores(),
	)
}

func updateVPNConfigIPAddress(vpnConfig *v1vpnconfig.VPNConfig, vpn *vpn.VPN) error {
	if isValid, err := checkIPAddressIsValid(vpnConfig, vpn); err != nil {
		return err
	} else if isValid {
		return nil
	}

	// request a new (valid) IP address from the pool
	ip, err := vpn.RequestAvailableIPAddress(vpnConfig.ClusterEdgeID())
	if err != nil {
		return err
	}

	// set the IP address in the VPNConfig status field
	if vpnConfig.Status == nil {
		vpnConfig.Status = &v1vpnconfig.VPNConfigStatus{}
	}
	vpnConfig.Status.IP = ip.String()
	return nil
}

func checkIPAddressIsValid(vpnConfig *v1vpnconfig.VPNConfig, vpn *vpn.VPN) (bool, error) {
	// IP address is not valid if outside of subnet
	if isInSubnet, err := vpn.IPAddressIsInSubnet(vpnConfig.IP()); err != nil {
		return false, err
	} else if !isInSubnet {
		return false, nil
	}

	// if the IP address unassigned in the pool, IP is valid
	if vpn.CheckIPAddressIsAvailable(vpnConfig.IP()) {
		return true, nil
	}

	// if the IP address is taken by itself, IP is valid
	if vpn.CheckIPAddressIsAssignedToStore(vpnConfig.IP(), vpnConfig.ClusterEdgeID()) {
		return true, nil
	}

	return false, nil
}

func Remove(ctx context.Context, c client.Client, sm types.SecretManagerService, vpn *vpn.VPN, vpnConfig *v1vpnconfig.VPNConfig, cluster *v1cluster.Cluster) error {
	// create dummy store if it doesn't exist yet so we can delete any objects that may be in the cluster before removing it
	if !vpn.HasStore(vpnConfig.ClusterEdgeID()) {
		if err := vpn.UpdateStore(ctx, c, vpnConfig, cluster); err != nil {
			return err
		}
	}

	// remove store Wireguard objects
	if err := vpn.Store(vpnConfig.ClusterEdgeID()).RemoveWireguardSecret(ctx, c, sm); err != nil {
		return err
	}

	// remove store Emissary CRDs
	if err := vpn.Store(vpnConfig.ClusterEdgeID()).RemoveEmissaryMapping(ctx, c); err != nil {
		return err
	}

	// remove the store from banner config
	vpn.RemoveStore(vpnConfig.ClusterEdgeID())

	// retrieve external load balancer ip and reconcile if its not set
	lbIP, err := service.GetLoadBalancerExternalIP(ctx, c)
	if err != nil {
		return err
	} else if lbIP == nil {
		return errors.New("load balancer does not have an external IP")
	}

	_, subnet, err := net.ParseCIDR(vpn.GetSubnetCIDR())
	if err != nil {
		return err
	}

	// update client objects
	if err := vpn.Client().UpdateWireguardSecret(
		ctx,
		c,
		subnet,
		lbIP,
		vpn.Relay().GetPublicKey(),
		vpn.Stores(),
	); err != nil {
		return err
	}

	// update relay objects
	return vpn.Relay().UpdateWireguardSecret(
		ctx,
		c,
		subnet,
		vpn.Client().GetIPAddress(),
		vpn.Client().GetPublicKey(),
		vpn.Stores(),
	)
}

func AddOwnerReference(vpnConfig *v1vpnconfig.VPNConfig, cluster *v1cluster.Cluster) {
	vpnConfig.SetOwnerReferences([]metav1.OwnerReference{createOwnerReferenceToCluster(cluster)})
}

func createOwnerReferenceToCluster(cluster *v1cluster.Cluster) metav1.OwnerReference {
	return metav1.OwnerReference{
		APIVersion: cluster.APIVersion,
		Kind:       cluster.Kind,
		Name:       cluster.GetName(),
		UID:        cluster.GetUID(),
	}
}