/*
Copyright 2019 The Kubernetes Authors.

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 gcetasks

import (
	"fmt"
	"reflect"

	compute "google.golang.org/api/compute/v1"
	"k8s.io/klog/v2"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/gce"
	"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
	"k8s.io/kops/upup/pkg/fi/cloudup/terraformWriter"
)

// +kops:fitask
type Subnet struct {
	Name      *string
	Lifecycle fi.Lifecycle

	Network *Network
	Region  *string
	CIDR    *string

	// StackType indicates the address families supported (IPV4_IPV6 or IPV4_ONLY)
	StackType *string

	// Ipv6AccessType indicates whether the IPv6 addresses are accessible externally
	Ipv6AccessType *string

	SecondaryIpRanges map[string]string

	Shared *bool
}

var _ fi.CompareWithID = &Subnet{}

func (e *Subnet) CompareWithID() *string {
	return e.Name
}

func (e *Subnet) Find(c *fi.CloudupContext) (*Subnet, error) {
	cloud := c.T.Cloud.(gce.GCECloud)
	_, project, err := gce.ParseNameAndProjectFromNetworkID(c.T.Cluster.Spec.Networking.NetworkID)
	if err != nil {
		return nil, fmt.Errorf("error parsing network name from cluster spec: %w", err)
	} else if project == "" {
		project = cloud.Project()
	}
	s, err := cloud.Compute().Subnetworks().Get(project, cloud.Region(), *e.Name)
	if err != nil {
		if gce.IsNotFound(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("error listing Subnets: %w", err)
	}

	actual := &Subnet{}
	actual.Name = &s.Name
	actual.Network = &Network{Name: fi.PtrTo(lastComponent(s.Network))}
	actual.Region = fi.PtrTo(lastComponent(s.Region))
	actual.CIDR = &s.IpCidrRange
	actual.StackType = &s.StackType
	actual.Ipv6AccessType = &s.Ipv6AccessType

	shared := fi.ValueOf(e.Shared)
	{
		actual.SecondaryIpRanges = make(map[string]string)
		for _, r := range s.SecondaryIpRanges {
			if shared {
				// In the shared case, only show differences on the ranges we specified
				if _, found := e.SecondaryIpRanges[r.RangeName]; !found {
					continue
				}
			}

			actual.SecondaryIpRanges[r.RangeName] = r.IpCidrRange
		}
	}

	// Prevent spurious changes
	actual.Lifecycle = e.Lifecycle
	actual.Name = e.Name
	actual.Shared = e.Shared

	return actual, nil
}

func (e *Subnet) Run(c *fi.CloudupContext) error {
	return fi.CloudupDefaultDeltaRunMethod(e, c)
}

func (_ *Subnet) CheckChanges(a, e, changes *Subnet) error {
	return nil
}

func (_ *Subnet) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Subnet) error {
	shared := fi.ValueOf(e.Shared)
	if shared {
		// Verify the subnet was found
		if a == nil {
			return fmt.Errorf("Subnet with name %q not found", fi.ValueOf(e.Name))
		}
	}

	cloud := t.Cloud
	project := cloud.Project()

	if a == nil {
		klog.V(2).Infof("Creating Subnet with CIDR: %q", fi.ValueOf(e.CIDR))

		subnet := &compute.Subnetwork{
			IpCidrRange:    fi.ValueOf(e.CIDR),
			Name:           *e.Name,
			Network:        e.Network.URL(project),
			StackType:      fi.ValueOf(e.StackType),
			Ipv6AccessType: fi.ValueOf(e.Ipv6AccessType),
		}

		for k, v := range e.SecondaryIpRanges {
			subnet.SecondaryIpRanges = append(subnet.SecondaryIpRanges, &compute.SubnetworkSecondaryRange{
				RangeName:   k,
				IpCidrRange: v,
			})
		}

		op, err := cloud.Compute().Subnetworks().Insert(t.Cloud.Project(), t.Cloud.Region(), subnet)
		if err != nil {
			return fmt.Errorf("error creating Subnet: %v", err)
		}
		if err := t.Cloud.WaitForOp(op); err != nil {
			return fmt.Errorf("error waiting for Subnet creation to complete: %w", err)
		}
	} else {
		if changes.SecondaryIpRanges != nil {
			// Update is split into two calls as GCE does not allow us to add and remove ranges in the same call
			if err := updateSecondaryRanges(cloud, "add", e); err != nil {
				return err
			}

			if !shared {
				if err := updateSecondaryRanges(cloud, "remove", e); err != nil {
					return err
				}
			}

			changes.SecondaryIpRanges = nil
		}

		if changes.StackType != nil {
			if err := updateStackTypeAndIPv6AccessType(cloud, e); err != nil {
				return err
			}

			changes.StackType = nil
			changes.Ipv6AccessType = nil
		}

		empty := &Subnet{}
		if !reflect.DeepEqual(empty, changes) {
			return fmt.Errorf("cannot apply changes to Subnet: %v", changes)
		}
	}

	return nil
}

func updateSecondaryRanges(cloud gce.GCECloud, op string, e *Subnet) error {
	// We need to refetch to patch it
	subnet, err := cloud.Compute().Subnetworks().Get(cloud.Project(), cloud.Region(), *e.Name)
	if err != nil {
		return fmt.Errorf("error fetching subnet for patch: %w", err)
	}

	expectedRanges := e.SecondaryIpRanges

	actualRanges := make(map[string]string)
	for _, r := range subnet.SecondaryIpRanges {
		actualRanges[r.RangeName] = r.IpCidrRange
	}

	// Cannot add and remove ranges in the same call
	if op == "add" {
		patch := false
		for k, v := range expectedRanges {
			if actualRanges[k] != v {
				actualRanges[k] = v
				subnet.SecondaryIpRanges = append(subnet.SecondaryIpRanges, &compute.SubnetworkSecondaryRange{
					RangeName:   k,
					IpCidrRange: v,
				})
				patch = true
			}
		}

		if !patch {
			return nil
		}
	} else if op == "remove" {
		patch := false
		if len(actualRanges) != len(expectedRanges) {
			patch = true
		} else {
			for k := range expectedRanges {
				if actualRanges[k] != e.SecondaryIpRanges[k] {
					patch = true
				}
			}
		}

		if !patch {
			return nil
		}

		subnet.SecondaryIpRanges = nil
		for k, v := range expectedRanges {
			subnet.SecondaryIpRanges = append(subnet.SecondaryIpRanges, &compute.SubnetworkSecondaryRange{
				RangeName:   k,
				IpCidrRange: v,
			})
		}
	}

	patchOp, err := cloud.Compute().Subnetworks().Patch(cloud.Project(), cloud.Region(), subnet.Name, subnet)
	if err != nil {
		return fmt.Errorf("error patching Subnet: %w", err)
	}

	if err := cloud.WaitForOp(patchOp); err != nil {
		return fmt.Errorf("error waiting for Subnet patch to complete: %w", err)
	}

	return nil
}

func updateStackTypeAndIPv6AccessType(cloud gce.GCECloud, e *Subnet) error {
	// We need to refetch to patch it
	subnet, err := cloud.Compute().Subnetworks().Get(cloud.Project(), cloud.Region(), *e.Name)
	if err != nil {
		return fmt.Errorf("error fetching subnet for patch: %w", err)
	}

	subnet.StackType = fi.ValueOf(e.StackType)
	subnet.Ipv6AccessType = fi.ValueOf(e.Ipv6AccessType)

	patchOp, err := cloud.Compute().Subnetworks().Patch(cloud.Project(), cloud.Region(), subnet.Name, subnet)
	if err != nil {
		return fmt.Errorf("error patching Subnet: %w", err)
	}

	if err := cloud.WaitForOp(patchOp); err != nil {
		return fmt.Errorf("error waiting for Subnet patch to complete: %w", err)
	}

	return nil
}

func (e *Subnet) URL(project string, region string) string {
	u := gce.GoogleCloudURL{
		Version: "v1",
		Project: project,
		Name:    *e.Name,
		Type:    "subnetworks",
		Region:  region,
	}
	return u.BuildURL()
}

type terraformSubnet struct {
	Name    *string                  `cty:"name"`
	Network *terraformWriter.Literal `cty:"network"`
	Region  *string                  `cty:"region"`
	CIDR    *string                  `cty:"ip_cidr_range"`

	// SecondaryIPRange defines additional IP ranges
	SecondaryIPRange []terraformSubnetRange `cty:"secondary_ip_range"`

	StackType      *string `cty:"stack_type"`
	Ipv6AccessType *string `cty:"ipv6_access_type"`
}

type terraformSubnetRange struct {
	Name string `cty:"range_name"`
	CIDR string `cty:"ip_cidr_range"`
}

func (_ *Subnet) RenderSubnet(t *terraform.TerraformTarget, a, e, changes *Subnet) error {
	shared := fi.ValueOf(e.Shared)
	if shared {
		// Not terraform owned / managed
		return nil
	}

	tf := &terraformSubnet{
		Name:           e.Name,
		Network:        e.Network.TerraformLink(),
		Region:         e.Region,
		CIDR:           e.CIDR,
		StackType:      e.StackType,
		Ipv6AccessType: e.Ipv6AccessType,
	}

	for k, v := range e.SecondaryIpRanges {
		tf.SecondaryIPRange = append(tf.SecondaryIPRange, terraformSubnetRange{
			Name: k,
			CIDR: v,
		})
	}

	return t.RenderResource("google_compute_subnetwork", *e.Name, tf)
}

func (e *Subnet) TerraformLink() *terraformWriter.Literal {
	shared := fi.ValueOf(e.Shared)
	if shared {
		if e.Name == nil {
			klog.Fatalf("GCEName must be set, if subnet is shared: %#v", e)
		}

		name := *e.Name
		if e.Network != nil && e.Network.Project != nil {
			name = *e.Network.Project + "/" + name
		}
		klog.V(4).Infof("reusing existing subnet with name %q", name)
		return terraformWriter.LiteralFromStringValue(name)
	}

	return terraformWriter.LiteralProperty("google_compute_subnetwork", *e.Name, "name")
}
