package checker

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"slices"
	"strconv"
	"strings"
	"time"

	monent "github.com/tidbcloud/devops/entity/monitoring"
	"github.com/tidbcloud/devops/pkg/infra/asset"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container/pair"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/service/tenant"

	dediv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.opentelemetry.io/otel/trace"
)

const (
	timeLayout = "200601021504"
)

// Report files stucture:
//
//	monitoring/component_diff_reports/
//	  +- <report_type>
//	  	+- report_metadata_<name>.json
//	  	+- report_<name>.json
type CompDiffChecker struct {
	coresvc              *coresvccli.CoreSvcCli
	tenantService        tenant.TenantServiceAPI
	asset                asset.Asset
	reportRetentionCount int
}

func NewCompDiffChecker(coresvc *coresvccli.CoreSvcCli, tenantService tenant.TenantServiceAPI, asset asset.Asset, reportRetentionCount int) (*CompDiffChecker, error) {
	return &CompDiffChecker{
		coresvc:              coresvc,
		tenantService:        tenantService,
		reportRetentionCount: reportRetentionCount,
		asset:                asset,
	}, nil
}

func (c *CompDiffChecker) Check(ctx context.Context) error {
	metadata, report, err := c.GenerateClusterSpecReport(ctx)
	if err != nil {
		return fmt.Errorf("failed to generate cluster spec report: %w", err)
	}

	return c.storeReport(ctx, metadata, report)
}

func (c *CompDiffChecker) ListReports(ctx context.Context, typ monent.ComponentDifferenceReportType) ([]monent.ComponentDifferenceReportMetadata, error) {
	bucket, err := c.assetForType(typ).Bucket(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get bucket for report type '%s': %w", typ, err)
	}
	defer bucket.Close()

	results := []monent.ComponentDifferenceReportMetadata{}
	iter := bucket.List(nil)
	for {
		obj, err := iter.Next(ctx)
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("failed to query bucket: %w", err)
		}

		if obj.IsDir {
			continue
		}
		if !isReportMetadataFilename(obj.Key) {
			continue
		}

		data, err := bucket.ReadAll(ctx, obj.Key)
		if err != nil {
			return nil, fmt.Errorf("failed to read report metadata '%s': %w", obj.Key, err)
		}

		var metadata monent.ComponentDifferenceReportMetadata
		if err := json.Unmarshal(data, &metadata); err != nil {
			return nil, fmt.Errorf("failed to unmarshal report metadata '%s': %w", obj.Key, err)
		}

		results = append(results, metadata)
	}

	// Sort the results by CreatedAt in ascending order
	slices.SortFunc(results, func(metadataA, metadataB monent.ComponentDifferenceReportMetadata) int {
		return metadataA.CreatedAt.Compare(metadataB.CreatedAt)
	})

	return results, nil
}

func (c *CompDiffChecker) GetReport(ctx context.Context, typ monent.ComponentDifferenceReportType, name string) (*monent.ComponentDifferenceReportMetadata, *monent.ComponentDifferenceReport, error) {
	// get the bucket
	bucket, err := c.assetForType(typ).Bucket(ctx)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get bucket for report type '%s': %w", typ, err)
	}
	defer bucket.Close()

	metadataFilename, reportFilename := formatReportFilename(name)

	// read the metadata
	metadataData, err := bucket.ReadAll(ctx, metadataFilename)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to read report metadata '%s': %w", metadataFilename, err)
	}
	metadata := &monent.ComponentDifferenceReportMetadata{}
	if err := json.Unmarshal(metadataData, metadata); err != nil {
		return nil, nil, fmt.Errorf("failed to unmarshal report metadata '%s': %w", metadataFilename, err)
	}

	// read the report
	reportData, err := bucket.ReadAll(ctx, reportFilename)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to read report '%s': %w", reportFilename, err)
	}
	report := &monent.ComponentDifferenceReport{}
	if err := json.Unmarshal(reportData, report); err != nil {
		return nil, nil, fmt.Errorf("failed to unmarshal report '%s': %w", reportFilename, err)
	}

	return metadata, report, nil
}

func (c *CompDiffChecker) DeleteReport(ctx context.Context, typ monent.ComponentDifferenceReportType, name string) error {
	bucket, err := c.assetForType(typ).Bucket(ctx)
	if err != nil {
		return fmt.Errorf("failed to get bucket for report type '%s': %w", typ, err)
	}
	defer bucket.Close()

	metadataFilename, reportFilename := formatReportFilename(name)
	if err := bucket.Delete(ctx, metadataFilename); err != nil {
		return fmt.Errorf("failed to delete report metadata '%s': %w", metadataFilename, err)
	}
	if err := bucket.Delete(ctx, reportFilename); err != nil {
		return fmt.Errorf("failed to delete report '%s': %w", reportFilename, err)
	}

	return nil
}

