// Copyright 2015-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 route53 contains functions for working with the route53 APIs
// that back ECS Service Discovery
package route53

import (
	"fmt"
	"time"

	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/config"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/route53"
	"github.com/aws/aws-sdk-go/service/servicediscovery"
)

const (
	waitUntilSDSDeletableWaitTime = 2 * time.Second
	waitUntilSDSDeletableRetries  = 5
)

// FindPrivateNamespaceFunc is the interface/signature for FindPrivateNamespace
// This helps when writing code in other packages that need to mock this function
type FindPrivateNamespaceFunc func(name, vpc string, config *config.CommandConfig) (*string, error)

// FindPublicNamespaceFunc is the interface/signature for FindPublicNamespace
// This helps when writing code in other packages that need to mock this function
type FindPublicNamespaceFunc func(name string, config *config.CommandConfig) (*string, error)

// WaitUntilSDSDeletableFunc is the interface/signature for WaitUntilSDSDeletable
// This helps when writing code in other packages that need to mock this function
type WaitUntilSDSDeletableFunc func(id string, config *config.CommandConfig) error

// FindPrivateNamespace returns the ID(s) of the private namespace with the given name and vpc
func FindPrivateNamespace(name, vpc string, config *config.CommandConfig) (*string, error) {
	r53Client := newRoute53Client(config)
	sdClient := newSDClient(config)
	return findPrivateNamespace(name, vpc, config.Region(), r53Client, sdClient)
}

// private function findPrivateNamespace can accept mock client objects, allowing it to be unit tested
func findPrivateNamespace(name, vpc, region string, r53Client route53Client, sdClient serviceDiscoveryClient) (*string, error) {
	var nameMatch []*string

	err := listNamespaces(false, sdClient, func(namespace *servicediscovery.NamespaceSummary) bool {
		if aws.StringValue(namespace.Name) == name {
			nameMatch = append(nameMatch, namespace.Id)
		}
		return true
	})
	if err != nil {
		return nil, err
	}

	for _, namespaceID := range nameMatch {
		hasVPC, err := checkNamespaceVPC(namespaceID, vpc, region, r53Client, sdClient)
		if err != nil {
			return nil, err
		}
		if hasVPC {
			return namespaceID, nil
		}
	}

	return nil, nil
}

// FindPublicNamespace returns the ID of the public namespace with the given name
func FindPublicNamespace(name string, config *config.CommandConfig) (*string, error) {
	sdClient := newSDClient(config)
	return findPublicNamespace(name, sdClient)
}

// private function findPublicNamespace can accept mock client objects, allowing it to be unit tested
func findPublicNamespace(name string, sdClient serviceDiscoveryClient) (*string, error) {
	var namespace *string
	err := listNamespaces(true, sdClient, func(n *servicediscovery.NamespaceSummary) bool {
		if aws.StringValue(n.Name) == name {
			namespace = n.Id
			return false // we found it, stop the list call
		}
		return true
	})

	return namespace, err
}

// WaitUntilSDSDeletable waits at most 10 seconds to see if the SDS no longer
// has any instances (ECS Tasks) using it
func WaitUntilSDSDeletable(id string, config *config.CommandConfig) error {
	sdClient := newSDClient(config)
	return waitUntilSDSDeletable(id, sdClient, waitUntilSDSDeletableRetries)
}

func waitUntilSDSDeletable(id string, sdClient serviceDiscoveryClient, maxRetries int) error {
	input := &servicediscovery.GetServiceInput{
		Id: aws.String(id),
	}
	var sdsInstanceCount int64 = 0
	for retryCount := 0; retryCount < maxRetries; retryCount++ {
		sds, err := sdClient.GetService(input)
		if err != nil {
			return err
		}
		sdsInstanceCount = aws.Int64Value(sds.Service.InstanceCount)
		if sdsInstanceCount == 0 {
			return nil // SDS is no longer in use, it can be deleted
		}
		time.Sleep(waitUntilSDSDeletableWaitTime)
	}
	return fmt.Errorf("Service Discovery Service can not be deleted. It is still in use. InstanceCount=%d", sdsInstanceCount)
}

func checkNamespaceVPC(namespaceID *string, vpc string, region string, r53Client route53Client, sdClient serviceDiscoveryClient) (bool, error) {
	namespaceInfo, err := sdClient.GetNamespace(&servicediscovery.GetNamespaceInput{
		Id: namespaceID,
	})
	if err != nil {
		return false, err
	}
	hostedZoneID := namespaceInfo.Namespace.Properties.DnsProperties.HostedZoneId
	hostedZone, err := r53Client.GetHostedZone(&route53.GetHostedZoneInput{
		Id: hostedZoneID,
	})
	if err != nil {
		return false, err
	}
	for _, hostedZoneVPC := range hostedZone.VPCs {
		// The VPC must be in the region that we will be launching the ECS Service
		if (aws.StringValue(hostedZoneVPC.VPCId) == vpc) && (aws.StringValue(hostedZoneVPC.VPCRegion) == region) {
			return true, nil
		}
	}

	return false, nil
}

// Private ServiceDiscovery Client that can be mocked in unit tests
// The SDK's servicediscovery client implements this interface
type serviceDiscoveryClient interface {
	ListNamespacesPages(input *servicediscovery.ListNamespacesInput, fn func(*servicediscovery.ListNamespacesOutput, bool) bool) error
	GetNamespace(input *servicediscovery.GetNamespaceInput) (*servicediscovery.GetNamespaceOutput, error)
	GetService(input *servicediscovery.GetServiceInput) (*servicediscovery.GetServiceOutput, error)
}

// factory function to create clients
func newSDClient(config *config.CommandConfig) serviceDiscoveryClient {
	sdClient := servicediscovery.New(config.Session)
	sdClient.Handlers.Build.PushBackNamed(clients.CustomUserAgentHandler())
	return sdClient
}

// Lists namespaces, calling 'fn' on each namespace returned.
// To stop iterating over namespaces, return false from 'fn'
func listNamespaces(isPublic bool, client serviceDiscoveryClient, fn func(*servicediscovery.NamespaceSummary) bool) error {
	typeFilter := servicediscovery.NamespaceTypeDnsPrivate
	if isPublic {
		typeFilter = servicediscovery.NamespaceTypeDnsPublic
	}
	request := &servicediscovery.ListNamespacesInput{
		Filters: []*servicediscovery.NamespaceFilter{
			&servicediscovery.NamespaceFilter{
				Condition: aws.String(servicediscovery.FilterConditionEq),
				Name:      aws.String(servicediscovery.NamespaceFilterNameType),
				Values:    aws.StringSlice([]string{typeFilter}),
			},
		},
	}
	err := client.ListNamespacesPages(request,
		func(page *servicediscovery.ListNamespacesOutput, lastPage bool) bool {
			for _, namespace := range page.Namespaces {
				if !fn(namespace) {
					return false
				}
			}
			return !lastPage
		})
	return err
}
