package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
)

type counterSorter struct {
	resources []*Resource
}

func (cs counterSorter) Len() int {
	return len(cs.resources)
}

func (cs counterSorter) Swap(i, j int) {
	cs.resources[i], cs.resources[j] = cs.resources[j], cs.resources[i]
}

func (cs counterSorter) Less(i, j int) bool {
	return cs.resources[i].counterNumeric < cs.resources[j].counterNumeric || (cs.resources[i].counterNumeric == cs.resources[j].counterNumeric && cs.resources[i].counterStr < cs.resources[j].counterStr)
}

type allGroup struct {
	Hosts []string               `json:"hosts"`
	Vars  map[string]interface{} `json:"vars"`
}

func appendUniq(strs []string, item string) []string {
	if len(strs) == 0 {
		strs = append(strs, item)
		return strs
	}
	sort.Strings(strs)
	i := sort.SearchStrings(strs, item)
	if i == len(strs) || (i < len(strs) && strs[i] != item) {
		strs = append(strs, item)
	}
	return strs
}

func gatherResources(s *stateAnyTerraformVersion) map[string]interface{} {
	if s.TerraformVersion == TerraformVersionPre0dot12 {
		return gatherResourcesPre0dot12(&s.StatePre0dot12)
	} else if s.TerraformVersion == TerraformVersion0dot12 {
		return gatherResources0dot12(&s.State0dot12)
	} else {
		panic("Unimplemented Terraform version enum")
	}
}

func gatherResourcesPre0dot12(s *state) map[string]interface{} {
	outputGroups := make(map[string]interface{})

	all := &allGroup{Hosts: make([]string, 0), Vars: make(map[string]interface{})}
	types := make(map[string][]string)
	individual := make(map[string][]string)
	ordered := make(map[string][]string)
	tags := make(map[string][]string)

	unsortedOrdered := make(map[string][]*Resource)

	resourceIDNames := s.mapResourceIDNames()
	for _, res := range s.resources() {
		// place in list of all resources
		all.Hosts = appendUniq(all.Hosts, res.Hostname())

		// place in list of resource types
		tp := fmt.Sprintf("type_%s", res.resourceType)
		types[tp] = appendUniq(types[tp], res.Hostname())

		unsortedOrdered[res.baseName] = append(unsortedOrdered[res.baseName], res)

		// store as individual host (e.g. <name>_<count>)
		var invdName string
		if res.counterStr != "" {
			invdName = fmt.Sprintf("%s_%s", res.baseName, strings.Replace(res.counterStr, ".", "_", -1))
		} else {
			invdName = fmt.Sprintf("%s_%d", res.baseName, res.counterNumeric)
		}
		if old, exists := individual[invdName]; exists {
			fmt.Fprintf(os.Stderr, "overwriting already existing individual key %s, old: %v, new: %v\n", invdName, old, res.Hostname())
		}
		individual[invdName] = []string{res.Hostname()}

		// inventorize tags
		for k, v := range res.Tags() {
			// Valueless
			tag := k
			if v != "" {
				tag = fmt.Sprintf("%s_%s", k, v)
			}
			// if v is a resource ID, then tag should be resource name
			if _, exists := resourceIDNames[v]; exists {
				tag = resourceIDNames[v]
			}
			tags[tag] = appendUniq(tags[tag], res.Hostname())
		}
	}

	// inventorize outputs as variables
	if len(s.outputs()) > 0 {
		for _, out := range s.outputs() {
			all.Vars[out.keyName] = out.value
		}
	}

	// sort the ordered groups
	for basename, resources := range unsortedOrdered {
		cs := counterSorter{resources}
		sort.Sort(cs)

		for i := range resources {
			ordered[basename] = append(ordered[basename], resources[i].Hostname())
		}
	}

	outputGroups["all"] = all
	for k, v := range individual {
		if old, exists := outputGroups[k]; exists {
			fmt.Fprintf(os.Stderr, "individual overwriting already existing output with key %s, old: %v, new: %v", k, old, v)
		}
		outputGroups[k] = v
	}
	for k, v := range ordered {
		if old, exists := outputGroups[k]; exists {
			fmt.Fprintf(os.Stderr, "ordered overwriting already existing output with key %s, old: %v, new: %v", k, old, v)
		}
		outputGroups[k] = v
	}
	for k, v := range types {
		if old, exists := outputGroups[k]; exists {
			fmt.Fprintf(os.Stderr, "types overwriting already existing output key %s, old: %v, new: %v", k, old, v)
		}
		outputGroups[k] = v
	}
	for k, v := range tags {
		if old, exists := outputGroups[k]; exists {
			fmt.Fprintf(os.Stderr, "tags overwriting already existing output key %s, old: %v, new: %v", k, old, v)
		}
		outputGroups[k] = v
	}

	return outputGroups
}