func (c *CompDiffChecker) GenerateClusterSpecReport(ctx context.Context) (*monent.ComponentDifferenceReportMetadata, *monent.ComponentDifferenceReport, error) {
	now := time.Now()

	metadata := &monent.ComponentDifferenceReportMetadata{
		Name:           formatReportName(now),
		CreatedAt:      now,
		Type:           monent.ComponentDifferenceReportTypeClusterSpec,
		RequestTraceID: trace.SpanFromContext(ctx).SpanContext().TraceID().String(),
	}
	report := &monent.ComponentDifferenceReport{
		Difference: []monent.ComponentDifferenceItemsPerCluster{},
	}

	page := 1 // start from page 1
	perPage := 100
	for {
		req := &mgmtv1.BuildComponentSpecDiffReportsRequest{
			Page:     uint32(page),
			PageSize: uint32(perPage),
		}
		resp, err := c.coresvc.Mgmt.Cluster.BuildComponentSpecDiffReports(ctx, req)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to query component spec: %w", err)
		}

		if len(resp.Clusters) == 0 {
			break
		}
		page++

		// structure the report metadata
		for _, cluster := range resp.Clusters {
			compPairs := pair.NewPairs[*dediv1.Cluster_ClusterComponent, *dediv1.Cluster_ClusterComponent]()

			// pair the default and effective components by component type
			for _, compDefault := range cluster.CurrentDefaultComponents {
				var compEffective *dediv1.Cluster_ClusterComponent

				for _, compEffectiveTmp := range cluster.CurrentEffectiveComponents {
					if compDefault.ComponentType == compEffectiveTmp.ComponentType {
						compEffective = compEffectiveTmp
						break
					}
				}

				if compEffective == nil {
					log.FromContext(ctx).Info("Ingore to diff a cluster cause no found effective component", "component_type", compDefault.ComponentType, "cluster_id", cluster.ClusterId)
					continue
				}

				compPairs.Append(compDefault, compEffective)
			}

			// diff the component spec
			diffItems := map[string][]monent.ComponentDifferenceItem{}
			for compDefault, compEffective := range compPairs.Pairs() {
				items := c.diffComponent(compDefault, compEffective)
				if len(items) == 0 {
					continue
				}
				diffItems[compEffective.ComponentType.String()] = items
			}

			metadata.TotalClusterCount++
			if len(diffItems) != 0 {
				diffPerCluster := monent.ComponentDifferenceItemsPerCluster{
					TenantID:     strconv.FormatUint(cluster.TenantId, 10),
					TenantName:   "", // populate later
					TenantRegion: "", // populate later
					ProjectID:    strconv.FormatUint(cluster.ProjectId, 10),
					ClusterID:    strconv.FormatUint(cluster.ClusterId, 10),
					ClusterName:  cluster.ClusterName,
					Items:        diffItems,
				}

				tenant, err := c.tenantService.GetTenant(ctx, cluster.TenantId)
				if err != nil {
					log.FromContext(ctx).Error(err, "Failed to get tenant for cluster", "tenant_id", cluster.TenantId, "cluster_id", cluster.ClusterId)
				} else {
					diffPerCluster.TenantName = tenant.Name
					diffPerCluster.TenantRegion = string(tenant.CountryRegion)
				}

				metadata.TenantIDs = append(metadata.TenantIDs, strconv.FormatUint(cluster.TenantId, 10))
				metadata.ClusterIDs = append(metadata.ClusterIDs, strconv.FormatUint(cluster.ClusterId, 10))
				report.Difference = append(report.Difference, diffPerCluster)
			}

		}
	}

	return metadata, report, nil
}

func (c *CompDiffChecker) CleanReports(ctx context.Context) error {
	// List all reports in the bucket
	metadatas, err := c.ListReports(ctx, monent.ComponentDifferenceReportTypeClusterSpec)
	if err != nil {
		return fmt.Errorf("failed to list reports: %w", err)
	}

	if len(metadatas) <= c.reportRetentionCount {
		return nil
	}

	for i := range len(metadatas) - c.reportRetentionCount {
		metadata := metadatas[i]

		err := c.DeleteReport(ctx, metadata.Type, metadata.Name)
		if err != nil {
			log.FromContext(ctx).Error(err, "Failed to delete an expired component diff report", "type", metadata.Type, "name", metadata.Name)
			continue
		}
	}

	return nil
}

