package tenants

import (
	"context"
	"fmt"
	"sort"

	commonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	tidbmgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	tigrpcclient "github.com/tidbcloud/tiinf/transport/tigrpc/client"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/tidbcloud/observability/vm-migration/internal/status"
)

// essentialPageSize is the mgmt ListClusters page size.
const essentialPageSize = 100

// essentialPlan is the fixed service-plan filter for Essential collection.
const essentialPlan = commonv1beta1.ServicePlan_Essential

// EssentialCluster is one cluster row produced by mgmt ListClusters, later
// enriched with regional-server vm info.
//
// It is the element type of the cluster report artifact.
type EssentialCluster struct {
	ClusterID      uint64 `json:"clusterId"`
	ClusterName    string `json:"clusterName"`
	ProjectID      uint64 `json:"projectId"`
	OrganizationID uint64 `json:"organizationId"`
	CloudProvider  string `json:"cloudProvider"`
	DeployType     string `json:"deployType"`
	Region         string `json:"region"`
	ServicePlan    string `json:"servicePlan"`
	Deleted        bool   `json:"deleted"`
	CreateTime     string `json:"createTime,omitempty"`
	UpdateTime     string `json:"updateTime,omitempty"`
	DeleteTime     string `json:"deleteTime,omitempty"`

	// Fields enriched from the regional-server MySQL join.
	InternalClusterID string `json:"internalClusterId,omitempty"`
	VMAccountID       string `json:"vmAccountId,omitempty"`
	VMProjectID       string `json:"vmProjectId,omitempty"`
	DBFound           bool   `json:"dbFound"`
	DBDeletionTime    string `json:"dbDeletionTime,omitempty"`
}

// essentialClusterFromProto converts one mgmt Cluster message. Deleted is
// derived from DeleteTime presence, not from the query filter.
func essentialClusterFromProto(c *tidbmgmt.Cluster) EssentialCluster {
	return EssentialCluster{
		ClusterID:      c.GetClusterId(),
		ClusterName:    c.GetClusterName(),
		ProjectID:      c.GetProjectId(),
		OrganizationID: c.GetOrganizationId(),
		CloudProvider:  c.GetCloudProvider(),
		DeployType:     c.GetDeployType().String(),
		Region:         c.GetRegion(),
		ServicePlan:    c.GetServicePlan().String(),
		Deleted:        c.GetDeleteTime() != nil,
		CreateTime:     protoTS(c.GetCreateTime()),
		UpdateTime:     protoTS(c.GetUpdateTime()),
		DeleteTime:     protoTS(c.GetDeleteTime()),
	}
}

// protoTS formats a protobuf timestamp per the artifact timestamp convention;
// nil returns "".
func protoTS(t *timestamppb.Timestamp) string {
	if t == nil {
		return ""
	}
	return status.FormatTime(t.AsTime())
}

// mgmtClusterPageClient is the ListClusters subset used by pagination.
//
// Kept narrower than tidbmgmt.ClusterServiceClient so tests implement one
// method with canned pages.
type mgmtClusterPageClient interface {
	ListClusters(ctx context.Context, req *tidbmgmt.ListClustersRequest, opts ...grpc.CallOption) (*tidbmgmt.ListClustersResponse, error)
}

// MgmtClusterLister abstracts mgmt cluster discovery so tests inject fakes
// without a gRPC connection.
type MgmtClusterLister interface {
	// List returns Essential-plan clusters for region and cloud provider at
	// the given deletion state.
	List(ctx context.Context, region, provider string, deleted *bool) ([]EssentialCluster, error)
}

// mgmtLister is the production MgmtClusterLister.
//
// Calls dial lazily; connection errors surface on the first List call.
type mgmtLister struct {
	cli tidbmgmt.ClusterServiceClient
}

func (m *mgmtLister) List(ctx context.Context, region, provider string, deleted *bool) ([]EssentialCluster, error) {
	return listMgmtPages(ctx, m.cli, region, provider, essentialPlan, deleted)
}

// NewMgmtLister dials the mgmt gRPC service and returns the production
// MgmtClusterLister plus a closer for the underlying connection.
//
// The connection is insecure (no TLS / no auth), identical to how the global
// server dials mgmt; the address is only reachable in-cluster or via
// port-forward. Callers own the closer.
func NewMgmtLister(addr string) (MgmtClusterLister, func() error, error) {
	conn, err := tigrpcclient.NewClientConn(addr, tigrpcclient.WithDefault())
	if err != nil {
		return nil, nil, fmt.Errorf("dial mgmt service %s: %w", addr, err)
	}
	return &mgmtLister{cli: tidbmgmt.NewClusterServiceClient(conn)}, conn.Close, nil
}

// listMgmtPages paginates ListClusters by incrementing Page until exhausted.
//
// Stops on a short page or once the accumulated count reaches Total, guarding
// against a server that keeps returning full pages.
func listMgmtPages(ctx context.Context, c mgmtClusterPageClient, region, provider string, plan commonv1beta1.ServicePlan, deleted *bool) ([]EssentialCluster, error) {
	var all []EssentialCluster
	for page := uint32(0); ; page++ {
		resp, err := c.ListClusters(ctx, &tidbmgmt.ListClustersRequest{
			Filter: &tidbmgmt.ClusterFilter{
				CloudProviders: []string{provider},
				Regions:        []string{region},
				ServicePlan:    plan,
				Deleted:        deleted,
			},
			PageSize: essentialPageSize,
			Page:     page,
		})
		if err != nil {
			return nil, fmt.Errorf("ListClusters page %d: %w", page, err)
		}
		rows := make([]EssentialCluster, 0, len(resp.GetClusters()))
		for _, cl := range resp.GetClusters() {
			rows = append(rows, essentialClusterFromProto(cl))
		}
		all = append(all, rows...)
		if len(rows) < essentialPageSize {
			break
		}
		if resp.GetTotal() > 0 && uint32(len(all)) >= resp.GetTotal() {
			break
		}
	}
	return all, nil
}

// mergeByClusterID deduplicates two deletion-state listings by ClusterID; on
// collision the deleted-state row wins (it carries DeleteTime).
//
// Output is sorted by ClusterID so staged report artifacts are byte-stable
// across runs.
func mergeByClusterID(active, deleted []EssentialCluster) []EssentialCluster {
	seen := make(map[uint64]EssentialCluster, len(active)+len(deleted))
	for _, c := range active {
		seen[c.ClusterID] = c
	}
	for _, c := range deleted {
		seen[c.ClusterID] = c
	}
	out := make([]EssentialCluster, 0, len(seen))
	for _, c := range seen {
		out = append(out, c)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].ClusterID < out[j].ClusterID })
	return out
}
