package models

import (
	"encoding/json"
	"fmt"
	"math"
	"strconv"
	"strings"
)

func ComparatorBuilder(startTime int64, endTime int64) *Comparator {
	return &Comparator{
		StartTime: startTime,
		EndTime:   endTime,
		Clusters:  map[string]*ClusterComparison{},
	}
}

type Comparator struct {
	StartTime int64                         `json:"startTime"`
	EndTime   int64                         `json:"endTime"`
	Clusters  map[string]*ClusterComparison `json:"clusters"`
}

func (c *Comparator) InMetering(info ClusterInfo, sku string, count int64) *Comparator {
	c.ensureClusterExists(info)
	clusterComparison := c.Clusters[info.ClusterID]
	clusterComparison.InMetering = true
	clusterComparison.ensureSkuExists(sku)
	clusterComparison.SKUs[sku].CountInMetering = clusterComparison.SKUs[sku].CountInMetering + count
	return c
}

func (c *Comparator) InMeteringV2(info ClusterInfo, sku string, count int64) *Comparator {
	c.ensureClusterExists(info)
	clusterComparison := c.Clusters[info.ClusterID]
	clusterComparison.InMeteringV2 = true
	clusterComparison.ensureSkuExists(sku)
	clusterComparison.SKUs[sku].CountInMeteringV2 = clusterComparison.SKUs[sku].CountInMeteringV2 + count
	return c
}

func (c *Comparator) InCoreSvc(info ClusterInfo, sku string, count int64) *Comparator {
	c.ensureClusterExists(info)
	clusterComparison := c.Clusters[info.ClusterID]
	clusterComparison.InCoreSvc = true
	clusterComparison.ensureSkuExists(sku)
	clusterComparison.SKUs[sku].CountInCoreSvc = clusterComparison.SKUs[sku].CountInCoreSvc + count
	return c
}

func (c *Comparator) ensureClusterExists(info ClusterInfo) {
	if _, ok := c.Clusters[info.ClusterID]; !ok {
		c.Clusters[info.ClusterID] = &ClusterComparison{
			ClusterInfo: ClusterInfo{
				TenantID:  info.TenantID,
				ProjectID: info.ProjectID,
				ClusterID: info.ClusterID,
			},
			SKUs: map[string]*SKUComparison{},
		}
	}
}

func (c *Comparator) CalculateResult(tolerance float64) *Comparator {
	for _, cluster := range c.Clusters {
		cluster.WithinTolerableRange = cluster.InMetering && cluster.InCoreSvc
		cluster.WithinTolerableRangeV2 = cluster.InMeteringV2 && cluster.InMetering
		if cluster.WithinTolerableRange {
			for _, sku := range cluster.SKUs {
				skuTolerance := sku.CalculateTolerance(tolerance)
				cluster.WithinTolerableRange = cluster.WithinTolerableRange && skuTolerance

			}
		} else {
			for _, sku := range cluster.SKUs {
				if strings.Contains(sku.SKU, "azure") {
					cluster.WithinTolerableRange = true
				}
			}
		}
		//for new sku metering and old sku metering, they must be equal
		if cluster.WithinTolerableRangeV2 {
			for _, sku := range cluster.SKUs {
				skuToleranceV2 := sku.CalculateToleranceV2()
				cluster.WithinTolerableRangeV2 = cluster.WithinTolerableRangeV2 && skuToleranceV2
			}

		}
	}

	return c
}

func (c *Comparator) UnmatchedClusters() ([]string, []ClusterComparison) {
	var list []string
	var comparisonList []ClusterComparison

	for _, cluster := range c.Clusters {
		if !cluster.WithinTolerableRange {
			list = append(list, cluster.ClusterID)
			comparisonList = append(comparisonList, *cluster)
		}
	}
	return list, comparisonList
}

func (c *Comparator) UnmatchedClustersV2() ([]string, []ClusterComparison) {
	var list []string
	var comparisonList []ClusterComparison

	for _, cluster := range c.Clusters {
		if !cluster.WithinTolerableRangeV2 {
			list = append(list, cluster.ClusterID)
			comparisonList = append(comparisonList, *cluster)
		}
	}
	return list, comparisonList
}

