package apikey

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

func AuthnApiKey(ctx context.Context, ak, sk string) (*AuthnApiKeyRet, error) {
	bm := utils.Begin(ctx, "AuthnApiKey", zap.Any("ak", ak))
	defer bm.OnExit(ctx)

	ret := &AuthnApiKeyRet{
		AuthRet: false,
		Message: "authenticate error",
	}
	if ak == "" || sk == "" {
		ret.Message = "invalid param"
		return nil, nil
	}
	keyEnt, err := GetApiKeyEntWithCache(ctx, 0, ak)
	if err != nil {
		tilog.Error(ctx, "AuthnApiKey GetApiKeyByAk error", zap.Any("ak", ak), zap.Any("err", err))
		return ret, errs.NewInternalDB("get api_key by ak error", err)
	}
	if keyEnt == nil {
		tilog.Info(ctx, "AuthnApiKey fail, not found key ent", zap.Any("ak", ak))
		ret.Message = "not found api_key by access_key"
		return ret, nil
	}
	if keyEnt.ExpiredAt != nil && time.Now().After(*keyEnt.ExpiredAt) {
		tilog.Info(ctx, "AuthnApiKey fail, key has expired", zap.Any("ak", ak))
		ret.Message = "api_key has expired"
		return ret, nil
	}
	encSk := GetKeyUtil().EncryptKey(sk)
	if encSk != keyEnt.SecretKey {
		tilog.Info(ctx, "AuthnApiKey fail, sk check fail")
		ret.Message = "sk check fail"
		return ret, nil
	}
	bindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg, entity.RoleBindingScopeProject},
		TargetType: []entity.TargetType{entity.RoleBindingTargetAPIKey},
		TargetIDs:  []entity.ID{keyEnt.ID},
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		return ret, errs.NewInternalDB("get api_key role bindings error", err)
	}
	resourceInfos, err := authnResourceInfosFromRoleBindings(bindings)
	if err != nil {
		ret.Message = err.Error()
		return ret, nil
	}
	tilog.Info(ctx, "AuthnApiKey success")

	ret.AuthRet = true
	ret.Message = "authenticate success"
	ret.ResourceInfos = resourceInfos
	return ret, nil
}

func authnResourceInfosFromRoleBindings(bindings []entity.RoleBinding) ([]AuthnApiKeyResourceInfo, error) {
	if _, err := organizationIDFromRoleBindings(bindings); err != nil {
		return nil, err
	}

	resourceInfos := make([]AuthnApiKeyResourceInfo, 0, len(bindings))
	for _, binding := range bindings {
		if binding.Status != entity.RoleBindingStatusActive {
			continue
		}
		if binding.ScopeType != entity.RoleBindingScopeOrg && binding.ScopeType != entity.RoleBindingScopeProject {
			continue
		}
		resourceInfos = append(resourceInfos, AuthnApiKeyResourceInfo{
			ScopeType:  binding.ScopeType.String(),
			ResourceID: binding.ScopeID.Uint64(),
			Role:       binding.Role,
			RoleID:     binding.RoleID.Uint64(),
		})
	}
	return resourceInfos, nil
}

func organizationIDFromRoleBindings(bindings []entity.RoleBinding) (uint64, error) {
	var organizationID uint64
	for _, binding := range bindings {
		if binding.Status != entity.RoleBindingStatusActive || binding.ScopeType != entity.RoleBindingScopeOrg {
			continue
		}
		candidate := binding.ScopeID.Uint64()
		if organizationID != 0 && organizationID != candidate {
			return 0, fmt.Errorf("api_key has ambiguous organization bindings")
		}
		organizationID = candidate
	}
	if organizationID == 0 {
		return 0, fmt.Errorf("api_key has no active organization binding")
	}
	return organizationID, nil
}
