// Package lambda implements the API Gateway & AWS Lambda platform.
package lambda

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/apex/log"
	"github.com/apex/log/handlers/discard"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/acm"
	"github.com/aws/aws-sdk-go/service/apigateway"
	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/aws/aws-sdk-go/service/lambda"
	"github.com/aws/aws-sdk-go/service/route53"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/dchest/uniuri"
	humanize "github.com/dustin/go-humanize"
	"github.com/golang/sync/errgroup"
	"github.com/pkg/errors"

	"github.com/apex/up"
	"github.com/apex/up/config"
	"github.com/apex/up/internal/proxy/bin"
	"github.com/apex/up/internal/shim"
	"github.com/apex/up/internal/util"
	"github.com/apex/up/internal/zip"
	"github.com/apex/up/platform/aws/domains"
	"github.com/apex/up/platform/aws/logs"
	"github.com/apex/up/platform/aws/runtime"
	"github.com/apex/up/platform/event"
	"github.com/apex/up/platform/lambda/stack"
	"github.com/apex/up/platform/lambda/stack/resources"
)

// errFirstDeploy is returned from .deploy() when a function is created.
var errFirstDeploy = errors.New("first deploy")

const (
	// maxCodeSize is the max code size supported by Lambda (250MiB).
	maxCodeSize = 250 << 20
)

// assume policy for the lambda function.
var apiGatewayAssumePolicy = `{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Principal": {
				"Service": "apigateway.amazonaws.com"
			},
			"Action": "sts:AssumeRole"
		},
		{
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
	]
}`

// TODO: aggregate progress report for N regions or distinct progress bars
// TODO: refactor with another region-scoped struct to clean this up

// Platform implementation.
type Platform struct {
	config  *up.Config
	handler string
	zip     *bytes.Buffer
	events  event.Events
}

// New platform.
func New(c *up.Config, events event.Events) *Platform {
	return &Platform{
		config:  c,
		handler: "_proxy.handle",
		events:  events,
	}
}

// Build implementation.
func (p *Platform) Build() error {
	start := time.Now()
	p.zip = new(bytes.Buffer)

	if err := p.injectProxy(); err != nil {
		return errors.Wrap(err, "injecting proxy")
	}
	defer p.removeProxy()

	r, stats, err := zip.Build(".")
	if err != nil {
		return errors.Wrap(err, "zip")
	}

	if _, err := io.Copy(p.zip, r); err != nil {
		return errors.Wrap(err, "copying")
	}

	if err := r.Close(); err != nil {
		return errors.Wrap(err, "closing")
	}

	p.events.Emit("platform.build.zip", event.Fields{
		"files":             stats.FilesAdded,
		"size_uncompressed": stats.SizeUncompressed,
		"size_compressed":   p.zip.Len(),
		"duration":          time.Since(start),
	})

	if stats.SizeUncompressed > maxCodeSize {
		size := humanize.Bytes(uint64(stats.SizeUncompressed))
		max := humanize.Bytes(uint64(maxCodeSize))
		return errors.Errorf("zip contents is %s, exceeding Lambda's limit of %s", size, max)
	}

	return nil
}

// Zip returns the zip reader.
func (p *Platform) Zip() io.Reader {
	return p.zip
}

// Init initializes the runtime.
func (p *Platform) Init(stage string) error {
	return runtime.New(
		p.config,
		runtime.WithLogger(&log.Logger{
			Handler: discard.Default,
		}),
	).Init(stage)
}

// Deploy implementation.
func (p *Platform) Deploy(d up.Deploy) error {
	regions := p.config.Regions
	var g errgroup.Group

	if err := p.createRole(); err != nil {
		return errors.Wrap(err, "iam")
	}

	for _, r := range regions {
		region := r
		g.Go(func() error {
			version, err := p.deploy(region, d)
			if err == nil {
				goto endpoint
			}

			if err != errFirstDeploy {
				return errors.Wrap(err, region)
			}

			if err := p.CreateStack(region, version); err != nil {
				return errors.Wrap(err, region)
			}

		endpoint:
			url, err := p.URL(region, d.Stage)
			if err != nil {
				return errors.Wrap(err, "fetching url")
			}

			p.events.Emit("platform.deploy.url", event.Fields{
				"url": url,
			})

			return nil
		})
	}

	return g.Wait()
}