func gatherResources0dot12(s *stateTerraform0dot12) map[string]interface{} {
	outputGroups := make(map[string]interface{})

	all := &allGroup{Hosts: make([]string, 0), Vars: make(map[string]interface{})}
	types := make(map[string][]string)
	individual := make(map[string][]string)
	ordered := make(map[string][]string)
	tags := make(map[string][]string)

	unsortedOrdered := make(map[string][]*Resource)

	resourceIDNames := s.mapResourceIDNames()
	for _, res := range s.resources() {
		// place in list of all resources
		all.Hosts = appendUniq(all.Hosts, res.Hostname())

		// place in list of resource types
		tp := fmt.Sprintf("type_%s", res.resourceType)
		types[tp] = appendUniq(types[tp], res.Hostname())
		sort.Strings(types[tp])

		unsortedOrdered[res.baseName] = append(unsortedOrdered[res.baseName], res)

		// store as individual host (e.g. <name>_<count>)
		var invdName string
		if res.counterStr != "" {
			invdName = fmt.Sprintf("%s_%s", res.baseName, strings.Replace(res.counterStr, ".", "_", -1))
		} else {
			invdName = fmt.Sprintf("%s_%d", res.baseName, res.counterNumeric)
		}
		if old, exists := individual[invdName]; exists {
			fmt.Fprintf(os.Stderr, "overwriting already existing individual key %s, old: %v, new: %v", invdName, old, res.Hostname())
		}
		individual[invdName] = []string{res.Hostname()}

		// inventorize tags
		for k, v := range res.Tags() {
			// Valueless
			tag := k
			if v != "" {
				tag = fmt.Sprintf("%s_%s", k, v)
			}
			// if v is a resource ID, then tag should be resource name
			if _, exists := resourceIDNames[v]; exists {
				tag = resourceIDNames[v]
			}
			tags[tag] = appendUniq(tags[tag], res.Hostname())
			sort.Strings(tags[tag])
		}
	}

	sort.Strings(all.Hosts)

	// inventorize outputs as variables
	if len(s.outputs()) > 0 {
		for _, out := range s.outputs() {
			all.Vars[out.keyName] = out.value
		}
	}

	// sort the ordered groups
	for basename, resources := range unsortedOrdered {
		cs := counterSorter{resources}
		sort.Sort(cs)

		for i := range resources {
			ordered[basename] = append(ordered[basename], resources[i].Hostname())
		}
	}

	outputGroups["all"] = all
	for k, v := range individual {
		if old, exists := outputGroups[k]; exists {
			fmt.Fprintf(os.Stderr, "individual overwriting already existing output with key %s, old: %v, new: %v", k, old, v)
		}
		outputGroups[k] = v
	}
	for k, v := range ordered {
		if old, exists := outputGroups[k]; exists {
			fmt.Fprintf(os.Stderr, "ordered overwriting already existing output with key %s, old: %v, new: %v", k, old, v)
		}
		outputGroups[k] = v
	}
	for k, v := range types {
		if old, exists := outputGroups[k]; exists {
			fmt.Fprintf(os.Stderr, "types overwriting already existing output key %s, old: %v, new: %v", k, old, v)
		}
		outputGroups[k] = v
	}
	for k, v := range tags {
		if old, exists := outputGroups[k]; exists {
			fmt.Fprintf(os.Stderr, "tags overwriting already existing output key %s, old: %v, new: %v", k, old, v)
		}
		outputGroups[k] = v
	}

	return outputGroups
}

func cmdList(stdout io.Writer, stderr io.Writer, s *stateAnyTerraformVersion) int {
	return output(stdout, stderr, gatherResources(s))
}

func cmdInventory(stdout io.Writer, stderr io.Writer, s *stateAnyTerraformVersion) int {
	groups := gatherResources(s)
	group_names := []string{}
	for group, _ := range groups {
		group_names = append(group_names, group)
	}
	sort.Strings(group_names)
	for _, group := range group_names {

		switch grp := groups[group].(type) {
		case []string:
			writeLn("["+group+"]", stdout, stderr)
			for _, item := range grp {
				writeLn(item, stdout, stderr)
			}

		case *allGroup:
			writeLn("["+group+"]", stdout, stderr)
			for _, item := range grp.Hosts {
				writeLn(item, stdout, stderr)
			}
			writeLn("", stdout, stderr)
			writeLn("["+group+":vars]", stdout, stderr)
			vars := []string{}
			for key, _ := range grp.Vars {
				vars = append(vars, key)
			}
			sort.Strings(vars)
			for _, key := range vars {
				jsonItem, _ := json.Marshal(grp.Vars[key])
				itemLn := fmt.Sprintf("%s", string(jsonItem))
				writeLn(key+"="+itemLn, stdout, stderr)
			}
		}

		writeLn("", stdout, stderr)
	}

	return 0
}

func writeLn(str string, stdout io.Writer, stderr io.Writer) {
	_, err := io.WriteString(stdout, str+"\n")
	checkErr(err, stderr)
}

func checkErr(err error, stderr io.Writer) int {
	if err != nil {
		fmt.Fprintf(stderr, "Error writing inventory: %s\n", err)
		return 1
	}
	return 0
}

func cmdHost(stdout io.Writer, stderr io.Writer, s *stateAnyTerraformVersion, hostname string) int {
	for _, res := range s.resources() {
		if hostname == res.Hostname() {
			attributes := res.Attributes()
			attributes["ansible_host"] = res.Address()
			return output(stdout, stderr, attributes)
		}
	}

	fmt.Fprintf(stdout, "{}")
	return 1
}

// output marshals an arbitrary JSON object and writes it to stdout, or writes
// an error to stderr, then returns the appropriate exit code.
func output(stdout io.Writer, stderr io.Writer, whatever interface{}) int {
	b, err := json.Marshal(whatever)
	if err != nil {
		fmt.Fprintf(stderr, "Error encoding JSON: %s\n", err)
		return 1
	}

	_, err = stdout.Write(b)
	if err != nil {
		fmt.Fprintf(stderr, "Error writing JSON: %s\n", err)
		return 1
	}

	return 0
}
