// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package instances

import (
	"bytes"
	"flag"
	"fmt"
	"sort"
	"strings"
	"text/tabwriter"

	"github.com/mitchellh/cli"
	"golang.org/x/exp/maps"

	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/command/flags"
)

func New(ui cli.Ui) *cmd {
	c := &cmd{UI: ui}
	c.init()
	return c
}

type cmd struct {
	UI    cli.Ui
	flags *flag.FlagSet
	http  *flags.HTTPFlags
	help  string

	// flags
	onlyBillable   bool
	onlyConnect    bool
	allDatacenters bool
}

func (c *cmd) init() {
	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
	c.flags.BoolVar(&c.onlyBillable, "billable", false, "Display only billable service info. "+
		"Cannot be used with -connect.")
	c.flags.BoolVar(&c.onlyConnect, "connect", false, "Display only Connect service info."+
		"Cannot be used with -billable.")
	c.flags.BoolVar(&c.allDatacenters, "all-datacenters", false, "Display service counts from "+
		"all datacenters.")

	c.http = &flags.HTTPFlags{}
	flags.Merge(c.flags, c.http.ClientFlags())
	flags.Merge(c.flags, c.http.ServerFlags())
	c.help = flags.Usage(help, c.flags)
}

func (c *cmd) Run(args []string) int {
	if err := c.flags.Parse(args); err != nil {
		return 1
	}

	if l := len(c.flags.Args()); l > 0 {
		c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", l))
		return 1
	}

	if c.onlyBillable && c.onlyConnect {
		c.UI.Error("Cannot specify both -billable and -connect flags")
		return 1
	}

	// Create and test the HTTP client
	client, err := c.http.APIClient()
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
		return 1
	}

	billableTotal := 0
	var datacenterBillableTotals []string
	usage, _, err := client.Operator().Usage(&api.QueryOptions{Global: c.allDatacenters})
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error fetching usage information: %s", err))
		return 1
	}
	for dc, usage := range usage.Usage {
		billableTotal += usage.BillableServiceInstances
		datacenterBillableTotals = append(datacenterBillableTotals,
			fmt.Sprintf("%s Billable Service Instances: %d", dc, usage.BillableServiceInstances))
	}

	// Output billable service counts
	if !c.onlyConnect {
		c.UI.Output(fmt.Sprintf("Billable Service Instances Total: %d", billableTotal))
		sort.Strings(datacenterBillableTotals)
		for _, datacenterTotal := range datacenterBillableTotals {
			c.UI.Output(datacenterTotal)
		}

		c.UI.Output("\nBillable Services")
		billableOutput, err := formatServiceCounts(usage.Usage, true, c.allDatacenters)
		if err != nil {
			c.UI.Error(err.Error())
			return 1
		}
		c.UI.Output(billableOutput + "\n")

		c.UI.Output("\nNodes")
		nodesOutput, err := formatNodesCounts(usage.Usage)
		if err != nil {
			c.UI.Error(err.Error())
			return 1
		}
		c.UI.Output(nodesOutput + "\n\n")
	}

	// Output Connect service counts
	if !c.onlyBillable {
		c.UI.Output("Connect Services")
		connectOutput, err := formatServiceCounts(usage.Usage, false, c.allDatacenters)
		if err != nil {
			c.UI.Error(err.Error())
			return 1
		}
		c.UI.Output(connectOutput)
	}

	return 0
}

func formatNodesCounts(usageStats map[string]api.ServiceUsage) (string, error) {
	var output bytes.Buffer
	tw := tabwriter.NewWriter(&output, 0, 2, 6, ' ', 0)

	nodesTotal := 0

	fmt.Fprintf(tw, "Datacenter\t")

	fmt.Fprintf(tw, "Count\t")

	fmt.Fprint(tw, "\t\n")

	nodes := maps.Keys(usageStats)
	sort.Strings(nodes)
	for _, dc := range nodes {
		nodesTotal += usageStats[dc].Nodes
		fmt.Fprintf(tw, "%s\t%d\n", dc, usageStats[dc].Nodes)
	}

	fmt.Fprint(tw, "\t\n")
	fmt.Fprintf(tw, "Total")

	fmt.Fprintf(tw, "\t%d", nodesTotal)

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("Error flushing tabwriter: %s", err)
	}
	return strings.TrimSpace(output.String()), nil
}