// Logs implementation.
func (p *Platform) Logs(c up.LogsConfig) up.Logs {
	g := "/aws/lambda/" + p.config.Name
	return logs.New(g, c)
}

// Domains implementation.
func (p *Platform) Domains() up.Domains {
	return domains.New()
}

// URL returns the stage url.
func (p *Platform) URL(region, stage string) (string, error) {
	s := session.New(aws.NewConfig().WithRegion(region))
	c := apigateway.New(s)

	api, err := p.getAPI(c)
	if err != nil {
		return "", errors.Wrap(err, "fetching api")
	}

	if api == nil {
		return "", errors.Errorf("cannot find the API, looks like you haven't deployed")
	}

	id := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", *api.Id, region, stage)
	return id, nil
}

// CreateStack implementation.
func (p *Platform) CreateStack(region, version string) error {
	versions := make(resources.Versions)

	for _, s := range p.config.Stages {
		versions[s.Name] = version
	}

	if err := p.createCerts(); err != nil {
		return errors.Wrap(err, "creating certs")
	}

	zones, err := p.getHostedZone()
	if err != nil {
		return errors.Wrap(err, "fetching zones")
	}

	return stack.New(p.config, p.events, zones, region).Create(versions)
}

// DeleteStack implementation.
func (p *Platform) DeleteStack(region string, wait bool) error {
	versions := resources.Versions{}

	for _, s := range p.config.Stages {
		versions[s.Name] = "1"
	}

	if err := p.createRole(); err != nil {
		return errors.Wrap(err, "creating iam role")
	}

	log.Debug("deleting bucket objects")
	if err := p.deleteBucketObjects(region); err != nil && !util.IsNotFound(err) {
		return errors.Wrap(err, "deleting s3 objects")
	}

	log.Debug("deleting stack")
	if err := stack.New(p.config, p.events, nil, region).Delete(versions, wait); err != nil && !util.IsNotFound(err) {
		return errors.Wrap(err, "deleting stack")
	}

	log.Debug("deleting function")
	if err := p.deleteFunction(region); err != nil && !util.IsNotFound(err) {
		return errors.Wrap(err, "deleting function")
	}

	log.Debug("deleting role")
	if err := p.deleteRole(region); err != nil && !util.IsNotFound(err) {
		return errors.Wrap(err, "deleting function iam role")
	}

	return nil
}

// ShowStack implementation.
func (p *Platform) ShowStack(region string) error {
	return stack.New(p.config, p.events, nil, region).Show()
}

// PlanStack implementation.
func (p *Platform) PlanStack(region string) error {
	versions, err := p.getAliasVersions(region)
	if err != nil {
		return errors.Wrap(err, "fetching alias versions")
	}

	if err := p.createCerts(); err != nil {
		return errors.Wrap(err, "creating certs")
	}

	zones, err := p.getHostedZone()
	if err != nil {
		return errors.Wrap(err, "fetching zones")
	}

	return stack.New(p.config, p.events, zones, region).Plan(versions)
}

// ApplyStack implementation.
func (p *Platform) ApplyStack(region string) error {
	if err := p.createCerts(); err != nil {
		return errors.Wrap(err, "creating certs")
	}

	return stack.New(p.config, p.events, nil, region).Apply()
}

// Exists implementation.
func (p *Platform) Exists(region string) (bool, error) {
	log.Debug("checking if application exists")
	c := lambda.New(session.New(aws.NewConfig().WithRegion(region)))

	_, err := c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{
		FunctionName: &p.config.Name,
	})

	if util.IsNotFound(err) {
		return false, nil
	}

	if err != nil {
		return false, err
	}

	return true, nil
}

