package repository

import (
	"context"
	"errors"
	"slices"
	"strings"
	"time"

	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	repo "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/infra/repository/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

type customStatusRepositoryImpl struct {
	dbProvider db.Provider
}

func NewCustomStatusRepository(dbProvider db.Provider) repo.CustomStatusRepository {
	return customStatusRepositoryImpl{dbProvider: dbProvider}
}

func (c customStatusRepositoryImpl) LockClustersForUpdate(ctx context.Context, clusterIds []uint64) error {
	if len(clusterIds) == 0 {
		return nil
	}
	ids := append([]uint64(nil), clusterIds...)
	slices.Sort(ids)
	uniqueIDs := ids[:0]
	for _, id := range ids {
		if len(uniqueIDs) == 0 || uniqueIDs[len(uniqueIDs)-1] != id {
			uniqueIDs = append(uniqueIDs, id)
		}
	}
	var clusters []schema.Cluster
	if err := c.dbProvider.WriteDB(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("cluster_id IN ?", uniqueIDs).
		Order("cluster_id ASC").
		Select("cluster_id").
		Find(&clusters).Error; err != nil {
		return err
	}
	if len(clusters) != len(uniqueIDs) {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (c customStatusRepositoryImpl) NewClusterCustomStatus(
	ctx context.Context,
	status *entity.ClusterCustomStatus,
) (uint64, error) {
	wdb := c.dbProvider.WriteDB(ctx)

	statusRecord := cvtStatusRecord(status)

	err := wdb.Create(statusRecord).Error
	if err != nil {
		return 0, err
	}
	return statusRecord.StatusID, nil
}

func (c customStatusRepositoryImpl) ClearClusterCustomStatusByClusterId(ctx context.Context, clusterId uint64) error {
	wdb := c.dbProvider.WriteDB(ctx)

	err := wdb.Model(&schema.ClusterCustomStatus{}).
		Where("cluster_id = ?", clusterId).
		Delete(&schema.ClusterCustomStatus{}).
		Error
	if err != nil {
		return err
	}
	return nil
}

func (c customStatusRepositoryImpl) ClearExpiredClusterCustomStatus(ctx context.Context) error {
	wdb := c.dbProvider.WriteDB(ctx)

	err := wdb.Model(&schema.ClusterCustomStatus{}).
		Where("DATE_ADD(created_at, INTERVAL timeout_secs second) > NOW() AND timeout_secs IS NOT NULL").
		Delete(&schema.ClusterCustomStatus{}).
		Error
	if err != nil {
		return err
	}
	return nil
}

func (c customStatusRepositoryImpl) GetClusterCustomStatusById(
	ctx context.Context,
	clusterId, statusId uint64,
) (*entity.ClusterCustomStatus, error) {
	rdb := c.dbProvider.ReadDB(ctx)

	var status schema.ClusterCustomStatus
	err := rdb.Model(&schema.ClusterCustomStatus{}).
		Where("cluster_id = ? AND status_id = ?", clusterId, statusId).
		First(&status).
		Error
	if err != nil {
		return nil, err
	}

	return cvtStatusEntity(&status), nil
}

func (c customStatusRepositoryImpl) GetClusterCustomStatusByName(
	ctx context.Context,
	clusterId uint64,
	statusName string,
	statusType tidbmgmtservicev1.CustomStatusType,
) (*entity.ClusterCustomStatus, error) {
	rdb := c.dbProvider.ReadDB(ctx)

	var status schema.ClusterCustomStatus
	err := rdb.Model(&schema.ClusterCustomStatus{}).Where("cluster_id = ? AND status_type = ? AND status_name = ?",
		clusterId, statusType.String(), statusName).First(&status).Error
	if err != nil {
		return nil, err
	}
	return cvtStatusEntity(&status), nil
}

func (c customStatusRepositoryImpl) GetRestoreStatusByName(
	ctx context.Context,
	statusName string,
) (*entity.ClusterCustomStatus, error) {
	rdb := c.dbProvider.ReadDB(ctx)

	var status schema.ClusterCustomStatus
	err := rdb.Model(&schema.ClusterCustomStatus{}).Where("status_type = ? AND status_name = ?",
		tidbmgmtservicev1.CustomStatusType_CUSTOM_STATUS_TYPE_RESTORE.String(), statusName).First(&status).Error
	if err != nil {
		tilog.Warn(ctx, "failed to get restore status", zap.Error(err))
		return nil, err
	}
	return cvtStatusEntity(&status), nil
}

func (c customStatusRepositoryImpl) IsMutexClusterAction(
	ctx context.Context,
	clusterId uint64,
	clusterAction tidbmgmtservicev1.ClusterAction,
) (bool, *entity.ClusterCustomStatus, error) {
	rdb := c.dbProvider.ReadDB(ctx)

	var statuses []*schema.ClusterCustomStatus
	err := rdb.Model(&schema.ClusterCustomStatus{}).Where("cluster_id = ?", clusterId).Find(&statuses).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, nil, nil
		}
		return false, nil, err
	}

	statusesEntity := make([]*entity.ClusterCustomStatus, 0, len(statuses))
	for _, status := range statuses {
		statusesEntity = append(statusesEntity, cvtStatusEntity(status))
	}

	for _, statusEntity := range statusesEntity {
		if statusEntity.IsExpired() {
			continue
		}

		if statusEntity.IsActionMutexed(clusterAction) {
			reqMeta, err := metadata.ExtractRequestMeta(ctx)
			if err != nil {
				tilog.Warn(ctx, "failed to extract request metadata", zap.Error(err))
			} else if statusEntity.StatusId == reqMeta.IgnoreCustomStatusID {
				// if status is ignored, skip it
				continue
			}
			return true, statusEntity, nil
		}
	}

	return false, nil, nil
}

func (c customStatusRepositoryImpl) IsMutexCustomStatus(
	ctx context.Context,
	status *entity.ClusterCustomStatus,
) (bool, *entity.ClusterCustomStatus, error) {
	rdb := c.dbProvider.ReadDB(ctx)

	var statuses []*schema.ClusterCustomStatus
	err := rdb.Model(&schema.ClusterCustomStatus{}).Where("cluster_id = ?", status.ClusterId).Find(&statuses).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, nil, nil
		}
		return false, nil, err
	}

	statusesEntity := make([]*entity.ClusterCustomStatus, 0, len(statuses))
	for _, status := range statuses {
		statusesEntity = append(statusesEntity, cvtStatusEntity(status))
	}

	for _, statusEntity := range statusesEntity {
		if statusEntity.IsExpired() {
			continue
		}
		if statusEntity.IsStatusMutexed(status.StatusType) || status.IsStatusMutexed(statusEntity.StatusType) {
			return true, statusEntity, nil
		}
	}

	return false, nil, nil
}

