package clusterrecovery

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/cloudevent"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/datasource/utils"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/nextgen_client"
)

var (
	ErrSourceNotFound     = errors.New("source object not found")
	ErrSourceUnsupported  = errors.New("source object type is not synchronized by Clinic")
	ErrSourceOwnership    = errors.New("source ownership does not match candidate")
	ErrSourceTypeMismatch = errors.New("source type does not match candidate")
	ErrSourceIncomplete   = errors.New("source object is incomplete")
)

type ResolvedSource struct {
	Candidate       Candidate
	Desired         *db.Cluster
	SourceUpdatedAt time.Time
	Terminal        bool
	Parent          *Candidate
}

type Source interface {
	Resolve(context.Context, Candidate) (*ResolvedSource, error)
}

type CloudSource struct {
	getMgmtClusters func(*session.CtxUser, []uint64) ([]*mgmtv1.Cluster, error)
	listPools       func(*session.CtxUser, []uint64) ([]*nextgenv1.ResourcePoolProfile, error)
	getPool         func(*session.CtxUser, uint64) (*nextgenv1.ResourcePoolProfile, *nextgen_client.SharedPool, error)
}

func NewCloudSource() *CloudSource {
	return &CloudSource{
		getMgmtClusters: datasource.RealtimeGetMgmtClusters,
		listPools:       datasource.ListResourcePoolWithProfile,
		getPool:         datasource.GetResourcePoolWithProfile,
	}
}

func CandidateFromSeed(seed SeedCandidate) Candidate {
	kind := ObjectKindUnsupported
	switch {
	case seed.Evidence == EvidenceImportDLQ:
		kind = ObjectKindMgmtCluster
	case seed.ClusterType == cloudevent.ClusterTypeTidbxResourcePool:
		kind = ObjectKindResourcePool
	case seed.ClusterType == "" ||
		seed.ClusterType == cloudevent.ClusterTypeDedicatedCluster ||
		seed.ClusterType == cloudevent.ClusterTypeTidbxInstance:
		kind = ObjectKindMgmtCluster
	}
	return Candidate{
		Kind:        kind,
		ClusterID:   seed.ClusterID,
		TenantID:    seed.TenantID,
		ClusterType: seed.ClusterType,
		Evidence: []Evidence{{
			Type:       seed.Evidence,
			ObservedAt: seed.EventCreatedAt.UTC(),
		}},
	}
}

func (s *CloudSource) Resolve(ctx context.Context, candidate Candidate) (*ResolvedSource, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if s == nil {
		return nil, errors.New("cloud source is required")
	}
	switch candidate.Kind {
	case ObjectKindMgmtCluster:
		return s.resolveMgmt(ctx, candidate)
	case ObjectKindResourcePool:
		return s.resolvePool(ctx, candidate)
	default:
		return nil, ErrSourceUnsupported
	}
}