// getAliasVersions returns the function alias versions.
func (p *Platform) getAliasVersions(region string) (resources.Versions, error) {
	var g errgroup.Group
	var mu sync.Mutex

	c := lambda.New(session.New(aws.NewConfig().WithRegion(region)))
	versions := make(resources.Versions)

	log.Debug("fetching aliases")
	for _, s := range p.config.Stages {
		s := s

		g.Go(func() error {
			log.Debugf("fetching %s alias", s.Name)
			version, err := p.getAliasVersion(c, s.Name)

			if util.IsNotFound(err) {
				log.Debugf("%s has no alias, defaulting to staging", s.Name)
				version, err = p.getAliasVersion(c, "staging")
				if err != nil {
					return errors.Wrap(err, "fetching staging alias")
				}
			}

			if err != nil {
				return errors.Wrapf(err, "fetching %q alias", s.Name)
			}

			log.Debugf("fetched %s alias (%s)", s.Name, version)
			mu.Lock()
			versions[s.Name] = version
			mu.Unlock()

			return nil
		})
	}

	return versions, g.Wait()
}

// getAliasVersion retruns the alias version for a stage.
func (p *Platform) getAliasVersion(c *lambda.Lambda, stage string) (string, error) {
	res, err := c.GetAlias(&lambda.GetAliasInput{
		FunctionName: &p.config.Name,
		Name:         &stage,
	})

	if err != nil {
		return "", err
	}

	return *res.FunctionVersion, nil
}

// getHostedZone returns existing hosted zones.
func (p *Platform) getHostedZone() (zones []*route53.HostedZone, err error) {
	r := route53.New(session.New(aws.NewConfig()))

	log.Debug("fetching hosted zones")
	res, err := r.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{
		MaxItems: aws.String("100"),
	})

	if err != nil {
		return
	}

	zones = res.HostedZones
	return
}

// createCerts creates the certificates if necessary.
//
// We perform this task outside of CloudFormation because
// the certificates currently must be created in the us-east-1
// region. This also gives us a chance to let the user know
// that they have to confirm an email.
func (p *Platform) createCerts() error {
	s := session.New(aws.NewConfig().WithRegion("us-east-1"))
	a := acm.New(s)
	var domains []string

	// existing certs
	log.Debug("fetching existing certs")
	certs, err := getCerts(a)
	if err != nil {
		return errors.Wrap(err, "fetching certs")
	}

	// request certs
	for _, s := range p.config.Stages.List() {
		if s.Domain == "" {
			continue
		}

		certDomains := util.CertDomainNames(s.Domain)

		// see if the cert exists
		log.Debugf("looking up cert for %s", s.Domain)
		arn := getCert(certs, s.Domain)
		if arn != "" {
			log.Debugf("found cert for %s: %s", s.Domain, arn)
			s.Cert = arn
			continue
		}

		option := acm.DomainValidationOption{
			DomainName:       aws.String(certDomains[0]),
			ValidationDomain: aws.String(util.Domain(s.Domain)),
		}

		options := []*acm.DomainValidationOption{
			&option,
		}

		// request the cert
		res, err := a.RequestCertificate(&acm.RequestCertificateInput{
			DomainName:              aws.String(certDomains[0]),
			DomainValidationOptions: options,
			SubjectAlternativeNames: aws.StringSlice(certDomains[1:]),
		})

		if err != nil {
			return errors.Wrapf(err, "requesting cert for %v", certDomains)
		}

		domains = append(domains, certDomains[0])
		s.Cert = *res.CertificateArn
	}

	// no certs needed
	if len(domains) == 0 {
		return nil
	}

	defer p.events.Time("platform.certs.create", event.Fields{
		"domains": domains,
	})()

	// wait for approval
	for range time.Tick(4 * time.Second) {
		res, err := a.ListCertificates(&acm.ListCertificatesInput{
			MaxItems:            aws.Int64(1000),
			CertificateStatuses: aws.StringSlice([]string{acm.CertificateStatusPendingValidation}),
		})

		if err != nil {
			return errors.Wrap(err, "listing")
		}

		if len(res.CertificateSummaryList) == 0 {
			break
		}
	}

	return nil
}