func formatServiceCounts(usageStats map[string]api.ServiceUsage, billable, showDatacenter bool) (string, error) {
	var output bytes.Buffer
	tw := tabwriter.NewWriter(&output, 0, 2, 6, ' ', 0)
	var serviceCounts []serviceCount

	for datacenter, usage := range usageStats {
		if billable {
			serviceCounts = append(serviceCounts, getBillableInstanceCounts(usage, datacenter)...)
		} else {
			serviceCounts = append(serviceCounts, getConnectInstanceCounts(usage, datacenter)...)
		}
	}

	sortServiceCounts(serviceCounts)

	if showDatacenter {
		fmt.Fprintf(tw, "Datacenter\t")
	}
	if showPartitionNamespace {
		fmt.Fprintf(tw, "Partition\tNamespace\t")
	}
	if !billable {
		fmt.Fprintf(tw, "Type\t")
	} else {
		fmt.Fprintf(tw, "Services\t")
	}
	fmt.Fprintf(tw, "Service instances\n")

	serviceTotal := 0
	instanceTotal := 0
	for _, c := range serviceCounts {
		if showDatacenter {
			fmt.Fprintf(tw, "%s\t", c.datacenter)
		}
		if showPartitionNamespace {
			fmt.Fprintf(tw, "%s\t%s\t", c.partition, c.namespace)
		}
		if !billable {
			fmt.Fprintf(tw, "%s\t", c.serviceType)
		} else {
			fmt.Fprintf(tw, "%d\t", c.services)
		}
		fmt.Fprintf(tw, "%d\n", c.instanceCount)

		serviceTotal += c.services
		instanceTotal += c.instanceCount
	}

	// Show total counts if there's multiple rows because of datacenter or partition/ns view
	if showDatacenter || showPartitionNamespace {
		if showDatacenter {
			fmt.Fprint(tw, "\t")
		}
		if showPartitionNamespace {
			fmt.Fprint(tw, "\t\t")
		}
		fmt.Fprint(tw, "\t\n")
		fmt.Fprintf(tw, "Total")
		if showPartitionNamespace {
			fmt.Fprint(tw, "\t")
			if showDatacenter {
				fmt.Fprint(tw, "\t")
			}
		}

		if billable {
			fmt.Fprintf(tw, "\t%d\t%d\n", serviceTotal, instanceTotal)
		} else {
			fmt.Fprintf(tw, "\t\t%d\n", instanceTotal)
		}
	}

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("Error flushing tabwriter: %s", err)
	}
	return strings.TrimSpace(output.String()), nil
}

type serviceCount struct {
	datacenter    string
	partition     string
	namespace     string
	serviceType   string
	instanceCount int
	services      int
}

// Sort entries by datacenter > partition > namespace
func sortServiceCounts(counts []serviceCount) {
	sort.Slice(counts, func(i, j int) bool {
		if counts[i].datacenter != counts[j].datacenter {
			return counts[i].datacenter < counts[j].datacenter
		}
		if counts[i].partition != counts[j].partition {
			return counts[i].partition < counts[j].partition
		}
		if counts[i].namespace != counts[j].namespace {
			return counts[i].namespace < counts[j].namespace
		}
		return counts[i].serviceType < counts[j].serviceType
	})
}

func (c *cmd) Synopsis() string {
	return synopsis
}

func (c *cmd) Help() string {
	return c.help
}

const (
	synopsis = "Display service instance usage information"
	help     = `
Usage: consul operator usage instances [options]

  Retrieves usage information about the number of services registered in a given
  datacenter. By default, the datacenter of the local agent is queried.

  To retrieve the service usage data:

      $ consul operator usage instances

  To show only billable service instance counts:

      $ consul operator usage instances -billable

  To show only connect service instance counts:

      $ consul operator usage instances -connect

  For a full list of options and examples, please see the Consul documentation.
`
)