func (c customStatusRepositoryImpl) DeleteClusterCustomStatus(ctx context.Context, statusId uint64) error {
	wdb := c.dbProvider.WriteDB(ctx)
	err := wdb.Model(&schema.ClusterCustomStatus{}).
		Where("status_id = ?", statusId).
		Delete(&schema.ClusterCustomStatus{}).
		Error
	if err != nil {
		return err
	}
	return nil
}

func (c customStatusRepositoryImpl) DeleteClusterCustomStatusByCluster(ctx context.Context, clusterId uint64) error {
	wdb := c.dbProvider.WriteDB(ctx)
	err := wdb.Model(&schema.ClusterCustomStatus{}).
		Where("cluster_id = ?", clusterId).
		Delete(&schema.ClusterCustomStatus{}).
		Error
	if err != nil {
		return err
	}
	return nil
}

func (c customStatusRepositoryImpl) ListClusterCustomStatus(
	ctx context.Context,
	clusterIds []uint64,
	statusTypes []tidbmgmtservicev1.CustomStatusType,
	queryDeleted bool,
) (map[uint64][]*entity.ClusterCustomStatus, error) {
	rdb := c.dbProvider.ReadDB(ctx)

	var statuses []*schema.ClusterCustomStatus

	tx := rdb.Model(&schema.ClusterCustomStatus{})

	if queryDeleted {
		tx = tx.Unscoped()
	}
	if len(statusTypes) > 0 {
		typesStr := make([]string, 0, len(statusTypes))
		for _, t := range statusTypes {
			typesStr = append(typesStr, t.String())
		}
		tx = tx.Where("status_type in ?", typesStr)
	}

	if len(clusterIds) > 0 {
		tx = tx.Where("cluster_id in ?", clusterIds)
	}

	// sort
	// TODO: order by other fields
	tx.Order("created_at desc")

	err := tx.Find(&statuses).Error
	if err != nil {
		return nil, err
	}
	cluster2statuses := map[uint64][]*entity.ClusterCustomStatus{}
	for _, status := range statuses {
		list := cluster2statuses[status.ClusterID]
		list = append(list, cvtStatusEntity(status))
		cluster2statuses[status.ClusterID] = list
	}
	return cluster2statuses, nil
}