// deploy to the given region.
func (p *Platform) deploy(region string, d up.Deploy) (version string, err error) {
	start := time.Now()

	fields := event.Fields{
		"commit": d.Commit,
		"stage":  d.Stage,
		"region": region,
	}

	p.events.Emit("platform.deploy", fields)

	defer func() {
		fields["duration"] = time.Since(start)
		fields["commit"] = d.Commit
		fields["version"] = version
		p.events.Emit("platform.deploy.complete", fields)
	}()

	ctx := log.WithField("region", region)
	s := session.New(aws.NewConfig().WithRegion(region))
	u := s3manager.NewUploaderWithClient(s3.New(s))
	a := apigateway.New(s)
	c := lambda.New(s)

	ctx.Debug("fetching function config")
	_, err = c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{
		FunctionName: &p.config.Name,
	})

	if util.IsNotFound(err) {
		defer p.events.Time("platform.function.create", fields)
		return p.createFunction(c, a, u, region, d)
	}

	if err != nil {
		return "", errors.Wrap(err, "fetching function config")
	}

	defer p.events.Time("platform.function.update", fields)
	return p.updateFunction(c, a, u, region, d)
}

// createFunction creates the function.
func (p *Platform) createFunction(c *lambda.Lambda, a *apigateway.APIGateway, up *s3manager.Uploader, region string, d up.Deploy) (version string, err error) {
	// ensure bucket exists
	if err := p.createBucket(region); err != nil && !util.IsBucketExists(err) {
		return "", errors.Wrap(err, "creating s3 bucket")
	}

	// upload to s3
	b := aws.String(p.getS3BucketName(region))
	k := aws.String(p.getS3Key(d.Stage))

	log.Debugf("uploading function to bucket %s key %s", *b, *k)
	_, err = up.Upload(&s3manager.UploadInput{
		Bucket:               b,
		Key:                  k,
		Body:                 bytes.NewReader(p.zip.Bytes()),
		ServerSideEncryption: aws.String("aws:kms"),
	})

	if err != nil {
		return "", errors.Wrap(err, "uploading function")
	}

	// load environment
	env, err := p.loadEnvironment(d)
	if err != nil {
		return "", errors.Wrap(err, "loading environment variables")
	}

	// create function
retry:
	log.Debug("creating function")
	res, err := c.CreateFunction(&lambda.CreateFunctionInput{
		FunctionName: &p.config.Name,
		Handler:      &p.handler,
		Runtime:      &p.config.Lambda.Runtime,
		Role:         &p.config.Lambda.Role,
		MemorySize:   aws.Int64(int64(p.config.Lambda.Memory)),
		Timeout:      aws.Int64(int64(p.config.Lambda.Timeout)),
		Publish:      aws.Bool(true),
		Environment:  env,
		Code: &lambda.FunctionCode{
			S3Bucket: b,
			S3Key:    k,
		},
		VpcConfig: p.vpc(),
	})

	// IAM is eventually consistent apparently, so we have to keep retrying
	if isCreatingRole(err) {
		log.Debug("waiting for role to be created")
		time.Sleep(500 * time.Millisecond)
		goto retry
	}

	if err != nil {
		return "", errors.Wrap(err, "creating function")
	}

	return *res.Version, errFirstDeploy
}

// updateFunction updates the function.
func (p *Platform) updateFunction(c *lambda.Lambda, a *apigateway.APIGateway, up *s3manager.Uploader, region string, d up.Deploy) (version string, err error) {
	b := aws.String(p.getS3BucketName(region))
	k := aws.String(p.getS3Key(d.Stage))

	// upload
	log.Debugf("uploading function to bucket %s key %s", *b, *k)
	_, err = up.Upload(&s3manager.UploadInput{
		Bucket:               b,
		Key:                  k,
		Body:                 bytes.NewReader(p.zip.Bytes()),
		ServerSideEncryption: aws.String("aws:kms"),
	})

	// ensure bucket exists
	if util.IsNotFound(err) {
		if err := p.createBucket(region); err != nil {
			return "", errors.Wrap(err, "creating s3 bucket")
		}
		err = nil
	}

	if err != nil {
		return "", errors.Wrap(err, "uploading function")
	}

	// load environment
	env, err := p.loadEnvironment(d)
	if err != nil {
		return "", errors.Wrap(err, "loading environment variables")
	}

	// update function config
	log.Debug("updating function")
	if err := p.isPending(c); err != nil {
		return "", err
	}
	_, err = c.UpdateFunctionConfiguration(&lambda.UpdateFunctionConfigurationInput{
		FunctionName: &p.config.Name,
		Handler:      &p.handler,
		Runtime:      &p.config.Lambda.Runtime,
		Role:         &p.config.Lambda.Role,
		MemorySize:   aws.Int64(int64(p.config.Lambda.Memory)),
		Timeout:      aws.Int64(int64(p.config.Lambda.Timeout)),
		Environment:  env,
		VpcConfig:    p.vpc(),
	})

	if err != nil {
		return "", errors.Wrap(err, "updating function config")
	}

	// update function code
	log.Debug("updating function code")
	if err := p.isPending(c); err != nil {
		return "", err
	}
	res, err := c.UpdateFunctionCode(&lambda.UpdateFunctionCodeInput{
		FunctionName: &p.config.Name,
		Publish:      aws.Bool(true),
		S3Bucket:     b,
		S3Key:        k,
	})

	if err != nil {
		return "", errors.Wrap(err, "updating function code")
	}

	// create stage alias
	if err := p.alias(c, d.Stage, *res.Version); err != nil {
		return "", errors.Wrapf(err, "creating function stage %q alias", d.Stage)
	}

	// create git alias
	if d.Commit != "" {
		if err := p.alias(c, util.EncodeAlias(d.Commit), *res.Version); err != nil {
			return "", errors.Wrapf(err, "creating function git %q alias", d.Commit)
		}
	}

	return *res.Version, nil
}

