package apikey

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

	ddbDao "github.com/tidbcloud/account/internel/dao/ddb"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/errs"
	"gorm.io/gorm"
)

const (
	tableName = "uni_api_keys"
)

type ApiKeyDaoIface interface {
	CreateApiKey(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, key *entity.ApiKey) (*entity.ApiKey, error)
	GetApiKeyByID(ctx context.Context, tx *gorm.DB, id uint64) (*entity.ApiKey, error)
	GetApiKeyByAk(ctx context.Context, tx *gorm.DB, ak string) (*entity.ApiKey, error)
	DeleteApiKey(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, key *entity.ApiKey, operator string) error
	UpdateApiKey(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, key *entity.ApiKey) error
	ListApiKeys(ctx context.Context, tx *gorm.DB, param *ListApiKeysParam) (*ListApiKeysResult, error)
	TraverseApiKeys(ctx context.Context, tx *gorm.DB, minId uint64, limit int) ([]entity.ApiKey, error)
	CheckAkExistInDB(ctx context.Context, tx *gorm.DB, ak string) (bool, error)
	BatchUpdateExpiredAtByIdIn(ctx context.Context, tx *gorm.DB, keyIds []uint64, expiredAt *time.Time) error
	ListOrgApiKeys(ctx context.Context, tx *gorm.DB, f *OrgApiKeyFilter) ([]OrgApiKey, error)
}

type ApiKeyDao struct {
}

func NewApiKeyDao() ApiKeyDaoIface {
	return &ApiKeyDao{}
}

func (dao *ApiKeyDao) CreateApiKey(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, key *entity.ApiKey) (*entity.ApiKey, error) {
	err := tx.Transaction(func(tx *gorm.DB) error {
		err := tx.Table(tableName).Save(key).Error
		if err != nil {
			return err
		}
		updateDdbMap := map[string]any{
			"private_key":    key.SecretKey,
			"rate_limit_rpm": key.RateLimitRpm,
		}
		if key.ExpiredAt != nil {
			updateDdbMap["expired_ts"] = key.ExpiredAt.UnixMilli()
		}
		return ddbDao.UpdateApiKey(ctx, ddb, key.AccessKey, updateDdbMap)
	})
	if err != nil {
		return nil, errs.WrapGORMError(err, fmt.Sprintf("create ApiKey failed: %+v", key))
	}
	return key, nil
}

func (dao *ApiKeyDao) GetApiKeyByID(ctx context.Context, tx *gorm.DB, id uint64) (*entity.ApiKey, error) {
	res := &entity.ApiKey{}
	err := tx.Table(tableName).Where("id=? and status=?", id, entity.Active).First(res).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, errs.WrapGORMError(err, fmt.Sprintf("get db ApiKey failed: %+v", id))
	}
	return res, nil
}

func (dao *ApiKeyDao) GetApiKeyByAk(ctx context.Context, tx *gorm.DB, ak string) (*entity.ApiKey, error) {
	res := &entity.ApiKey{}
	err := tx.Table(tableName).Where("access_key=? and status=?", ak, entity.Active).First(res).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, errs.WrapGORMError(err, fmt.Sprintf("get db ApiKey failed: %+v", ak))
	}
	return res, nil
}

func (dao *ApiKeyDao) CheckAkExistInDB(ctx context.Context, tx *gorm.DB, ak string) (bool, error) {
	res := &entity.ApiKey{}
	err := tx.Table(tableName).Where("access_key=?", ak).First(res).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, nil
		}
		return false, errs.WrapGORMError(err, fmt.Sprintf("get db ApiKey failed: %+v", ak))
	}
	return true, nil
}

func (dao *ApiKeyDao) DeleteApiKey(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, key *entity.ApiKey, operator string) error {
	err := tx.Transaction(func(tx *gorm.DB) error {
		res := tx.Table(tableName).Where("id=?", key.ID).Updates(map[string]interface{}{
			"status":     entity.Delete,
			"updated_by": operator,
		})
		if res.Error != nil {
			return res.Error
		}
		if res.RowsAffected != 1 {
			return res.Error
		}
		return ddbDao.DeleteApiKey(ctx, ddb, key.AccessKey)
	})
	if err != nil {
		return errs.WrapGORMError(err, fmt.Sprintf("delete ApiKey failed: %+v", key.ID))
	}
	return nil
}

// UpdateApiKey update the description and updated_by of an ApiKey.
func (dao *ApiKeyDao) UpdateApiKey(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, key *entity.ApiKey) error {
	updateMap := map[string]interface{}{}

	if key.Description != "" {
		updateMap["description"] = key.Description
	}
	if key.UpdatedBy != "" {
		updateMap["updated_by"] = key.UpdatedBy
	}
	if key.BizAttr != "" {
		updateMap["biz_attr"] = key.BizAttr
	}
	if key.UpdatedAt != nil {
		updateMap["updated_at"] = key.UpdatedAt
	}

	updateDdbMap := make(map[string]any)

	if key.RateLimitRpm != 0 {
		updateMap["rate_limit_rpm"] = key.RateLimitRpm
		updateDdbMap["rate_limit_rpm"] = key.RateLimitRpm
	}
	if key.ExpiredAt != nil {
		updateMap["expired_at"] = key.ExpiredAt
		updateDdbMap["expired_ts"] = key.ExpiredAt.UnixMilli()
	}

	err := tx.Table(tableName).Where("id=?", key.ID.Uint64()).Updates(updateMap).Error
	if err != nil {
		return errs.WrapGORMError(err, fmt.Sprintf("update ApiKey failed: %+v", key))
	}
	return ddbDao.UpdateApiKey(ctx, ddb, key.AccessKey, updateDdbMap)
}