func (c *Comparator) ToString() string {
	s, err := json.Marshal(c)
	if err != nil {
		panic(err)
	}
	return string(s)
}

type ClusterInfo struct {
	TenantID  string `json:"tenantID"`
	ProjectID string `json:"projectID"`
	ClusterID string `json:"clusterID"`
}

type ClusterComparison struct {
	ClusterInfo
	InMetering   bool `json:"inMetering"`
	InMeteringV2 bool `json:"inMeteringV2"`
	InCoreSvc    bool `json:"inCoreSvc"`

	WithinTolerableRange   bool                      `json:"withinTolerableRange"`
	WithinTolerableRangeV2 bool                      `json:"withinTolerableRangeV2"`
	SKUs                   map[string]*SKUComparison `json:"skus"`
}

func (c *ClusterComparison) ensureSkuExists(sku string) {
	if _, ok := c.SKUs[sku]; !ok {
		c.SKUs[sku] = &SKUComparison{
			SKU: sku,
		}
	}
}

type SKUComparison struct {
	SKU                    string  `json:"sku"`
	CountInCoreSvc         int64   `json:"countInCoreSvc"`
	CountInMetering        int64   `json:"countInMetering"`
	CountInMeteringV2      int64   `json:"countInMeteringV2"`
	DifferenceRatio        float64 `json:"differenceRatio"`
	DifferenceRatioV2      float64 `json:"differenceRatioV2"`
	WithinTolerableRange   bool    `json:"withinTolerableRange"`
	WithinTolerableRangeV2 bool    `json:"withinTolerableRangeV2"`
}

func (s *SKUComparison) CalculateTolerance(tolerance float64) bool {
	if s.CountInCoreSvc == 0 && s.CountInMetering == 0 {
		s.DifferenceRatio = 0
		s.WithinTolerableRange = true
		return s.WithinTolerableRange
	}
	if s.CountInCoreSvc == 0 && s.CountInMetering > 0 {
		s.DifferenceRatio = 1
		s.WithinTolerableRange = false
		return s.WithinTolerableRange
	}
	if s.CountInCoreSvc > 0 && s.CountInMetering == 0 {
		s.DifferenceRatio = -1
		s.WithinTolerableRange = false
		return s.WithinTolerableRange
	}

	differenceRatioStr := fmt.Sprintf("%.5f", float64(s.CountInMetering-s.CountInCoreSvc)/float64(s.CountInCoreSvc))
	s.DifferenceRatio, _ = strconv.ParseFloat(differenceRatioStr, 64)

	s.WithinTolerableRange = math.Abs(s.DifferenceRatio) < tolerance
	return s.WithinTolerableRange
}

// for v2 and v1, there must be equal
func (s *SKUComparison) CalculateToleranceV2() bool {
	if s.CountInMetering == 0 && s.CountInMeteringV2 == 0 {
		s.DifferenceRatioV2 = 0
		s.WithinTolerableRangeV2 = true
		return s.WithinTolerableRangeV2
	}
	if s.CountInMetering == 0 && s.CountInMeteringV2 > 0 {
		s.DifferenceRatioV2 = 1
		s.WithinTolerableRangeV2 = false
		return s.WithinTolerableRangeV2
	}
	if s.CountInMetering > 0 && s.CountInMeteringV2 == 0 {
		s.DifferenceRatioV2 = -1
		s.WithinTolerableRangeV2 = false
		return s.WithinTolerableRangeV2
	}
	differenceRatioStr := fmt.Sprintf("%.5f", float64(s.CountInMeteringV2-s.CountInMetering)/float64(s.CountInMetering))
	s.DifferenceRatioV2, _ = strconv.ParseFloat(differenceRatioStr, 64)
	epsilon := 1e-9 // floating point comparison tolerance
	s.WithinTolerableRangeV2 = math.Abs(s.DifferenceRatioV2) < epsilon
	return s.WithinTolerableRangeV2
}