// isPending implementation.
func (p *Platform) isPending(c *lambda.Lambda) error {
	var attempt int
	maxAttempts := 30       // TODO: ideally max attempts is configurable
	wait := time.Second * 5 // TODO: ideally some backoff

retry:
	attempt++

	log.Debugf("checking if function is pending (attempt %d of %d)", attempt, maxAttempts)
	conf, err := c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{
		FunctionName: &p.config.Name,
	})
	if err != nil {
		return errors.Wrapf(err, "getting function config")
	}

	if *conf.State == "Active" && *conf.LastUpdateStatus != "InProgress" {
		log.Debugf("function is in state %q / %q", *conf.State, *conf.LastUpdateStatus)
		return nil
	}

	if attempt >= maxAttempts {
		log.Debugf("max attempts exceeded")
		return errors.Errorf("function is stuck in the state %q / %q", *conf.State, *conf.LastUpdateStatus)
	}

	log.Debugf("function is in state %q / %q, trying again in %s", *conf.State, *conf.LastUpdateStatus, wait)
	time.Sleep(wait)
	goto retry
}

// vpc returns the vpc configuration or nil.
func (p *Platform) vpc() *lambda.VpcConfig {
	v := p.config.Lambda.VPC
	if v == nil {
		return nil
	}

	return &lambda.VpcConfig{
		SubnetIds:        aws.StringSlice(v.Subnets),
		SecurityGroupIds: aws.StringSlice(v.SecurityGroups),
	}
}

// alias creates or updates an alias.
func (p *Platform) alias(c *lambda.Lambda, alias, version string) error {
	log.Debugf("alias %s to %s", alias, version)
	_, err := c.UpdateAlias(&lambda.UpdateAliasInput{
		FunctionName:    &p.config.Name,
		FunctionVersion: &version,
		Name:            &alias,
		Description:     aws.String(util.ManagedByUp("")),
	})

	if util.IsNotFound(err) {
		_, err = c.CreateAlias(&lambda.CreateAliasInput{
			FunctionName:    &p.config.Name,
			FunctionVersion: &version,
			Name:            &alias,
			Description:     aws.String(util.ManagedByUp("")),
		})
	}

	return err
}

// deleteFunction deletes the lambda function.
func (p *Platform) deleteFunction(region string) error {
	// TODO: sessions all over... refactor
	c := lambda.New(session.New(aws.NewConfig().WithRegion(region)))

	_, err := c.DeleteFunction(&lambda.DeleteFunctionInput{
		FunctionName: &p.config.Name,
	})

	return err
}

// loadEnvironment loads environment variables.
func (p *Platform) loadEnvironment(d up.Deploy) (*lambda.Environment, error) {
	m := aws.StringMap(p.config.Environment)
	m["UP_STAGE"] = &d.Stage
	m["UP_COMMIT"] = &d.Commit
	m["UP_AUTHOR"] = &d.Author
	return &lambda.Environment{
		Variables: m,
	}, nil
}