func (c *CompDiffChecker) diffComponent(compDefault, compEffective *dediv1.Cluster_ClusterComponent) []monent.ComponentDifferenceItem {
	compType := compEffective.ComponentType.String()

	results := []monent.ComponentDifferenceItem{}

	if compDefault.SpuDisplayName != compEffective.SpuDisplayName {
		results = append(results, monent.ComponentDifferenceItem{
			Component: compType,
			Key:       "SpuDisplayName",
			Desired:   compDefault.SpuDisplayName,
			Actual:    compEffective.SpuDisplayName,
		})
	}

	if compDefault.CpuNum != compEffective.CpuNum {
		results = append(results, monent.ComponentDifferenceItem{
			Component: compType,
			Key:       "CpuNum",
			Desired:   compDefault.CpuNum,
			Actual:    compEffective.CpuNum,
		})
	}

	if compDefault.MemorySizeGi != compEffective.MemorySizeGi {
		results = append(results, monent.ComponentDifferenceItem{
			Component: compType,
			Key:       "MemoryGi",
			Desired:   compDefault.MemorySizeGi,
			Actual:    compEffective.MemorySizeGi,
		})
	}

	if compDefault.SkuKey != compEffective.SkuKey {
		results = append(results, monent.ComponentDifferenceItem{
			Component: compType,
			Key:       "SkuKey",
			Desired:   compDefault.SkuKey,
			Actual:    compEffective.SkuKey,
		})
	}

	dCount := pointer.Deref(compDefault.Count, 0)
	eCount := pointer.Deref(compEffective.Count, 0)
	if dCount != eCount {
		results = append(results, monent.ComponentDifferenceItem{
			Component: compType,
			Key:       "Count",
			Desired:   dCount,
			Actual:    eCount,
		})
	}

	for key, dStorage := range compDefault.Storages {
		eStorege, ok := compEffective.Storages[key]
		if !ok {
			continue
		}

		if dStorage.SizeGi != eStorege.SizeGi {
			results = append(results, monent.ComponentDifferenceItem{
				Component: compType,
				Key:       fmt.Sprintf("Storages[\"%s\"].Size", key),
				Desired:   dStorage.SizeGi,
				Actual:    eStorege.SizeGi,
			})
		}

		if dStorage.Iops != eStorege.Iops {
			results = append(results, monent.ComponentDifferenceItem{
				Component: compType,
				Key:       fmt.Sprintf("Storages[\"%s\"].Iops", key),
				Desired:   dStorage.Iops,
				Actual:    eStorege.Iops,
			})
		}
	}

	return results
}

func (c *CompDiffChecker) storeReport(ctx context.Context, metadata *monent.ComponentDifferenceReportMetadata, report *monent.ComponentDifferenceReport) error {
	// get the bucket
	bucket, err := c.assetForType(metadata.Type).Bucket(ctx)
	if err != nil {
		return fmt.Errorf("failed to get bucket for report type '%s': %w", metadata.Type, err)
	}
	defer bucket.Close()

	// marshal metadata and report
	metadataData, err := json.Marshal(metadata)
	if err != nil {
		return fmt.Errorf("failed to marshal report metadata: %w", err)
	}
	reportData, err := json.Marshal(report)
	if err != nil {
		return fmt.Errorf("failed to marshal report: %w", err)
	}

	// write metadata and report to the bucket
	metadataFile, reportFile := formatReportFilename(metadata.Name)
	err = bucket.WriteAll(ctx, metadataFile, metadataData, nil)
	if err != nil {
		return fmt.Errorf("failed to write report metadata '%s' to bucket: %w", metadataFile, err)
	}
	err = bucket.WriteAll(ctx, reportFile, reportData, nil)
	if err != nil {
		return fmt.Errorf("failed to write report '%s' to bucket: %w", reportFile, err)
	}

	return nil
}

func (c *CompDiffChecker) assetForType(typ monent.ComponentDifferenceReportType) asset.Asset {
	return c.asset.Sub(string(typ))
}

func formatReportName(now time.Time) string {
	return now.Format(timeLayout)
}

func formatReportFilename(name string) (string, string) {
	return fmt.Sprintf("report_metadata_%s.json", name), fmt.Sprintf("report_%s.json", name)
}

func isReportMetadataFilename(name string) bool {
	return strings.HasPrefix(name, "report_metadata_")
}
