// Package vpccreator simplifies the process for creating a terraform plan using
// the Systems Infrastructure account_terraform module. This module creates
// a Twitch-specific VPC in an AWS account. To use it, one must provide some basic
// data like: account name, environment, region, network address block (CIDR).
// This library provides a few small procedures to generate the tf plan file from
// a buil-in template.
// Account Template: https://git-aws.internal.justin.tv/terraform-modules/account_template
package vpccreator

import (
	"fmt"
	"os"
	"regexp"
	"strings"
	"text/template"
)

// VPC represents the data needed to create a VPC in an AWS account.
type VPC struct {
	AccountName string
	SysEnv      string
	Region      string
	CIDR        string
	Version     string
	Options     map[string]string
}

// These variables represent the baked-in defaults and supported strings.
var (
	// SupportedRegions is the list of regions in which Systems maintains DNS servers.
	SupportedRegions = []string{"us-west-2", "ap-southwest-1", "us-east-1", "eu-west-1", "us-east-2"}
	// DefaultRegion is used if no region is provided.
	DefaultRegion = "us-west-2"
	// SupportedSysEnvs is the list of environments in which Systems maintains DNS servers.
	SupportedSysEnvs = []string{"nonprod", "prod", ""}
	// DefaultVersion is the current version of the account_template module.
	// This is used if no version is provided.
	DefaultVersion = "v3.1.4"
	// AccountTemplate Is the path (http or git) to the account_template github repo.
	AccountTemplate = "git-aws.internal.justin.tv/terraform-modules/account_template"
)

// Template contains the raw template for our terraform usage. The output data is based on this.
var Template = `# Autogenerated by https://code.justin.tv/terraform-modules/account_template/vpccreator
#
# Using terraform 0.11.14, run this like so:
# terraform init ; terraform plan
#
# Credentials must be available for account '{{.AccountName}}'
#
terraform {
  backend "s3" {
    # This bucket must be created manually before this plan will run!
    # You may also change this bucket name to one that already exists.
    bucket  = "{{ Replace .AccountName "-" }}-{{ .Region }}"
    key     = "tfstate/account_template/{{ .Region }}/terraform.tfstate"
    region  = "{{ .Region }}"
    profile = "{{ .AccountName }}"
  }
}

provider "aws" {
  profile = "{{ .AccountName }}"
  alias   = "{{ .AccountName }}-{{ .Region }}"
  region  = "{{ .Region }}"
}

module "{{ .Region }}" {
{{- range $key, $value := .Options }}
  {{ Format $key "12" }} = {{ $value }}{{ end }}

  # Set additional resource tags by uncommenting this line, and adding tags.
  # tags = "${map("Environment","prod/dev")}"

  providers {
    aws = "aws.{{ .AccountName }}-{{ .Region }}"
  }
}

output "{{ Replace .Region "_" }}_private_subnets" {
  value = "${module.{{ .Region }}.private_subnets}"
}

output "{{ Replace .Region "_" }}_public_subnets" {
  value = "${module.{{ .Region }}.public_subnets}"
}

output "{{ Replace .Region "_" }}_twitch_subnets_sg_id" {
  value = "${module.{{ .Region }}.twitch_subnets_sg_id}"
}

output "{{ Replace .Region "_" }}_vpc_id" {
  value = "${module.{{ .Region }}.vpc_id}"
}

output "{{ Replace .Region "_" }}_azset" {
  value = "${module.{{ .Region }}.azset}"
}
`

// SetOptions fills in the data struct with valid data, then returns it.
func (v *VPC) SetOptions() *VPC {
	v.AccountName = strings.TrimSuffix(strings.ToLower(v.AccountName), "@amazon.com")
	if v.Region = strings.ToLower(v.Region); v.Region == "" {
		v.Region = DefaultRegion
	}
	if v.Version = strings.ToLower(v.Version); v.Version == "" {
		v.Version = DefaultVersion
	}
	if v.Options == nil {
		v.Options = make(map[string]string)
	}
	if v.SysEnv != "" {
		v.Options["systems_peer"] = v.SysEnv
	}
	// The 'D' availability zone in us-west-2 is useless so avoid it.
	if v.Region == "us-west-2" {
		v.Options["azset"] = `["a","b","c"]`
	}

	v.Options["source"] = `"git::git+ssh://git@` + AccountTemplate + ".git?ref=" + v.Version + `"`
	v.Options["owner"] = `"` + v.AccountName + `@amazon.com"`
	v.Options["vpc_cidr"] = `"` + v.CIDR + `"`
	v.Options["name"] = `"` + v.AccountName + `"`
	v.Options["enable_vgw"] = `"false"  # set true if using direct-connects`
	return v // so it can be chained.
}

// Validate returns an error if anything is invalid in the VPC information.
func (v *VPC) Validate() error {
	regexCIDR := regexp.MustCompile(`^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$`) // 10.200.200.0/22
	regexBuck := regexp.MustCompile(`((^|\.)[a-z0-9][a-z-]*)+`)
	bucketName := Replace(v.AccountName, "-")

	switch {
	case !StringInSlice(v.Region, SupportedRegions):
		return fmt.Errorf("invalid: %s, choose a supported region: %s", v.Region, strings.Join(SupportedRegions, ", "))
	case !StringInSlice(v.SysEnv, SupportedSysEnvs):
		return fmt.Errorf("invalid: %s, choose a supported environment: %s", v.SysEnv, strings.Join(SupportedSysEnvs, ", "))
	case !strings.Contains(v.AccountName, "-"):
		return fmt.Errorf("invalid: %s, provide account name, account name must have at least 1 dash", v.AccountName)
	case !regexBuck.MatchString(bucketName) || len(bucketName) > 63:
		return fmt.Errorf("invalid: %s, unable to create usable bucket from account name; account name may be invalid", v.AccountName)
	case !regexCIDR.MatchString(v.CIDR):
		return fmt.Errorf("invalid: %s, provide account CIDR in format: a.b.c.d/x where x is the bitmask", v.CIDR)
	}
	return nil
}

// Render prints the generated template to stdout.
func (v *VPC) Render() error {
	return v.RenderOutput(os.Stdout)
}

// RenderOutput writes the generated template to a provided file handler.
func (v *VPC) RenderOutput(output *os.File) error {
	funcMap := template.FuncMap{"Replace": Replace, "Format": FormatOptions}
	tmpl, err := template.New(v.AccountName + v.Region).Funcs(funcMap).Parse(Template)
	if err != nil {
		return err
	}
	return tmpl.Execute(output, v)
}

// Replace converts + and - to another character (usually _ or -). Used in template.
func Replace(in string, new string) string {
	return strings.NewReplacer("+", new, "-", new).Replace(in)
}

// FormatOptions prints options with white space appended.
func FormatOptions(in string, count string) string {
	return fmt.Sprintf("%-"+count+"s", in)
}

// StringInSlice returns true if a string is in a slice.
func StringInSlice(str string, slice []string) bool {
	for _, s := range slice {
		if s == str {
			return true
		}
	}
	return false
}