// createRole creates the IAM role unless it is present.
func (p *Platform) createRole() error {
	s := session.New(aws.NewConfig())
	c := iam.New(s)

	name := p.roleName()
	desc := util.ManagedByUp("")

	// role is provided
	if s := p.config.Lambda.Role; s != "" {
		log.Debugf("using role from config %s", s)
		return nil
	}

	log.Debug("checking for role")
	existing, err := c.GetRole(&iam.GetRoleInput{
		RoleName: &name,
	})

	// network or permission error
	if err != nil && !util.IsNotFound(err) {
		return errors.Wrap(err, "fetching role")
	}

	// use the existing role
	if err == nil {
		log.Debug("found existing role")

		if err := p.updateRole(c); err != nil {
			return errors.Wrap(err, "updating role policy")
		}

		p.setRoleARN(*existing.Role.Arn)
		return nil
	}

	log.Debug("creating role")
	role, err := c.CreateRole(&iam.CreateRoleInput{
		RoleName:                 &name,
		Description:              &desc,
		AssumeRolePolicyDocument: &apiGatewayAssumePolicy,
	})

	if err != nil {
		return errors.Wrap(err, "creating role")
	}

	if err := p.updateRole(c); err != nil {
		return errors.Wrap(err, "updating role policy")
	}

	p.setRoleARN(*role.Role.Arn)

	return nil
}

// updateRole updates the IAM role.
func (p *Platform) updateRole(c *iam.IAM) error {
	name := p.roleName()

	policy, err := p.functionPolicy()
	if err != nil {
		return errors.Wrap(err, "creating function policy")
	}

	log.Debug("updating role policy")
	_, err = c.PutRolePolicy(&iam.PutRolePolicyInput{
		PolicyName:     &name,
		RoleName:       &name,
		PolicyDocument: &policy,
	})

	return err
}

// setRoleARN sets the role ARN.
func (p *Platform) setRoleARN(arn string) {
	log.Debugf("set role to %s", arn)
	p.config.Lambda.Role = arn
}

// roleName returns the IAM role name.
func (p *Platform) roleName() string {
	return fmt.Sprintf("%s-function", p.config.Name)
}

// deleteRole deletes the role and policy.
func (p *Platform) deleteRole(region string) error {
	name := fmt.Sprintf("%s-function", p.config.Name)
	c := iam.New(session.New(aws.NewConfig().WithRegion(region)))

	// role is provided
	if s := p.config.Lambda.Role; s != "" {
		log.Debugf("using role from config %s; not deleting", s)
		return nil
	}

	_, err := c.DeleteRolePolicy(&iam.DeleteRolePolicyInput{
		RoleName:   &name,
		PolicyName: &name,
	})

	if err != nil {
		return errors.Wrap(err, "deleting policy")
	}

	_, err = c.DeleteRole(&iam.DeleteRoleInput{
		RoleName: &name,
	})

	if err != nil {
		return errors.Wrap(err, "deleting iam role")
	}

	return nil
}

// createBucket creates the bucket.
func (p *Platform) createBucket(region string) error {
	s := s3.New(session.New(aws.NewConfig().WithRegion(region)))
	n := p.getS3BucketName(region)

	log.WithField("name", n).Debug("creating s3 bucket")
	_, err := s.CreateBucket(&s3.CreateBucketInput{
		Bucket: &n,
	})

	return err
}

// deleteBucketObjects deletes the objects for the app.
func (p *Platform) deleteBucketObjects(region string) error {
	s := s3.New(session.New(aws.NewConfig().WithRegion(region)))
	b := aws.String(p.getS3BucketName(region))
	prefix := p.config.Name + "/"

	params := &s3.ListObjectsInput{
		Bucket: b,
		Prefix: &prefix,
	}

	return s.ListObjectsPages(params, func(page *s3.ListObjectsOutput, lastPage bool) bool {
		for _, c := range page.Contents {
			ctx := log.WithField("key", *c.Key)

			ctx.Debug("deleting object")
			_, err := s.DeleteObject(&s3.DeleteObjectInput{
				Bucket: b,
				Key:    c.Key,
			})

			if err != nil {
				ctx.WithError(err).Warn("deleting object")
			}
		}

		return *page.IsTruncated
	})
}

