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

package consul

import (
	"context"
	"fmt"
	"sort"
	"sync"
	"time"

	"github.com/hashicorp/go-hclog"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/consul/usagemetrics"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/api"
)

type OverviewManager struct {
	stateProvider usagemetrics.StateProvider
	logger        hclog.Logger
	interval      time.Duration

	currentSummary *structs.CatalogSummary
	sync.RWMutex
}

func NewOverviewManager(logger hclog.Logger, sp usagemetrics.StateProvider, interval time.Duration) *OverviewManager {
	return &OverviewManager{
		stateProvider:  sp,
		logger:         logger.Named("catalog-overview"),
		interval:       interval,
		currentSummary: &structs.CatalogSummary{},
	}
}

func (m *OverviewManager) GetCurrentSummary() *structs.CatalogSummary {
	m.RLock()
	defer m.RUnlock()
	return m.currentSummary
}

func (m *OverviewManager) Run(ctx context.Context) {
	ticker := time.NewTicker(m.interval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			state := m.stateProvider.State()
			catalog, err := state.CatalogDump()
			if err != nil {
				m.logger.Error("failed to update overview", "error", err)
				continue
			}

			summary := getCatalogOverview(catalog)
			m.Lock()
			m.currentSummary = summary
			m.Unlock()
		}
	}
}

// getCatalogOverview returns a breakdown of the number of nodes, services, and checks
// in the passing/warning/critical states. In Enterprise, it will also return this
// breakdown for each partition and namespace.
func getCatalogOverview(catalog *structs.CatalogContents) *structs.CatalogSummary {
	nodeChecks := make(map[string][]*structs.HealthCheck)
	serviceInstanceChecks := make(map[string][]*structs.HealthCheck)
	checkSummaries := make(map[string]structs.HealthSummary)

	entMetaIDString := func(id string, entMeta acl.EnterpriseMeta) string {
		return fmt.Sprintf("%s/%s/%s", id, entMeta.PartitionOrEmpty(), entMeta.NamespaceOrEmpty())
	}

	// Compute the health check summaries by taking the pass/warn/fail counts
	// of each unique part/ns/checkname combo and storing them. Also store the
	// per-node and per-service instance checks for their respective summaries below.
	for _, check := range catalog.Checks {
		checkID := entMetaIDString(check.Name, check.EnterpriseMeta)
		summary, ok := checkSummaries[checkID]
		if !ok {
			summary = structs.HealthSummary{
				Name:           check.Name,
				EnterpriseMeta: check.EnterpriseMeta,
			}
		}

		summary.Add(check.Status)
		checkSummaries[checkID] = summary

		if check.ServiceID != "" {
			serviceInstanceID := entMetaIDString(fmt.Sprintf("%s/%s", check.Node, check.ServiceID), check.EnterpriseMeta)
			serviceInstanceChecks[serviceInstanceID] = append(serviceInstanceChecks[serviceInstanceID], check)
		} else {
			nodeID := structs.NodeNameString(check.Node, &check.EnterpriseMeta)
			nodeChecks[nodeID] = append(nodeChecks[nodeID], check)
		}
	}

	// Compute the service instance summaries by taking the unhealthiest check for
	// a given service instance as its health status and totaling the counts for each
	// partition/ns/service combination.
	serviceSummaries := make(map[string]structs.HealthSummary)
	for _, svc := range catalog.Services {
		sid := structs.NewServiceID(svc.ServiceName, &svc.EnterpriseMeta)
		summary, ok := serviceSummaries[sid.String()]
		if !ok {
			summary = structs.HealthSummary{
				Name:           svc.ServiceName,
				EnterpriseMeta: svc.EnterpriseMeta,
			}
		}

		// Compute whether this service instance is healthy based on its associated checks.
		serviceInstanceID := entMetaIDString(fmt.Sprintf("%s/%s", svc.Node, svc.ServiceID), svc.EnterpriseMeta)
		status := api.HealthPassing
		for _, checks := range serviceInstanceChecks[serviceInstanceID] {
			if checks.Status == api.HealthWarning && status == api.HealthPassing {
				status = api.HealthWarning
			}
			if checks.Status == api.HealthCritical {
				status = api.HealthCritical
			}
		}

		summary.Add(status)
		serviceSummaries[sid.String()] = summary
	}

	// Compute the node summaries by taking the unhealthiest check for each node
	// as its health status and totaling the passing/warning/critical counts for
	// each partition.
	nodeSummaries := make(map[string]structs.HealthSummary)
	for _, node := range catalog.Nodes {
		summary, ok := nodeSummaries[node.Partition]
		if !ok {
			summary = structs.HealthSummary{
				EnterpriseMeta: *structs.NodeEnterpriseMetaInPartition(node.Partition),
			}
		}

		// Compute whether this node is healthy based on its associated checks.
		status := api.HealthPassing
		nodeID := structs.NodeNameString(node.Node, structs.NodeEnterpriseMetaInPartition(node.Partition))
		for _, checks := range nodeChecks[nodeID] {
			if checks.Status == api.HealthWarning && status == api.HealthPassing {
				status = api.HealthWarning
			}
			if checks.Status == api.HealthCritical {
				status = api.HealthCritical
			}
		}

		summary.Add(status)
		nodeSummaries[node.Partition] = summary
	}

	// Construct the summary.
	summary := &structs.CatalogSummary{}
	for _, healthSummary := range nodeSummaries {
		summary.Nodes = append(summary.Nodes, healthSummary)
	}
	for _, healthSummary := range serviceSummaries {
		summary.Services = append(summary.Services, healthSummary)
	}
	for _, healthSummary := range checkSummaries {
		summary.Checks = append(summary.Checks, healthSummary)
	}

	summarySort := func(slice []structs.HealthSummary) func(int, int) bool {
		return func(i, j int) bool {
			if slice[i].PartitionOrEmpty() < slice[j].PartitionOrEmpty() {
				return true
			}
			if slice[i].NamespaceOrEmpty() < slice[j].NamespaceOrEmpty() {
				return true
			}
			return slice[i].Name < slice[j].Name
		}
	}
	sort.Slice(summary.Nodes, summarySort(summary.Nodes))
	sort.Slice(summary.Services, summarySort(summary.Services))
	sort.Slice(summary.Checks, summarySort(summary.Checks))

	return summary
}