func (dao *ApiKeyDao) BatchUpdateExpiredAtByIdIn(ctx context.Context, tx *gorm.DB, keyIds []uint64, expiredAt *time.Time) error {
	err := tx.Table(tableName).Where("id in (?)", keyIds).Update("expired_at", expiredAt).Error
	if err != nil {
		return errs.WrapGORMError(err, fmt.Sprintf("batch update ApiKey expird_at failed: %+v", keyIds))
	}
	return nil
}

func (dao *ApiKeyDao) ListApiKeys(ctx context.Context, tx *gorm.DB, param *ListApiKeysParam) (*ListApiKeysResult, error) {
	tx = tx.Table(tableName).Where("status=?", entity.Active)
	filter := param.Filter
	if len(filter.ID) > 0 {
		tx = tx.Where("id in (?) ", param.Filter.ID)
	}
	if filter.AKLike != "" {
		tx = tx.Where("access_key like ?", "%"+filter.AKLike+"%")
	}
	if filter.Type != "" {
		tx = tx.Where("type = ?", filter.Type)
	}
	if filter.Source != "" {
		tx = tx.Where("source = ?", filter.Source)
	}
	if filter.DescLike != "" {
		tx = tx.Where("description like ?", "%"+filter.DescLike+"%")
	}
	if filter.CreatedBy != "" {
		tx = tx.Where("created_by = ?", filter.CreatedBy)
	}
	if filter.UpdatedBy != "" {
		tx = tx.Where("updated_by = ?", filter.UpdatedBy)
	}
	var sortBy = "id"
	if param.SortBy != "" {
		sortBy = param.SortBy
	}
	var order = "desc"
	if param.Order != "" {
		order = param.Order
	}
	tx = tx.Order(fmt.Sprintf("%s %s", sortBy, order))

	var total int64
	if err := tx.Count(&total).Error; err != nil {
		return nil, err
	}

	if param.Limit > 0 && param.Offset >= 0 {
		tx = tx.Limit(param.Limit).Offset(param.Offset)
	}

	var apiKeys []entity.ApiKey
	if err := tx.Find(&apiKeys).Error; err != nil {
		return nil, err
	}

	result := ListApiKeysResult{
		Total: total,
		List:  apiKeys,
	}
	return &result, nil
}

// TraverseApiKeys traverse the apiKey table and return the result.
// Note: ListApiKeysParam.Filter is not used.
func (dao *ApiKeyDao) TraverseApiKeys(ctx context.Context, tx *gorm.DB, minId uint64, limit int) ([]entity.ApiKey, error) {
	tx = tx.Table(tableName).Where("id > ?", minId)
	tx = tx.Order("id asc")
	if limit > 0 {
		tx = tx.Limit(limit)
	}

	var apiKeys []entity.ApiKey
	if err := tx.Find(&apiKeys).Error; err != nil {
		return nil, err
	}

	return apiKeys, nil
}

func (dao *ApiKeyDao) ListOrgApiKeys(ctx context.Context, tx *gorm.DB, f *OrgApiKeyFilter) ([]OrgApiKey, error) {
	tx = tx.Select("k.*, rb.role, rb.role_id, rb.status as role_status").
		Table("role_bindings rb").
		Joins("inner join uni_api_keys k on (rb.scope_type = 'ORG' and rb.scope_id = ? and rb.target_type = 'APIKEY' and k.id = rb.target_id)", f.OrgID).
		Where("rb.status = 'active' and k.status = 'active'")
	if len(f.OrgRoles) > 0 {
		tx = tx.Where("rb.role in (?)", f.OrgRoles)
	}
	if f.KeyLike != "" {
		m := "%" + strings.ToLower(f.KeyLike) + "%"
		tx = tx.Where("(lower(access_key) like ? or lower(description) like ?)", m, m)
	}
	if f.Type != "" {
		tx = tx.Where("type = ?", f.Type)
	}
	if f.Source != "" {
		tx = tx.Where("source = ?", f.Source)
	}
	if f.CreatedBy != "" {
		tx = tx.Where("created_by = ?", f.CreatedBy)
	}
	if f.UpdatedBy != "" {
		tx = tx.Where("updated_by = ?", f.UpdatedBy)
	}

	tx = tx.Order("created_at desc")

	var keys []OrgApiKey
	if err := tx.Find(&keys).Error; err != nil {
		return nil, err
	}

	return keys, nil
}