// getAPI returns the API if present or nil.
func (p *Platform) getAPI(c *apigateway.APIGateway) (api *apigateway.RestApi, err error) {
	name := p.config.Name

	res, err := c.GetRestApis(&apigateway.GetRestApisInput{
		Limit: aws.Int64(500),
	})

	if err != nil {
		return nil, errors.Wrap(err, "fetching apis")
	}

	for _, a := range res.Items {
		if *a.Name == name {
			api = a
		}
	}

	return
}

// injectProxy injects the Go proxy.
func (p *Platform) injectProxy() error {
	log.Debugf("injecting proxy")

	if err := ioutil.WriteFile("main", bin.MustAsset("up-proxy"), 0777); err != nil {
		return errors.Wrap(err, "writing up-proxy")
	}

	if err := ioutil.WriteFile("_proxy.js", shim.MustAsset("index.js"), 0755); err != nil {
		return errors.Wrap(err, "writing _proxy.js")
	}

	return nil
}

// removeProxy removes the Go proxy.
func (p *Platform) removeProxy() error {
	log.Debugf("removing proxy")
	os.Remove("main")
	os.Remove("_proxy.js")
	return nil
}

// getS3Key returns a randomized s3 key.
func (p *Platform) getS3Key(stage string) string {
	ts := time.Now().Unix()
	uid := uniuri.New()
	return fmt.Sprintf("%s/%s/%d-%s.zip", p.config.Name, stage, ts, uid)
}

// getS3BucketName returns the s3 bucket name.
func (p *Platform) getS3BucketName(region string) string {
	return fmt.Sprintf("up-%s-%s", p.getAccountID(), region)
}

// getAccountID returns the AWS account id derived from Lambda role,
// which is currently always present, implicitly or explicitly.
func (p *Platform) getAccountID() string {
	return strings.Split(p.config.Lambda.Role, ":")[4]
}

// functionPolicy returns the IAM function role policy.
func (p *Platform) functionPolicy() (string, error) {
	policy := struct {
		Version   string
		Statement []config.IAMPolicyStatement
	}{
		Version:   "2012-10-17",
		Statement: p.config.Lambda.Policy,
	}

	b, err := json.MarshalIndent(policy, "", "  ")
	if err != nil {
		return "", err
	}

	return string(b), nil
}

// isCreatingRole returns true if the role has not been created.
func isCreatingRole(err error) bool {
	return err != nil && strings.Contains(err.Error(), "role defined for the function cannot be assumed by Lambda")
}

// getCerts returns the certificates available.
func getCerts(a *acm.ACM) (certs []*acm.CertificateDetail, err error) {
	var g errgroup.Group
	var mu sync.Mutex

	res, err := a.ListCertificates(&acm.ListCertificatesInput{
		MaxItems: aws.Int64(1000),
	})

	if err != nil {
		return nil, errors.Wrap(err, "listing")
	}

	for _, c := range res.CertificateSummaryList {
		c := c
		g.Go(func() error {
			res, err := a.DescribeCertificate(&acm.DescribeCertificateInput{
				CertificateArn: c.CertificateArn,
			})

			if err != nil {
				return errors.Wrap(err, "describing")
			}

			mu.Lock()
			certs = append(certs, res.Certificate)
			mu.Unlock()
			return nil
		})
	}

	err = g.Wait()
	return
}

// getCert returns the ARN of a certificate with can satisfy domain,
// favoring more specific certificates, then falling back on wildcards.
func getCert(certs []*acm.CertificateDetail, domain string) string {
	// exact domain
	for _, c := range certs {
		if *c.DomainName == domain {
			return *c.CertificateArn
		}
	}

	// exact alt
	for _, c := range certs {
		for _, a := range c.SubjectAlternativeNames {
			if *a == domain {
				return *c.CertificateArn
			}
		}
	}

	// wildcards
	for _, c := range certs {
		if util.WildcardMatches(*c.DomainName, domain) {
			return *c.CertificateArn
		}

		for _, a := range c.SubjectAlternativeNames {
			if util.WildcardMatches(*a, domain) {
				return *c.CertificateArn
			}
		}
	}

	return ""
}
