package dynamodbcheck

import (
	"context"
	"fmt"
	"reflect"
	"time"

	"github.com/tidbcloud/account/internel/dao"
	ddbDao "github.com/tidbcloud/account/internel/dao/ddb"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// DynamoDB has a limit of 100 items per batchGetItem request
const BatchSize = 100

// RunApiKeyCheck compares the apiKeys in MySQL and DynamoDB
// If an apiKey is deleted in MySQL but still exists in DynamoDB, it will be deleted from DynamoDB
// If an apiKey is not deleted in MySQL but not exists in DynamoDB, it will be inserted into DynamoDB
// If an apiKey is not deleted in MySQL but exists in DynamoDB but with different attributes, it will be updated in DynamoDB
func RunApiKeyCheck(ctx context.Context, db *gorm.DB, ddb *ddbDao.DynamoDBClient, daoHub *dao.DaoHub) error {
	var id uint64 = 0
	for {
		time.Sleep(time.Millisecond * 300)
		tilog.Info(ctx, fmt.Sprintf("RunApiKeyCheck min id is %d", id))
		result, err := daoHub.ApiKey.TraverseApiKeys(ctx, db, id, BatchSize)
		if err != nil {
			tilog.Error(ctx, "failed to traverse apiKey table", zap.Error(err))
			return err
		}
		if len(result) == 0 {
			break
		}

		for _, oldApiKey := range result {
			oid := oldApiKey.ID.Uint64()
			if oid > id {
				id = oid
			}
			// read apiKey from MySQL again to make sure its data is latest as possible
			// it's better to use a transaction to lock the row
			apiKey, err := daoHub.ApiKey.GetApiKeyByID(ctx, db, oldApiKey.ID.Uint64())
			if err != nil {
				tilog.Error(ctx, "failed to get apiKey by ID", zap.Uint64("ID", oldApiKey.ID.Uint64()), zap.Error(err))
				return err
			}
			ddbApiKey, err := ddbDao.GetApiKey(ctx, ddb, oldApiKey.AccessKey)
			if err != nil {
				tilog.Warn(ctx, "failed to get apiKey from DynamoDB", zap.Uint64("ID", oldApiKey.ID.Uint64()), zap.Error(err))
				return err
			}
			if apiKey == nil {
				// apiKey is deleted in MySQL
				if ddbApiKey != nil {
					tilog.Warn(ctx, "apiKey is deleted but still exists in DynamoDB", zap.Int("ID", int(oldApiKey.ID)))
					err := ddbDao.DeleteApiKey(ctx, ddb, oldApiKey.AccessKey)
					if err != nil {
						tilog.Error(ctx, "failed to delete apiKey from DynamoDB", zap.Uint64("ID", oldApiKey.ID.Uint64()), zap.Error(err))
						return err
					}
					tilog.Info(ctx, "successfully deleted from DynamoDB", zap.Uint64("ID", oldApiKey.ID.Uint64()))
				}
			} else {
				roleBindings, err := daoHub.Role.ListDataAppRoleBindingsByApiKeyID(ctx, db, apiKey.ID.Uint64())
				if err != nil {
					tilog.Error(ctx, "failed to list dataapp roleBindings", zap.Error(err))
					return err
				}
				if ddbApiKey != nil {
					err = compareDynamoDBAndTiDB(ctx, db, ddb, daoHub, apiKey, ddbApiKey, roleBindings)
					if err != nil {
						tilog.Error(ctx, "failed to compare data between DynamoDB And TiDB", zap.Error(err))
						return err
					}
				} else {
					tilog.Warn(ctx, "apiKey does not exists in DynamoDB", zap.Int("ID", int(apiKey.ID)))
					err := ddbDao.CreateApiKey(ctx, ddb, apiKey.AccessKey, apiKey.SecretKey, apiKey.RateLimitRpm, roleBindings)
					if err != nil {
						return err
					}
					tilog.Info(ctx, "successfully created apiKey in DynamoDB", zap.Int("ID", int(apiKey.ID)))
				}
			}
		}
	}
	return nil
}

// compareDynamoDBAndMySQL compares the data in MySQL and DynamoDB, and update the data in DynamoDB if necessary
func compareDynamoDBAndTiDB(
	ctx context.Context, db *gorm.DB, ddb *ddbDao.DynamoDBClient, daoHub *dao.DaoHub,
	apiKey *entity.ApiKey, ddbApiKey *ddbDao.DdbApiKey, roleBindings []entity.RoleBinding,
) error {
	if apiKey.SecretKey != ddbApiKey.PrivateKey || apiKey.RateLimitRpm != ddbApiKey.RateLimitRpm {
		tilog.Warn(ctx, "apiKey secretKey does not match with DynamoDB record", zap.Int("ID", int(apiKey.ID)), zap.Any("apiKey", apiKey), zap.Any("ddbApiKey", ddbApiKey))
		updateDdbMap := map[string]any{
			"private_key":    apiKey.SecretKey,
			"rate_limit_rpm": apiKey.RateLimitRpm,
		}

		err := ddbDao.UpdateApiKey(ctx, ddb, apiKey.AccessKey, updateDdbMap)
		if err != nil {
			return err
		}
		tilog.Info(ctx, "successfully updated apiKey secretKey in DynamoDB", zap.Int("ID", int(apiKey.ID)))
	}

	appRoleMap := ddbDao.BuildAppRoleMap(roleBindings)
	if len(appRoleMap) == 0 && len(ddbApiKey.AppRoleMap) == 0 {
		// nil map and empty map are considered equal
		return nil
	}
	if !reflect.DeepEqual(appRoleMap, ddbApiKey.AppRoleMap) {
		tilog.Warn(ctx, "apiKey appRoleMap does not match with DynamoDB record", zap.Int("ID", int(apiKey.ID)), zap.Any("ddbRoleMap", ddbApiKey.AppRoleMap), zap.Any("tidbRoleMap", appRoleMap))
		err := daoHub.Role.SaveDataAppRoleBindingsToDynamodbByApiKeyID(ctx, db, ddb, apiKey.ID.Uint64())
		if err != nil {
			return err
		}
		tilog.Info(ctx, "successfully updated apiKey appRoleMap in DynamoDB", zap.Int("ID", int(apiKey.ID)))
	}
	return nil
}
