package orgsso

import (
	"context"
	"errors"
	"sync"

	"github.com/tidbcloud/account/commons/database"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/account/internel/dao/sso"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	CodeAuthPass int32 = 0
	CodeAuthFail int32 = 10
)

type OrgTokenAuthSvcIface interface {
	AuthOrgToken(ctx context.Context, param *OrgTokenAuthParam) (*OrgTokenAuthRet, error)
}

var (
	orgTokenAuthSvcInitOnce sync.Once
	orgTokenAuthSvcInstance OrgTokenAuthSvcIface
)

func GetOrgTokenAuthSvc() (OrgTokenAuthSvcIface, error) {
	orgTokenAuthSvcInitOnce.Do(func() {
		orgTokenAuthSvcInstance = NewOrgTokenAuthSvc()

	})
	return orgTokenAuthSvcInstance, nil
}

type OrgTokenAuthSvc struct {
}

func NewOrgTokenAuthSvc() OrgTokenAuthSvcIface {
	return &OrgTokenAuthSvc{}
}

func (s *OrgTokenAuthSvc) AuthOrgToken(ctx context.Context, param *OrgTokenAuthParam) (*OrgTokenAuthRet, error) {
	bm := utils.Begin(ctx, "AuthOrgToken")
	defer bm.OnExit(ctx)
	tilog.Info(ctx, "AuthOrgToken start", zap.Any("param", param))
	hub := dao.GetDaoHub()
	db := database.DBConn.DB

	// check user status
	user, err := hub.User.GetUserByEmail(ctx, db, param.Email)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return ret(CodeAuthFail, "user not found"), nil
		}
		tilog.Error(ctx, "AuthOrgToken GetUserByEmail error", zap.Any("err", err))
		return nil, err
	}
	if user.Status != entity.UserStatusActive {
		return ret(CodeAuthFail, "user status not active"), nil
	}

	_, isImpersonated, err := hub.User.GetImpersonatedUser(ctx, db, user.ID.Uint64())
	if err != nil {
		tilog.Error(ctx, "AuthOrgToken GetImpersonatedUser error", zap.Any("err", err))
		return nil, err
	}
	if isImpersonated {
		return ret(CodeAuthPass, "user is impersonated"), nil
	}

	// check access org status
	accessOrg, err := hub.Org.GetOrgByOrgID(ctx, db, param.AccessOrgID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return ret(CodeAuthFail, "access org not found"), nil
		}
		tilog.Error(ctx, "AuthOrgToken GetAccessOrgByOrgID error", zap.Any("err", err))
		return nil, err
	}
	if accessOrg.Status != entity.TenantStatusActive {
		return ret(CodeAuthFail, "access org status not active"), nil
	}

	if accessOrg.OrgType == entity.OrgTypeEnterprise {
		// check access org active auth method
		//matched, err := s.checkEnterpriseOrgActiveMethods(ctx, param)
		//if err != nil {
		//	return nil, err
		//}
		//if !matched {
		//	return ret(CodeAuthFail, "access org active method name or allow domain not match"), nil
		//}

		// user upgrade org during upgraded token just let go
		upgraded, err := s.checkUserUpgradeOrg(ctx, param, user, accessOrg)
		if err != nil {
			return nil, err
		}
		if upgraded {
			return ret(CodeAuthPass, "user upgrade access org to enterprise"), nil
		}
	}

	// token org is enterprise
	if param.OrgType == string(entity.OrgTypeEnterprise) {
		if param.OrgID == param.AccessOrgID {
			return ret(CodeAuthPass, "same enterprise org id"), nil
		}
		// enterprise org switch to other org, need reauthenticate
		return ret(CodeAuthFail, "different org and token_org is enterprise"), nil
	}

	// token org is personal
	// personal org switch to enterprise org, need reauthenticate
	if accessOrg.OrgType == entity.OrgTypeEnterprise {
		return ret(CodeAuthFail, "different org and access_org is enterprise"), nil
	}
	// personal org switch to personal org, check user in access org
	inOrg, err := s.checkUserInOrg(ctx, user.ID, accessOrg.ID)
	if err != nil {
		return nil, err
	}
	if inOrg {
		return ret(CodeAuthPass, "boh personal org and user in access org"), nil
	}
	return ret(CodeAuthFail, "boh personal org and user not in access org"), nil
}

func (s *OrgTokenAuthSvc) checkUserUpgradeOrg(ctx context.Context, param *OrgTokenAuthParam, user *entity.User, accessOrg *entity.Tenant) (bool, error) {
	methods, err := dao.GetDaoHub().OrgAuthMethod.ListOrgAuthMethods(ctx, database.DBConn.DB, sso.OrgAuthMethodFilter{
		OrgIds: []uint64{accessOrg.ID.Uint64()},
		Names:  []string{string(entity.AuthMethodNameGoogle)},
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			methods = nil
		} else {
			tilog.Error(ctx, "AuthOrgToken checkUserUpgradeOrg ListOrgAuthMethods error", zap.Any("err", err))
			return false, err
		}
	}
	var enableOperator string
	var enableGoogleTime int64 = 0
	if len(methods) > 0 {
		method := methods[0]
		enableOperator = method.CreatedBy
		enableGoogleTime = method.CreatedAt.Unix()
	}
	// token org is personal and access org is enterprise and user upgrade org to sso and upgrade time is after token iss time
	if param.OrgType == string(entity.OrgTypePersonal) &&
		accessOrg.OrgType == entity.OrgTypeEnterprise &&
		user.Email == enableOperator &&
		enableGoogleTime > param.Iat {
		return true, nil
	}
	return false, nil
}

func (s *OrgTokenAuthSvc) checkUserInOrg(ctx context.Context, userID, orgID entity.ID) (bool, error) {
	bindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  []entity.ID{userID},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{orgID},
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		tilog.Error(ctx, "AuthOrgToken ListRoleBindings error", zap.Any("err", err))
		return false, err
	}
	if len(bindings) > 0 {
		return true, nil
	}
	return false, nil

}

func (s *OrgTokenAuthSvc) checkEnterpriseOrgActiveMethods(ctx context.Context, param *OrgTokenAuthParam) (bool, error) {
	loginMethodName, err := GetMethodNameFromSubject(param.Subject)
	if err != nil {
		tilog.Error(ctx, "AuthOrgToken getMethodNameFromSubject error", zap.Any("err", err))
		return false, err
	}
	orgAuthMethods, err := dao.GetDaoHub().OrgAuthMethod.ListOrgAuthMethodsByOrgID(ctx, database.DBConn.DB, param.AccessOrgID)
	if err != nil {
		tilog.Error(ctx, "AuthOrgToken ListOrgAuthMethodsByOrgID error", zap.Any("err", err))
		return false, err
	}
	for _, method := range orgAuthMethods {
		if method.Status != entity.Enable {
			continue
		}
		if method.Name != loginMethodName {
			continue
		}
		if CheckEmailInAllowDomain(param.Email, method.AllowEmailDomain) {
			return true, nil
		}

	}
	return false, nil
}

func ret(code int32, msg string) *OrgTokenAuthRet {
	return &OrgTokenAuthRet{
		Code:    code,
		Message: msg,
	}
}

type OrgTokenAuthParam struct {
	Subject     string
	Iat         int64
	Email       string
	OrgType     string
	OrgID       uint64
	AccessOrgID uint64
}

type OrgTokenAuthRet struct {
	Code    int32
	Message string
}