func (s *CloudSource) resolveMgmt(ctx context.Context, candidate Candidate) (*ResolvedSource, error) {
	id, err := parsePositiveID(candidate.ClusterID)
	if err != nil {
		return nil, fmt.Errorf("invalid candidate cluster ID: %w", err)
	}
	if s.getMgmtClusters == nil {
		return nil, errors.New("mgmt source reader is required")
	}
	clusters, err := s.getMgmtClusters(internalUser(), []uint64{id})
	if err != nil {
		return nil, fmt.Errorf("read mgmt source: %w", err)
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if len(clusters) == 0 {
		return nil, ErrSourceNotFound
	}
	if len(clusters) != 1 || clusters[0] == nil || clusters[0].GetClusterId() != id {
		return nil, ErrSourceIncomplete
	}
	cluster := clusters[0]
	if cluster.GetOrganizationId() != candidate.TenantID {
		return nil, ErrSourceOwnership
	}
	updatedAt, err := sourceTimestamp(cluster.GetUpdateTime(), cluster.GetCreateTime())
	if err != nil {
		return nil, err
	}
	resolved := &ResolvedSource{
		Candidate:       candidate,
		SourceUpdatedAt: updatedAt,
		Terminal:        cluster.GetDeleteTime() != nil,
	}
	if cluster.GetCreateTime() == nil || !cluster.GetCreateTime().IsValid() {
		return nil, ErrSourceIncomplete
	}
	if err := validateMgmtType(candidate, cluster); err != nil {
		return nil, err
	}
	desired := utils.MgmtClusterToModel(0, cluster)
	if desired.Status == "" {
		return nil, ErrSourceIncomplete
	}
	if err := validateDesiredLifecycle(desired); err != nil {
		return nil, ErrSourceIncomplete
	}
	if resolved.Terminal != terminalStatus(desired.Status) {
		return nil, ErrSourceIncomplete
	}
	resolved.Desired = desired
	if !resolved.Terminal && desired.ParentID != "" && desired.ParentID != "0" {
		if _, err := parsePositiveID(desired.ParentID); err != nil {
			return nil, ErrSourceIncomplete
		}
		observedAt := updatedAt
		if len(candidate.Evidence) > 0 && !candidate.Evidence[0].ObservedAt.IsZero() {
			observedAt = candidate.Evidence[0].ObservedAt.UTC()
		}
		parent := Candidate{
			Kind:        ObjectKindResourcePool,
			ClusterID:   desired.ParentID,
			TenantID:    candidate.TenantID,
			ClusterType: cloudevent.ClusterTypeTidbxResourcePool,
			Evidence: []Evidence{{
				Type:       EvidenceDerivedParent,
				ObservedAt: observedAt,
				ParentOf:   candidate.ClusterID,
			}},
		}
		resolved.Parent = &parent
	}
	return resolved, nil
}

func (s *CloudSource) resolvePool(ctx context.Context, candidate Candidate) (*ResolvedSource, error) {
	id, err := parsePositiveID(candidate.ClusterID)
	if err != nil {
		return nil, fmt.Errorf("invalid candidate resource-pool ID: %w", err)
	}
	if s.listPools == nil || s.getPool == nil {
		return nil, errors.New("resource-pool source readers are required")
	}
	profiles, err := s.listPools(internalUser(), []uint64{id})
	if err != nil {
		return nil, fmt.Errorf("list resource-pool source: %w", err)
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if len(profiles) == 0 {
		return nil, ErrSourceNotFound
	}
	if len(profiles) != 1 || profiles[0] == nil || profiles[0].GetResourcePool() == nil || profiles[0].GetResourcePool().GetId() != id {
		return nil, ErrSourceIncomplete
	}
	listedPool := profiles[0].GetResourcePool()
	if err := validateResourcePoolOwnership(listedPool, candidate.TenantID); err != nil {
		return nil, err
	}
	updatedAt, err := sourceTimestamp(listedPool.GetUpdateTime(), listedPool.GetCreateTime())
	if err != nil {
		return nil, err
	}
	resolved := &ResolvedSource{
		Candidate:       candidate,
		SourceUpdatedAt: updatedAt,
		Terminal:        listedPool.GetDeleteTime() != nil,
	}
	if listedPool.GetCreateTime() == nil || !listedPool.GetCreateTime().IsValid() {
		return nil, ErrSourceIncomplete
	}
	profile, sharedPool, err := s.getPool(internalUser(), id)
	if err != nil {
		return nil, fmt.Errorf("read resource-pool source: %w", err)
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if profile == nil || profile.GetResourcePool() == nil || profile.GetResourcePool().GetId() != id {
		return nil, ErrSourceIncomplete
	}
	pool := profile.GetResourcePool()
	if err := validateResourcePoolOwnership(pool, candidate.TenantID); err != nil {
		return nil, err
	}
	if (pool.GetDeleteTime() != nil) != resolved.Terminal {
		return nil, ErrSourceIncomplete
	}
	profileUpdatedAt, err := sourceTimestamp(pool.GetUpdateTime(), pool.GetCreateTime())
	if err != nil || !profileUpdatedAt.Equal(updatedAt) {
		return nil, ErrSourceIncomplete
	}
	if pool.GetCreateTime() == nil || !pool.GetCreateTime().IsValid() ||
		!pool.GetCreateTime().AsTime().UTC().Equal(listedPool.GetCreateTime().AsTime().UTC()) {
		return nil, ErrSourceIncomplete
	}
	if sharedPool == nil && !resolved.Terminal {
		return nil, ErrSourceIncomplete
	}
	desired := utils.ResourcePoolToModel(0, candidate.TenantID, profile, sharedPool)
	if desired.DeployType == "" || desired.DeployTypeV2 == "" {
		return nil, ErrSourceUnsupported
	}
	if desired.Status == "" {
		return nil, ErrSourceIncomplete
	}
	if err := validateDesiredLifecycle(desired); err != nil {
		return nil, ErrSourceIncomplete
	}
	if resolved.Terminal != terminalStatus(desired.Status) {
		return nil, ErrSourceIncomplete
	}
	resolved.Desired = desired
	return resolved, nil
}

func validateResourcePoolOwnership(pool *nextgenv1.ResourcePool, tenantID uint64) error {
	if pool == nil {
		return ErrSourceIncomplete
	}
	switch pool.GetOwnerType() {
	case nextgenv1.ResourcePool_SHARED:
		if tenantID != 0 || pool.GetOwnerId() != 0 {
			return ErrSourceOwnership
		}
	case nextgenv1.ResourcePool_ORGANIZATION:
		if tenantID == 0 || pool.GetOwnerId() != tenantID {
			return ErrSourceOwnership
		}
	default:
		return ErrSourceUnsupported
	}
	return nil
}

func validateMgmtType(_ Candidate, cluster *mgmtv1.Cluster) error {
	// Clinic's live ClusterEvent handler intentionally routes both
	// dedicated_cluster and tidbx_instance through the same Mgmt lookup and
	// converter. The event taxonomy can therefore differ from the cluster's
	// current authoritative deploy type (for example, Starter/Essential events
	// observed as tidbx_instance). CandidateFromSeed has already restricted the
	// event taxonomy; validate the current source payload instead of requiring
	// the historical event label to match it.
	switch cluster.GetDeployType() {
	case mgmtv1.DeployType_DEPLOY_TYPE_DEDICATED:
		if cluster.GetDedicatedCluster() == nil {
			return ErrSourceIncomplete
		}
	case mgmtv1.DeployType_DEPLOY_TYPE_SERVERLESS:
		if cluster.GetServerlessCluster() == nil {
			return ErrSourceIncomplete
		}
	case mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN:
		if cluster.GetNextGenCluster() == nil {
			return ErrSourceIncomplete
		}
	default:
		return ErrSourceUnsupported
	}
	return nil
}

func sourceTimestamp(update, create *timestamppb.Timestamp) (time.Time, error) {
	for _, timestamp := range []*timestamppb.Timestamp{update, create} {
		if timestamp != nil && timestamp.IsValid() {
			return timestamp.AsTime().UTC(), nil
		}
	}
	return time.Time{}, ErrSourceIncomplete
}

func terminalStatus(status string) bool {
	switch strings.ToLower(status) {
	case "deleted", "deletecomplete", "cleared":
		return true
	default:
		return false
	}
}

func internalUser() *session.CtxUser {
	return &session.CtxUser{Internal: true, Email: "clinic@pingcap.com"}
}