func cvtStatusRecord(status *entity.ClusterCustomStatus) *schema.ClusterCustomStatus {
	c := &schema.ClusterCustomStatus{
		StatusID:    status.StatusId,
		ClusterID:   status.ClusterId,
		StatusType:  status.StatusType.String(),
		StatusName:  status.StatusName,
		TimeoutSecs: status.TimeoutSecs,
	}

	if status.CustomMutex != nil {
		c.CustomMutex = &schema.CustomMutex{}
		for _, a := range status.CustomMutex.ClusterAction {
			c.CustomMutex.ClusterAction = append(c.CustomMutex.ClusterAction, a.String())
		}
		for _, t := range status.CustomMutex.StatusType {
			c.CustomMutex.StatusType = append(c.CustomMutex.StatusType, t.String())
		}
	}

	if status.OverrideState != nil {
		c.OverrideState = proto.String(status.OverrideState.String())
	}

	return c
}

func cvtStatusEntity(statusRecord *schema.ClusterCustomStatus) *entity.ClusterCustomStatus {
	var deletedAt *time.Time
	if statusRecord.DeletedAt.Valid {
		deletedAt = &statusRecord.DeletedAt.Time
	}

	s := &entity.ClusterCustomStatus{
		StatusId:   statusRecord.StatusID,
		StatusName: statusRecord.StatusName,
		StatusType: tidbmgmtservicev1.CustomStatusType(
			tidbmgmtservicev1.CustomStatusType_value[strings.ToUpper(statusRecord.StatusType)],
		),
		ClusterId:   statusRecord.ClusterID,
		TimeoutSecs: statusRecord.TimeoutSecs,
		CreatedAt:   statusRecord.CreatedAt,
		DeleteAt:    deletedAt,
	}
	if statusRecord.CustomMutex != nil {
		s.CustomMutex = &tidbmgmtservicev1.ClusterCustomStatus_CustomMutex{}
		for _, a := range statusRecord.CustomMutex.ClusterAction {
			s.CustomMutex.ClusterAction = append(
				s.CustomMutex.ClusterAction,
				tidbmgmtservicev1.ClusterAction(tidbmgmtservicev1.ClusterAction_value[strings.ToUpper(a)]),
			)
		}
		for _, t := range statusRecord.CustomMutex.StatusType {
			s.CustomMutex.StatusType = append(
				s.CustomMutex.StatusType,
				tidbmgmtservicev1.CustomStatusType(tidbmgmtservicev1.CustomStatusType_value[strings.ToUpper(t)]),
			)
		}
	}

	if statusRecord.OverrideState != nil {
		state := openapicommonv1beta1.ClusterState(
			openapicommonv1beta1.ClusterState_value[strings.ToUpper(*statusRecord.OverrideState)],
		)
		s.OverrideState = &state
	}

	return s
}
