/*
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 awstasks

import (
	"context"
	"encoding/base64"
	"fmt"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"k8s.io/klog/v2"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
	"k8s.io/kops/upup/pkg/fi/cloudup/terraformWriter"
)

// MaxUserDataSize is the max size of the userdata
const MaxUserDataSize = 16384

// Instance defines the instance specification
// +kops:fitask
type Instance struct {
	ID        *string
	Lifecycle fi.Lifecycle

	UserData fi.Resource

	Subnet           *Subnet
	PrivateIPAddress *string

	Name *string
	Tags map[string]string

	Shared *bool

	ImageID            *string
	InstanceType       ec2types.InstanceType
	SSHKey             *SSHKey
	SecurityGroups     []*SecurityGroup
	AssociatePublicIP  *bool
	IAMInstanceProfile *IAMInstanceProfile
}

var _ fi.CompareWithID = &Instance{}

func (s *Instance) CompareWithID() *string {
	return s.ID
}

func (e *Instance) Find(c *fi.CloudupContext) (*Instance, error) {
	ctx := c.Context()
	cloud := awsup.GetCloud(c)
	var request *ec2.DescribeInstancesInput

	if fi.ValueOf(e.Shared) {
		var instanceIds []string
		instanceIds = append(instanceIds, aws.ToString(e.ID))
		request = &ec2.DescribeInstancesInput{
			InstanceIds: instanceIds,
		}
	} else {
		filters := cloud.BuildFilters(e.Name)
		filters = append(filters, awsup.NewEC2Filter("instance-state-name", "pending", "running", "stopping", "stopped"))
		request = &ec2.DescribeInstancesInput{
			Filters: filters,
		}
	}

	response, err := cloud.EC2().DescribeInstances(ctx, request)
	if err != nil {
		return nil, fmt.Errorf("error listing instances: %v", err)
	}

	instances := []ec2types.Instance{}
	if response != nil {
		for _, reservation := range response.Reservations {
			instances = append(instances, reservation.Instances...)
		}
	}

	if len(instances) == 0 {
		return nil, nil
	}

	if len(instances) != 1 {
		return nil, fmt.Errorf("found multiple Instances with name: %s", *e.Name)
	}

	klog.V(2).Info("found existing instance")
	i := instances[0]

	if i.InstanceId == nil {
		return nil, fmt.Errorf("found instance, but InstanceId was nil")
	}

	actual := &Instance{
		ID:               i.InstanceId,
		PrivateIPAddress: i.PrivateIpAddress,
		InstanceType:     i.InstanceType,
		ImageID:          i.ImageId,
		Name:             findNameTag(i.Tags),
	}

	// Fetch instance UserData
	{
		request := &ec2.DescribeInstanceAttributeInput{}
		request.InstanceId = i.InstanceId
		request.Attribute = ec2types.InstanceAttributeNameUserData
		response, err := cloud.EC2().DescribeInstanceAttribute(ctx, request)
		if err != nil {
			return nil, fmt.Errorf("error querying EC2 for user metadata for instance %q: %v", *i.InstanceId, err)
		}
		if response.UserData != nil {
			b, err := base64.StdEncoding.DecodeString(aws.ToString(response.UserData.Value))
			if err != nil {
				return nil, fmt.Errorf("error decoding EC2 UserData: %v", err)
			}
			actual.UserData = fi.NewBytesResource(b)
		}
	}

	if i.SubnetId != nil {
		actual.Subnet = &Subnet{ID: i.SubnetId}
	}
	if i.KeyName != nil {
		actual.SSHKey = &SSHKey{Name: i.KeyName}
	}

	for _, sg := range i.SecurityGroups {
		actual.SecurityGroups = append(actual.SecurityGroups, &SecurityGroup{ID: sg.GroupId})
	}

	associatePublicIpAddress := false
	for _, ni := range i.NetworkInterfaces {
		if aws.ToString(ni.Association.PublicIp) != "" {
			associatePublicIpAddress = true
		}
	}
	actual.AssociatePublicIP = &associatePublicIpAddress

	if i.IamInstanceProfile != nil {
		actual.IAMInstanceProfile = &IAMInstanceProfile{Name: nameFromIAMARN(i.IamInstanceProfile.Arn)}
	}

	actual.Tags = intersectTags(i.Tags, e.Tags)

	actual.Lifecycle = e.Lifecycle
	actual.Shared = e.Shared

	e.ID = actual.ID

	// Avoid spurious changes on ImageId
	if e.ImageID != nil && actual.ImageID != nil && *actual.ImageID != *e.ImageID {
		image, err := cloud.ResolveImage(*e.ImageID)
		if err != nil {
			klog.Warningf("unable to resolve image: %q: %v", *e.ImageID, err)
		} else if image == nil {
			klog.Warningf("unable to resolve image: %q: not found", *e.ImageID)
		} else if aws.ToString(image.ImageId) == *actual.ImageID {
			klog.V(4).Infof("Returning matching ImageId as expected name: %q -> %q", *actual.ImageID, *e.ImageID)
			actual.ImageID = e.ImageID
		}
	}

	return actual, nil
}

func nameFromIAMARN(arn *string) *string {
	if arn == nil {
		return nil
	}
	tokens := strings.Split(*arn, ":")
	last := tokens[len(tokens)-1]

	if !strings.HasPrefix(last, "instance-profile/") {
		klog.Warningf("Unexpected ARN for instance profile: %q", *arn)
	}

	name := strings.TrimPrefix(last, "instance-profile/")
	return &name
}

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

func (_ *Instance) CheckChanges(a, e, changes *Instance) error {
	if a != nil {
		if !fi.ValueOf(e.Shared) && e.Name == nil {
			return fi.RequiredField("Name")
		}
	}
	return nil
}

func (_ *Instance) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Instance) error {
	ctx := context.TODO()
	if a == nil {

		if fi.ValueOf(e.Shared) {
			return fmt.Errorf("NAT EC2 Instance %q not found", fi.ValueOf(e.ID))
		}

		if e.ImageID == nil {
			return fi.RequiredField("ImageID")
		}
		image, err := t.Cloud.ResolveImage(fi.ValueOf(e.ImageID))
		if err != nil {
			return err
		}

		klog.V(2).Infof("Creating Instance with Name:%q", fi.ValueOf(e.Name))
		request := &ec2.RunInstancesInput{
			ImageId:      image.ImageId,
			InstanceType: e.InstanceType,
			MinCount:     aws.Int32(1),
			MaxCount:     aws.Int32(1),
		}

		if e.SSHKey != nil {
			request.KeyName = e.SSHKey.Name
		}

		securityGroupIDs := []string{}
		for _, sg := range e.SecurityGroups {
			securityGroupIDs = append(securityGroupIDs, fi.ValueOf(sg.ID))
		}
		request.NetworkInterfaces = []ec2types.InstanceNetworkInterfaceSpecification{
			{
				DeviceIndex:              aws.Int32(0),
				AssociatePublicIpAddress: e.AssociatePublicIP,
				SubnetId:                 e.Subnet.ID,
				PrivateIpAddress:         e.PrivateIPAddress,
				Groups:                   securityGroupIDs,
			},
		}

		// Build up the actual block device mappings
		// TODO: Support RootVolumeType & RootVolumeSize (see launchconfiguration)
		blockDeviceMappings, err := buildEphemeralDevices(t.Cloud, e.InstanceType)
		if err != nil {
			return err
		}

		if len(blockDeviceMappings) != 0 {
			request.BlockDeviceMappings = []ec2types.BlockDeviceMapping{}
			for deviceName, bdm := range blockDeviceMappings {
				request.BlockDeviceMappings = append(request.BlockDeviceMappings, bdm.ToEC2(deviceName))
			}
		}

		if e.UserData != nil {
			d, err := fi.ResourceAsBytes(e.UserData)
			if err != nil {
				return fmt.Errorf("error rendering Instance UserData: %v", err)
			}
			if len(d) > MaxUserDataSize {
				// TODO: Re-enable gzip?
				// But it exposes some bugs in the AWS console, so if we can avoid it, we should
				//d, err = fi.GzipBytes(d)
				//if err != nil {
				//	return fmt.Errorf("error while gzipping UserData: %v", err)
				//}
				return fmt.Errorf("Instance UserData was too large (%d bytes)", len(d))
			}
			request.UserData = aws.String(base64.StdEncoding.EncodeToString(d))
		}

		if e.IAMInstanceProfile != nil {
			request.IamInstanceProfile = &ec2types.IamInstanceProfileSpecification{
				Name: e.IAMInstanceProfile.Name,
			}
		}

		response, err := t.Cloud.EC2().RunInstances(ctx, request)
		if err != nil {
			return fmt.Errorf("error creating Instance: %v", err)
		}

		e.ID = response.Instances[0].InstanceId
	}

	return t.AddAWSTags(*e.ID, e.Tags)
}

func (e *Instance) TerraformLink() *terraformWriter.Literal {
	if fi.ValueOf(e.Shared) {
		if e.ID == nil {
			klog.Fatalf("ID must be set, if NAT Instance is shared: %s", e)
		}

		return terraformWriter.LiteralFromStringValue(*e.ID)
	}

	return terraformWriter.LiteralSelfLink("aws_instance", *e.Name)
}
