package orgsso

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	apikeysvc "github.com/tidbcloud/account/internel/svc/apikey"

	"github.com/tidbcloud/account/commons/database"
	usersvc "github.com/tidbcloud/account/internel/svc/user"

	"github.com/tidbcloud/account/thirdparty/account"
	"github.com/tidbcloud/account/thirdparty/billing2"

	accountpb "github.com/tidbcloud/account/idl/pbgen/proto/account"

	"github.com/google/uuid"

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

	"github.com/tidbcloud/tiinf/sdk/auditlog"

	"github.com/jinzhu/copier"

	"github.com/duke-git/lancet/v2/convertor"

	"github.com/duke-git/lancet/v2/slice"
	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"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/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type OrgSsoSvcIface interface {
	OpenOrgSso(ctx context.Context, req *central.OpenOrgSsoReq) error
	UpdateAuthMethod(ctx context.Context, req *central.UpdateAuthMethodReq) error
	SetAuthMethodStatus(ctx context.Context, req *central.SetAuthMethodStatusReq) error
	ListAuthMethods(ctx context.Context, filter sso.OrgAuthMethodFilter) ([]entity.OrgAuthMethod, error)
	GetOrgActiveAuthMethods(ctx context.Context, req *central.ListOrgActiveAuthMethodsReq) (*OrgAuthMethodInfo, error)
	GetOidcConfigFromContent(ctx context.Context, methodContent string) (*central.OidcConfig, error)
	GetSamlConfigFromContent(ctx context.Context, methodContent string) (*central.SamlConfig, error)
	CheckAuthMethod(ctx context.Context, req *central.CheckAuthMethodReq) (bool, error)
	CreateBearerToken(ctx context.Context, req *central.CreateBearerTokenReq) (*central.BearerToken, error)
	ListBearerTokens(ctx context.Context, req *central.ListBearerTokensReq) (*central.BearerTokens, error)
}

var (
	orgSsoSvcInitOnce sync.Once
	orgSsoSvcInstance OrgSsoSvcIface
)

func GetOrgSsoSvc() (OrgSsoSvcIface, error) {
	orgSsoSvcInitOnce.Do(func() {
		orgSsoSvcInstance = NewOrgSsoSvc(database.DBConn.DB, dao.GetDaoHub())

	})
	return orgSsoSvcInstance, nil
}

type OrgSsoSvc struct {
	db  *gorm.DB
	hub *dao.DaoHub
}

func NewOrgSsoSvc(db *gorm.DB, hub *dao.DaoHub) OrgSsoSvcIface {
	return &OrgSsoSvc{
		db:  db,
		hub: hub,
	}
}

const OpenIDConnectNamePrefix = "OIDC-"

func (s *OrgSsoSvc) OpenOrgSso(ctx context.Context, req *central.OpenOrgSsoReq) error {

	tilog.Info(ctx, "svc OpenCloudSSO start", zap.Any("req", req))
	org, err := s.GetAndValidOrg(ctx, req.OrgId)
	if err != nil {
		tilog.Warn(ctx, "svc OpenCloudSSO GetAndValidOrg err", zap.Any("orgID", req.OrgId), zap.Any("err", err))
		return err
	}
	if org.OrgType == entity.OrgTypeEnterprise {
		tilog.Info(ctx, "svc OpenCloudSSO org has already opened sso", zap.Any("orgID", req.OrgId))
		return errs.NewBadRequest("org has already opened sso", nil)
	}

	// Security: SSO enablement requires a paid org. UI gates this on credit-card
	// binding, but the backend previously had no gate — any authed user could flip
	// org_type to enterprise and write a company_name. Replaced the credit-card
	// check with an effective-plan check against billing: only on-demand paid
	// orgs may enable organization SSO.
	planInfo, err := billing2.GetHandler().GetTenantPlanInfo(ctx, req.OrgId)
	if err != nil {
		tilog.Error(ctx, "svc OpenCloudSSO call billing GetTenantPlanInfo err", zap.Uint64("orgID", req.OrgId), zap.Error(err))
		return err
	}
	if planInfo == nil {
		tilog.Warn(ctx, "svc OpenCloudSSO nil plan info from billing", zap.Uint64("orgID", req.OrgId))
		return errs.NewBadRequest("failed to load organization plan info", nil)
	}
	tilog.Info(ctx, "svc OpenCloudSSO check org effective plan", zap.Uint64("orgID", req.OrgId), zap.String("effective_plan", planInfo.EffectivePlan))
	if planInfo.EffectivePlan != string(entity.TenantPlanOnDemand) {
		return errs.NewBadRequest("organization SSO is only available on on-demand paid plans; please add a credit card to upgrade", nil)
	}

	user, err := usersvc.GetUserFromContext(ctx)
	if err != nil || user == nil {
		tilog.Warn(ctx, "svc OpenCloudSSO GetUserFromContext err", zap.Uint64("orgID", req.OrgId), zap.Error(err))
		return errs.NewUnauthorized("login required", nil)
	}
	err = s.db.Transaction(func(tx *gorm.DB) error {
		// OpenOrgSso
		err = s.hub.Org.OpenOrgSso(ctx, tx, req.OrgId, string(entity.OrgTypeEnterprise), req.CompanyName)
		if err != nil {
			tilog.Error(ctx, "svc OpenCloudSSO  failed", zap.Any("orgID", req.OrgId))
			return err
		}
		//  create auth method
		for _, name := range entity.AuthMethodArr {
			obj := &entity.OrgAuthMethod{
				OrgID:       entity.ID(req.OrgId),
				Name:        name,
				DisplayName: utils.GetAuthMethodDisplayName(name),
				Type:        utils.GetAuthMethodTypeByName(name),
				Status:      entity.Enable,
				CreatedBy:   user.Email,
			}
			if obj.Type == entity.AuthMethodTypeOIDC || obj.Type == entity.AuthMethodTypeSAML {
				obj.Status = entity.Disable

			}
			tilog.Info(ctx, "will create auth method when openSso ", zap.Any("AuthMethod", obj))
			_, err = s.hub.OrgAuthMethod.CreateOrgAuthMethod(ctx, tx, obj)
			// return  nil commit Transaction
			if err != nil {
				tilog.Error(ctx, " CreateOrgAuthMethod  failed", zap.Any("orgID", req.OrgId), zap.Any("orgID", req.OrgId))
				return err
			}

		}
		//return s.deleteUsersFromOrg(ctx, entity.ID(req.OrgId), user.ID)
		// set user.status= pending in org and project
		if err := s.setUsersStatusPendingInOrg(ctx, *org, *user, req.CompanyName); err != nil {
			return err
		}
		return s.setUsersStatusInProject(ctx, *org, *user)

	})

	if err != nil {
		tilog.Info(ctx, "svc OpenCloudSSO  err", zap.Any("orgID", req.OrgId), zap.Any("err", err))
		return err
	}
	tilog.Info(ctx, "svc OpenCloudSSO success", zap.Any("param", req))
	if !config.IsLocalEnv() {

		details := auditlog.Details{
			auditlog.KeyDescription: "enable CloudOrganizationSSO",
			auditlog.KeyError:       err,
		}
		auditlog.NewEvent(ctx, auditlog.EventEnableCloudOrganizationSSO).SetOrgID(req.OrgId).SetDetails(details.Build()).SetOperatorEmail(user.Email).SetResult(err == nil).Submit() //nolint:errcheck
	}
	return nil

}

func (s *OrgSsoSvc) GetAndValidOrg(ctx context.Context, orgID uint64) (*entity.Tenant, error) {
	org, err := s.GetOrg(ctx, orgID)
	if err != nil {
		return nil, err
	}
	if org == nil {
		return nil, errs.NewBadRequest("org_id not exist", nil)
	}
	if org.Status != entity.TenantStatusActive {
		return nil, errs.NewBadRequest("org not active", nil)
	}
	return org, nil
}

func (s *OrgSsoSvc) GetOrg(ctx context.Context, orgID uint64) (*entity.Tenant, error) {
	org, err := s.hub.Org.GetOrgByOrgID(ctx, s.db, orgID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return org, nil
}

func whetherCheckProvisionAllowDomain(ctx context.Context, orgId uint64) (bool, error) {
	// Compatible with old users (already enable sso orgs do not check)
	am, err := dao.GetDaoHub().OrgAuthMethod.GetOrgAuthMethodByName(ctx, database.DBConn.DB, orgId, string(entity.AuthMethodNameEmail))
	if err != nil {
		return false, err
	}
	var confTime time.Time
	if config.IsProdEnv() {
		confTime = time.Date(2026, 6, 17, 0, 0, 0, 0, time.UTC)
	} else {
		confTime = time.Date(2026, 5, 17, 0, 0, 0, 0, time.UTC)
	}

	amTime := am.CreatedAt.UTC()
	tilog.Info(ctx, fmt.Sprintf("svc UpdateAuthMethod amTime: %v, configTime: %v", amTime, confTime))

	return amTime.After(confTime), nil
}

func (s *OrgSsoSvc) UpdateAuthMethod(ctx context.Context, req *central.UpdateAuthMethodReq) error {
	tilog.Info(ctx, "svc UpdateAuthMethod request", zap.Any("req", req))
	// oidc/saml enable auto/scim provision check allow domain
	if (req.Type == string(entity.AuthMethodTypeOIDC) || req.Type == string(entity.AuthMethodTypeSAML)) && (req.AutoProvision || req.ScimProvision) {
		toCheck, err := whetherCheckProvisionAllowDomain(ctx, req.OrgId)
		if err != nil {
			tilog.Error(ctx, "svc UpdateAuthMethod GetOrgAuthMethodByName error", zap.Any("err", err))
			return err
		}
		if toCheck {
			if len(req.AllowEmailDomain) == 0 {
				return errs.NewBadRequest("OIDC/SAML enable Auto/SCIM Provision must set allow email domains", nil)
			}
			domains, err := dao.GetDaoHub().OrgDomain.ListByOrgID(ctx, database.DBConn.DB, req.OrgId)
			if err != nil {
				tilog.Error(ctx, "svc UpdateAuthMethod list org domains error", zap.Any("err", err))
				return err
			}
			var domainMap = map[string]struct{}{}
			for _, d := range domains {
				domainMap[d.DomainName] = struct{}{}
			}
			for _, sd := range req.AllowEmailDomain {
				if _, ok := domainMap[sd]; !ok {
					return errs.NewBadRequest(fmt.Sprintf("domain %s is invalid", sd), nil)
				}
			}
		}
	}
	_, err := s.GetAndValidOrg(ctx, req.OrgId)
	if err != nil {
		tilog.Info(ctx, "svc UpdateAuthMethod GetAndValidOrg err", zap.Any("err", err))
		return err
	}
	// update AuthMethodEntity
	authMethodEntity, err := GetAuthMethodEntity(ctx, req)
	if err != nil {
		return err
	}
	dbAuthMethod, err := s.GetAuthMethodByParam(ctx, req.OrgId, req.Name, authMethodEntity.Type)

	if err != nil {
		tilog.Error(ctx, "svc ConfigAuthMethod GetOrgAuthMethodByName err", zap.Any("err", err))
		return err
	}
	if dbAuthMethod == nil {
		return errs.NewBadRequest("the authMethodName is not exist ", nil)
	}

	authMethodEntity.ID = dbAuthMethod.ID

	oidcConnectionName := OpenIDConnectNamePrefix + convertor.ToString(req.OrgId)
	samlConnectionName := GetSamlConnectionName(req.OrgId)

	reqOidcInfo := &OidcInfo{}
	reqSamlInfo := &SamlInfo{}
	if authMethodEntity.Type == entity.AuthMethodTypeOIDC {
		authMethodEntity.DisplayName = req.Name
		authMethodEntity.Name = oidcConnectionName

		_ = copier.Copy(reqOidcInfo, req.OidcConfig)

	} else if authMethodEntity.Type == entity.AuthMethodTypeSAML {
		authMethodEntity.DisplayName = req.Name
		authMethodEntity.Name = samlConnectionName

		_ = copier.Copy(reqSamlInfo, req.SamlConfig)
	}

	dbOidcInfo := &OidcInfo{}
	dbSamlInfo := &SamlInfo{}
	err = s.db.Transaction(func(tx *gorm.DB) error {

		if authMethodEntity.Type == entity.AuthMethodTypeOIDC {
			dbOidcInfo = getOidcInfoFromContent(ctx, dbAuthMethod.Content)
			reqOidcInfo.Id = dbOidcInfo.Id
			//  pushContentToAuth0
			if req.OidcConfig.ClientId == dbOidcInfo.ClientId && req.OidcConfig.ClientSecret == dbOidcInfo.ClientSecret && req.OidcConfig.IssuerUrl == dbOidcInfo.IssuerUrl {
				tilog.Info(ctx, "oidc config in not change , ignore ", zap.Any("reqContent", req.OidcConfig), zap.Any("dbContent", dbOidcInfo))
				authMethodEntity.Content = dbAuthMethod.Content
			} else {
				resp, err := pushContentToAuth0(ctx, *reqOidcInfo, oidcConnectionName)
				if err != nil {
					return err
				}
				reqOidcInfo.Id = resp.Id
				authMethodEntity.Content = getContentFromOidcInfo(ctx, *reqOidcInfo)
			}
		} else if authMethodEntity.Type == entity.AuthMethodTypeSAML {
			dbSamlInfo = getSamlInfoFromContent(ctx, dbAuthMethod.Content)
			reqSamlInfo.Id = dbSamlInfo.Id
			// not configured before
			if dbSamlInfo.MetadataUrl == "" && (dbSamlInfo.SignOnUrl == "" || dbSamlInfo.SigningCert == "") {
				tilog.Info(ctx, "db saml config metadata_url and sign_on_url/signing_cert are both empty, need to change auth0 connection", zap.Any("reqContent", reqSamlInfo), zap.Any("dbContent", dbSamlInfo))
				resp, err := pushSamlContentToAuth0(ctx, *reqSamlInfo, samlConnectionName)
				if err != nil {
					return err
				}
				reqSamlInfo.Id = resp.Id
				authMethodEntity.Content = getContentFromSamlInfo(ctx, *reqSamlInfo)
			} else if dbSamlInfo.MetadataUrl != "" {
				// configured metadata_url before
				if reqSamlInfo.MetadataUrl == dbSamlInfo.MetadataUrl {
					authMethodEntity.Content = dbAuthMethod.Content
					tilog.Info(ctx, "saml config metadata_url not change, ignore to change auth0 connection", zap.Any("reqContent", reqSamlInfo), zap.Any("dbContent", dbSamlInfo))
				} else {
					tilog.Info(ctx, "db saml config metadata_url changed, need to change auth0 connection", zap.Any("reqContent", reqSamlInfo), zap.Any("dbContent", dbSamlInfo))
					resp, err := pushSamlContentToAuth0(ctx, *reqSamlInfo, samlConnectionName)
					if err != nil {
						return err
					}
					reqSamlInfo.Id = resp.Id
					authMethodEntity.Content = getContentFromSamlInfo(ctx, *reqSamlInfo)
				}
			} else {
				if reqSamlInfo.SignOnUrl == dbSamlInfo.SignOnUrl && reqSamlInfo.SigningCert == dbSamlInfo.SigningCert {
					authMethodEntity.Content = dbAuthMethod.Content
					tilog.Info(ctx, "saml config sign_on_url/signing_cert not change , ignore to change auth0 connection", zap.Any("reqContent", reqSamlInfo), zap.Any("dbContent", dbSamlInfo))
				} else {
					tilog.Info(ctx, "db saml config sign_on_url/signing_cert changed, need to change auth0 connection", zap.Any("reqContent", reqSamlInfo), zap.Any("dbContent", dbSamlInfo))
					resp, err := pushSamlContentToAuth0(ctx, *reqSamlInfo, samlConnectionName)
					if err != nil {
						return err
					}
					reqSamlInfo.Id = resp.Id
					authMethodEntity.Content = getContentFromSamlInfo(ctx, *reqSamlInfo)
				}
			}

		}

		err = s.hub.OrgAuthMethod.UpdateOrgAuthMethod(ctx, tx, authMethodEntity)
		if err != nil {
			tilog.Error(ctx, "svc ConfigAuthMethod Operate DB err", zap.Any("err", err))
			return err
		}
		return nil
	})

	if !config.IsLocalEnv() {

		var oldMap = map[string]interface{}{}
		var newMap = map[string]interface{}{}

		if authMethodEntity.DisplayName != dbAuthMethod.DisplayName {
			oldMap["method_name"] = dbAuthMethod.DisplayName
			newMap["method_name"] = authMethodEntity.DisplayName

		}
		if authMethodEntity.AllowEmailDomain != dbAuthMethod.AllowEmailDomain {
			oldMap["Allowed Email Domains"] = strings.Split(dbAuthMethod.AllowEmailDomain, entity.CommaSeparator)
			newMap["Allowed Email Domains"] = req.GetAllowEmailDomain()

		}
		if authMethodEntity.AutoProvision != dbAuthMethod.AutoProvision {
			oldMap["Auto-provision Accounts"] = dbAuthMethod.AutoProvision
			newMap["Auto-provision Accounts"] = req.AutoProvision

		}
		if req.OidcConfig != nil {
			if req.OidcConfig.ClientId != dbOidcInfo.ClientId {
				oldMap["Client ID"] = dbOidcInfo.ClientId
				newMap["Client ID"] = req.OidcConfig.ClientId

			}
			if req.OidcConfig.ClientSecret != dbOidcInfo.ClientSecret {
				oldMap["Client Secret"] = dbOidcInfo.ClientSecret
				newMap["Client Secret"] = req.OidcConfig.ClientSecret

			}
			if req.OidcConfig.IssuerUrl != dbOidcInfo.IssuerUrl {
				oldMap["Issuer URL"] = dbOidcInfo.IssuerUrl
				newMap["Issuer URL"] = req.OidcConfig.IssuerUrl

			}
		}
		if req.SamlConfig != nil {
			if req.SamlConfig.MetadataUrl != dbSamlInfo.MetadataUrl {
				oldMap["Metadata URL"] = dbSamlInfo.MetadataUrl
				newMap["Metadata URL"] = req.SamlConfig.MetadataUrl
			}
			if req.SamlConfig.SignOnUrl != dbSamlInfo.SignOnUrl {
				oldMap["Sign On URL"] = dbSamlInfo.SignOnUrl
				newMap["Sign On URL"] = req.SamlConfig.SignOnUrl
			}
			if req.SamlConfig.SigningCert != dbSamlInfo.SigningCert {
				oldMap["Signing Cert"] = dbSamlInfo.SigningCert
				newMap["Signing Cert"] = req.SamlConfig.SigningCert
			}
		}
		oldByte, _ := json.Marshal(oldMap)
		newByte, _ := json.Marshal(newMap)
		oldStr := string(oldByte)
		newStr := string(newByte)
		details := auditlog.Details{
			auditlog.KeyOldValue:                 oldStr,
			auditlog.KeyNewValue:                 newStr,
			auditlog.KeyAuthenticationMethodName: dbAuthMethod.DisplayName,
			auditlog.KeyError:                    err,
		}
		auditlog.NewEvent(ctx, auditlog.EventUpdateAuthenticationMethod).SetOrgID(req.OrgId).SetDetails(details.Build()).SetResult(err == nil).Submit() //nolint:errcheck
	}
	return err
}
func (s *OrgSsoSvc) SetAuthMethodStatus(ctx context.Context, req *central.SetAuthMethodStatusReq) (err error) {
	// Build the entity from the request
	paramBean, err := s.buildEntity(ctx, req.OrgId, req.Name, req.Type, req.Status)
	if err != nil {
		tilog.Info(ctx, "svc  buildEntityByStatusReq param  err", zap.Any("err", err.Error()))
		return err
	}

	user, err := usersvc.GetUserFromContext(ctx)
	if err != nil {
		return err
	}
	paramBean.UpdatedBy = user.Email
	err = s.hub.OrgAuthMethod.SetAuthMethodStatus(ctx, s.db, paramBean)
	tilog.Info(ctx, "update the authMethod status result", zap.Any("paramBean", paramBean), zap.Any("err", err))
	var eventType auditlog.EventType
	if paramBean.Status == entity.Enable {
		eventType = auditlog.EventEnableAuthenticationMethod
	} else if paramBean.Status == entity.Disable {
		eventType = auditlog.EventDisableAuthenticationMethod
	}
	details := auditlog.Details{
		auditlog.KeyAuthenticationMethodName: paramBean.DisplayName,
		auditlog.KeyDescription:              string(paramBean.Status),
		auditlog.KeyError:                    err,
	}
	//tilog.Info(ctx, "update the authMethod status = "+string(paramBean.Status))
	if !config.IsLocalEnv() {
		auditlog.NewEvent(ctx, eventType).SetOrgID(req.OrgId).SetDetails(details.Build()).SetOperatorEmail(user.Email).SetResult(err == nil).Submit() //nolint:errcheck
	}
	return err
}

func (s *OrgSsoSvc) GetAuthMethodByParam(ctx context.Context, orgId uint64, name string, authType entity.AuthMethodType) (*entity.OrgAuthMethod, error) {
	var dbAuthMethod *entity.OrgAuthMethod
	var err error
	// if oidc  query db by type
	//  find the auth method in db
	if authType == entity.AuthMethodTypeOIDC || authType == entity.AuthMethodTypeSAML {

		typeFilter := sso.OrgAuthMethodFilter{
			OrgIds: []uint64{orgId},
			Types:  []entity.AuthMethodType{authType},
		}

		authMethodsByType, err := s.hub.OrgAuthMethod.ListOrgAuthMethods(ctx, s.db, typeFilter)
		if err != nil {
			return nil, err
		}
		// type=OIDC in DB
		if len(authMethodsByType) == 1 {
			dbAuthMethod = &authMethodsByType[0]
		} else if len(authMethodsByType) > 1 {
			tilog.Error(ctx, fmt.Sprintf("more than one AuthMethod type=%s  in the org %d,please check ", authType, orgId))
		}
	} else {
		// if not oidc  query db by name
		dbAuthMethod, err = s.hub.OrgAuthMethod.GetOrgAuthMethodByName(ctx, s.db, orgId, name)
	}
	if err != nil {
		return nil, err
	}
	return dbAuthMethod, nil
}

func (s *OrgSsoSvc) buildEntity(ctx context.Context, orgId uint64, name, authType, status string) (obj *entity.OrgAuthMethod, err error) {
	if orgId == 0 {
		return nil, errs.NewBadRequest("org_id must not null ", nil)
	}

	if utils.GetEntityStatus(status) == "" {
		return nil, errs.NewBadRequest("status is invalid ", nil)
	}

	if utils.GetAuthMethodType(authType) == "" {
		return nil, errs.NewBadRequest("type is invalid ", nil)
	}

	// name must is num  when authType != entity.AuthMethodTypeOIDC
	if utils.GetAuthMethodType(authType) != entity.AuthMethodTypeOIDC && utils.GetAuthMethodType(authType) != entity.AuthMethodTypeSAML && !slice.Contain(entity.AuthMethodArr, name) {
		return nil, errs.NewBadRequest("name is invalid ", nil)
	}

	_, err = s.GetAndValidOrg(ctx, orgId)
	if err != nil {
		tilog.Info(ctx, "svc  GetAndValidOrg err", zap.Any("err", err.Error()))
		return nil, err
	}
	connectionName := name
	obj = &entity.OrgAuthMethod{
		OrgID:       entity.ID(orgId),
		Name:        connectionName,
		DisplayName: utils.GetAuthMethodDisplayName(name),
		Type:        utils.GetAuthMethodType(authType),
		Status:      utils.GetEntityStatus(status),
	}
	return obj, nil
}
func (s *OrgSsoSvc) GetOrgActiveAuthMethods(ctx context.Context, req *central.ListOrgActiveAuthMethodsReq) (*OrgAuthMethodInfo, error) {
	var org *entity.Tenant
	var err error
	if req.OrgId != "" {
		orgID, err := strconv.ParseUint(req.OrgId, 10, 64)
		if err != nil {
			return nil, errs.NewBadRequest("org_id invalid", nil)
		}
		org, err = s.hub.Org.GetOrgByOrgID(ctx, s.db, orgID)
		if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
			org = nil
			err = nil //nolint
		}
	} else if req.CompanyName != "" {
		org, err = s.hub.Org.GetOrgByCompanyName(ctx, s.db, req.CompanyName)
	} else {
		return nil, errs.NewBadRequest("org_id company_name must has one value", nil)
	}
	if err != nil {
		tilog.Warn(ctx, "GeActiveAuthMethodsByCustomName GetOrgByCustomName error", zap.Any("err", err))
		return nil, err
	}
	if org == nil {
		tilog.Info(ctx, "company_name not found org:"+req.CompanyName)
		return nil, nil
	}
	orgName, err := s.getOrgName(ctx, org)
	if err != nil {
		tilog.Warn(ctx, "GeActiveAuthMethodsByCustomName getOrgName error", zap.Any("err", err))
		return nil, err
	}

	methods, err := s.hub.OrgAuthMethod.ListOrgAuthMethodsByOrgID(ctx, s.db, org.ID.Uint64())
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			tilog.Warn(ctx, "GetOrgActiveAuthMethods ListOrgAuthMethodsByOrgID error", zap.Any("err", err))
			return nil, err
		}
	}
	var retAuthMethods []AuthMethodInfo
	if len(methods) > 0 {
		for _, method := range methods {
			if method.Status != entity.Enable {
				continue
			}
			info := AuthMethodInfo{
				Name:        method.Name,
				Type:        string(method.Type),
				DisplayName: method.DisplayName,
			}
			retAuthMethods = append(retAuthMethods, info)
		}
	}

	retInfo := OrgAuthMethodInfo{
		OrgID:       org.ID.Uint64(),
		OrgName:     orgName,
		AuthMethods: retAuthMethods,
	}
	return &retInfo, nil
}

func (s *OrgSsoSvc) getOrgName(ctx context.Context, org *entity.Tenant) (string, error) {
	orgName := org.Name
	if orgName == "" {
		orgName = org.Alias
	}
	if orgName == "" {
		user, err := s.hub.User.GetUserByEmail(ctx, s.db, org.Email)
		if err != nil {
			tilog.Warn(ctx, "GeActiveAuthMethodsByCustomName GetUserByEmail error", zap.Any("err", err))
			return "", err
		}
		userName := user.FirstName
		if userName == "" {
			userName = user.LastName
		}
		if userName == "" {
			split := strings.Split(org.Email, "@")
			userName = split[0]
		}
		orgName = fmt.Sprintf("%s's Org", userName)
	}
	return orgName, nil
}
func (s *OrgSsoSvc) ListAuthMethods(ctx context.Context, filter sso.OrgAuthMethodFilter) ([]entity.OrgAuthMethod, error) {
	authMethods, err := s.hub.OrgAuthMethod.ListOrgAuthMethods(ctx, s.db, filter)
	if err != nil {
		tilog.Warn(ctx, "ListAuthMethods error", zap.Any("err", err))
		return nil, err
	}

	return authMethods, nil
}

func (s *OrgSsoSvc) GetOidcConfigFromContent(ctx context.Context, methodContent string) (*central.OidcConfig, error) {

	oidcInfo := getOidcInfoFromContent(ctx, methodContent)
	oidcConfig := &central.OidcConfig{}
	err := copier.Copy(oidcConfig, oidcInfo)
	return oidcConfig, err
}

func (s *OrgSsoSvc) GetSamlConfigFromContent(ctx context.Context, methodContent string) (*central.SamlConfig, error) {

	samlInfo := getSamlInfoFromContent(ctx, methodContent)
	samlConfig := &central.SamlConfig{}
	err := copier.Copy(samlConfig, samlInfo)
	return samlConfig, err
}

/*
func (s *OrgSsoSvc) deleteUsersFromOrg(ctx context.Context, OrgId entity.ID, skipUserID entity.ID) error {

		filter := role.RoleBindingFilter{
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
			ScopeIDs:   []entity.ID{OrgId},
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			Status:     []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive},
		}

		roleBindings, err := s.hub.Role.ListRoleBindings(ctx, s.db, filter)

		if err != nil {
			if err == gorm.ErrRecordNotFound {
				tilog.Info(ctx, "the org's user is null", zap.Any("OrgId", OrgId))
			}
			return nil
		}
		tilog.Info(ctx, "find the org's user", zap.Any("OrgId", OrgId), zap.Any("len", len(roleBindings)))

		if len(roleBindings) > 0 {
			for _, r := range roleBindings {
				if skipUserID == r.TargetID {
					tilog.Info(ctx, "DeleteUsersFromOrg skip this user ", zap.Any("OrgId", OrgId), zap.Any("userID", r.TargetID))
					continue
				}
				// wangletian todo remove
				//if config.IsLocalEnv() {
				//	tilog.Info(ctx, "DeleteUsersFromOrg :", zap.Any("userID", r.TargetID), zap.Any("OrgId", OrgId))
				//	//continue
				//}

				email, isDeleteFromAuthO, err := common.DeleteUserFromOrg(ctx, OrgId, r.TargetID)
				tilog.Info(ctx, "DeleteUserFrom auth0 ", zap.Any("isDeleteFromAuthO", isDeleteFromAuthO), zap.Any("user", email))
				if err != nil {
					return err
				}

				if isDeleteFromAuthO && email != "" {
					if err = DeleteAuth0User(ctx, email); err != nil {
						return err
					}
				}
				// delete dbuser
				if r.Status == entity.RoleBindingStatusActive {

					if err := s.hub.DBUser.CreateCommandUserAction(ctx, s.db, skipUserID.Uint64(), r.TargetID.Uint64(), email, OrgId.Uint64(), 0, entity.CommandStatusWaiting, entity.CommandActionDelete, entity.RoleBindingTargetUser); err != nil {
						tilog.Error(ctx, "create delete dbuser command  fail ", zap.Any("operator", skipUserID.Uint64()), zap.Any("userId", r.TargetID.Uint64()), zap.Any("email", email), zap.Error(err))

					}
				}
			}

			return nil

		}
		return nil
	}
*/
func (s *OrgSsoSvc) setUsersStatusPendingInOrg(ctx context.Context, org entity.Tenant, operatorUser usersvc.User, companyName string) error {

	filter := role.RoleBindingFilter{
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{org.ID},
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		Status:     []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive},
	}
	roleBindings, err := s.hub.Role.ListRoleBindings(ctx, s.db, filter)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			tilog.Info(ctx, "the org's user is null", zap.Any("OrgId", org.ID))
		}
		return nil
	}

	tilog.Info(ctx, "find the org's user", zap.Any("OrgId", org.ID), zap.Any("len(users)", len(roleBindings)))
	var updateUserIds []entity.ID

	for _, r := range roleBindings {
		if operatorUser.ID == r.TargetID {
			tilog.Info(ctx, "setUsersStatusPending in project skip this user ", zap.Any("OrgId", org.ID), zap.Any("project", r.ScopeID), zap.Any("userID", r.TargetID))
			continue
		}
		if r.Status == entity.RoleBindingStatusActive {
			updateUserIds = append(updateUserIds, r.TargetID)
		}

	}
	// batch update user status in org
	if len(updateUserIds) > 0 {
		paramMap := map[string]interface{}{}
		paramMap["status"] = entity.RoleBindingStatusInactive
		UpdateFilter := role.RoleBindingFilter{
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
			ScopeIDs:   []entity.ID{org.ID},
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  updateUserIds,
		}
		if err := s.hub.Role.BatchUpdateRoleOrStatus(ctx, s.db, UpdateFilter, paramMap); err != nil {
			tilog.Info(ctx, "update role_binding  user's project  status = pending", zap.Any("OrgId", org.ID), zap.Any("users", updateUserIds))
			return err
		}
	}

	for _, r := range roleBindings {
		if operatorUser.ID == r.TargetID {
			tilog.Info(ctx, "open sso sendInviteEmail skip operator", zap.Any("OrgId", org.ID), zap.Any("userID", r.TargetID))
			continue
		}
		//
		user, err := s.hub.User.GetUserByID(ctx, s.db, r.TargetID.Uint64())
		if err != nil {
			return err
		}
		if user.Status == entity.UserStatusActive {

			//method := "/auto-invite"
			invitationVerificationMethod := "/invitation-verification"

			period := 24 * time.Hour
			token, err := GetSession().InsertUserInviteToken(s.db, operatorUser.ID, org.ID, invitationVerificationMethod, user.Email, period, true)

			tilog.Info(ctx, "this user need to send auto invite email ,to create user invite token ", zap.Any("token", token))
			if err != nil {
				tilog.Error(ctx, "failed to create user invite token", zap.String("feature", "invitation"), zap.String("email", user.Email), zap.Error(err))
				return errors.New("failed to create user invite token")
			}
			if !config.IsLocalEnv() {
				go sendInviteEmail(context.Background(), user.Email, user.FirstName, token, org.Name, org.ID.Uint64(), companyName)
				// delete dbuser
				go deleteDbUser(context.Background(), org, operatorUser, s, user.ID, user)
			}
		}

	}
	return nil
}
func (s *OrgSsoSvc) setUsersStatusInProject(ctx context.Context, org entity.Tenant, operatorUser usersvc.User) error {

	projects, err := s.hub.Project.ListProjects(ctx, s.db, []uint64{org.ID.Uint64()}, []uint64{}, []string{string(entity.ProjectStatusActive)})
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			tilog.Info(ctx, "the org's  project   is null", zap.Any("OrgId", org.ID))
		}
		return nil
	}
	var pids []entity.ID
	for _, p := range projects {
		pids = append(pids, p.ID)
	}
	filter := role.RoleBindingFilter{
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
		ScopeIDs:   pids,
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		Status:     []string{entity.RoleBindingStatusActive},
	}
	roleBindings, err := s.hub.Role.ListRoleBindings(ctx, s.db, filter)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			tilog.Info(ctx, "the org's  project  user is null", zap.Any("OrgId", org.ID), zap.Any("projects", pids))
		}
		return nil
	}
	var userIds []entity.ID
	for _, r := range roleBindings {
		if operatorUser.ID == r.TargetID {
			tilog.Info(ctx, "setUsersStatusPending in project skip this user ", zap.Any("OrgId", org.ID), zap.Any("project", r.ScopeID), zap.Any("userID", r.TargetID))
			continue
		}
		userIds = append(userIds, r.TargetID)

	}
	if len(userIds) > 0 {
		paramMap := map[string]interface{}{}
		paramMap["status"] = entity.RoleBindingStatusInactive
		UpdateFilter := role.RoleBindingFilter{
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
			ScopeIDs:   pids,
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  userIds,
		}

		return s.hub.Role.BatchUpdateRoleOrStatus(ctx, s.db, UpdateFilter, paramMap)

	}

	return nil

}

func deleteDbUser(ctx context.Context, org entity.Tenant, operatorUser usersvc.User, s *OrgSsoSvc, r entity.ID, user *entity.User) {
	if err := s.hub.DBUser.CreateCommandUserAction(ctx, s.db, operatorUser.ID.Uint64(), r.Uint64(), user.Email, org.ID.Uint64(), 0, entity.CommandStatusWaiting, entity.CommandActionDelete, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser); err != nil {
		tilog.Error(ctx, "create delete dbuser command  fail ", zap.Any("operator", operatorUser.ID.Uint64()), zap.Any("userId", r.Uint64()), zap.Any("email", user.Email), zap.Error(err))

	}

	tilog.Info(ctx, "create delete dbuser command", zap.Any("email", user.Email))
}

func sendInviteEmail(ctx context.Context, email, firstName, token, orgName string, orgID uint64, companyName string) {
	logger := tilog.WithContextL(ctx)

	url := fmt.Sprintf("https://%s/invitation-verification?token=%s", utils.GetCentralBackendDns(), token)
	companyUrl := fmt.Sprintf("https://%s/enterprise/signin/%s", config.Global().ServerConfig.FrontendDNS, companyName)
	orgName = utils.EscapeStr(ctx, orgName)
	firstName = utils.EscapeStr(ctx, firstName)
	vars := map[string]string{
		"orgName":    orgName,
		"userName":   firstName,
		"url":        url,
		"companyUrl": companyUrl,
	}
	_, err := account.GetHandler().SendEmail(ctx, &accountpb.SendEmailReq{
		OrgId:        orgID,
		Scene:        string(entity.NotificationSceneAutoInvite),
		Identifier:   uuid.New().String(),
		Emails:       []string{email},
		CcEmails:     nil,
		TemplateId:   config.SendgridTemplateName2id[config.TNAutoInviteUser],
		TemplateVars: vars,
	})
	if err != nil {
		logger.Error("failed to send invitation email", zap.String("feature", "invitation"), zap.String("email", email), zap.Error(err))
		return
	}
	logger.Info("send auto invite email", zap.String("email", email), zap.Any("org_id", orgID), zap.Any("orgName", orgName), zap.Time("time", time.Now().UTC()))

}

func (s *OrgSsoSvc) CheckAuthMethod(ctx context.Context, req *central.CheckAuthMethodReq) (bool, error) {
	tilog.Info(ctx, "CheckAuthMethod request", zap.Any("req", req))
	matched := connectionNameCompile.MatchString(req.Name)
	if !matched {
		return false, errs.NewBadRequest("auth method name invalid", nil)
	}
	submatch := connectionNameCompile.FindStringSubmatch(req.Name)
	orgID, _ := strconv.ParseUint(submatch[2], 10, 64)

	dbAuthMethod, err := s.hub.OrgAuthMethod.GetOrgAuthMethodByName(ctx, s.db, orgID, req.Name)
	if err != nil {
		tilog.Error(ctx, "CheckAuthMethod GetOrgAuthMethodByName err", zap.Any("err", err))
		return false, err
	}
	if dbAuthMethod == nil {
		tilog.Info(ctx, "CheckAuthMethod dbAuthMethod not found", zap.Any("name", req.Name))
		return false, nil
	}
	if dbAuthMethod.Status != entity.Enable {
		tilog.Info(ctx, "CheckAuthMethod dbAuthMethod not enable", zap.Any("name", req.Name))
		return false, nil
	}
	tilog.Info(ctx, "CheckAuthMethod valid check success", zap.Any("req", req))
	return true, nil
}

func (s *OrgSsoSvc) CreateBearerToken(ctx context.Context, req *central.CreateBearerTokenReq) (*central.BearerToken, error) {
	tilog.Info(ctx, "CreateBearerToken request", zap.Any("req", req))
	user, err := usersvc.GetUserFromContext(ctx)
	if err != nil {
		tilog.Error(ctx, "CreateBearerToken GetUserFromContext error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("get user from context error", err)
	}
	createKeyReq := &accountpb.CreateApiKeyReq{
		Source:      "dbaas-console",
		Description: req.Description,
		ScopeType:   string(entity.RoleBindingScopeOrg),
		ResourceIds: []uint64{req.OrgId},
		Role:        entity.RoleBearerTokenApiKeyScimOwner,
		Operator:    user.Email,
		Type:        entity.ApiKeyTypeBearerToken,
	}
	createKeyRsp, err := account.GetHandler().CreateApiKey(ctx, createKeyReq)
	tilog.Info(ctx, "CreateBearerToken CreateApiKey result", zap.Any("createKeyReq", createKeyReq), zap.Any("createKeyRsp", createKeyRsp), zap.Any("err", err))
	if err != nil {
		tilog.Error(ctx, "CreateBearerToken CreateApiKey error", zap.Any("err", err))
		return nil, err
	}
	keyReq := &accountpb.GetApiKeyByAccessKeyReq{
		AccessKey: createKeyRsp.ApiKey.AccessKey,
	}
	// to get encrypt secret key
	keyRsp, err := account.GetHandler().GetApiKeyByAccessKey(ctx, keyReq)
	tilog.Info(ctx, "CreateBearerToken GetApiKeyByAccessKey result", zap.Any("keyReq", keyReq), zap.Any("keyRsp", keyRsp), zap.Any("err", err))
	if err != nil {
		tilog.Error(ctx, "CreateBearerToken GetApiKeyByAccessKey error", zap.Any("err", err))
		return nil, err
	}
	// gen token
	token, err := apikeysvc.GenBearerToken(ctx, keyRsp.ApiKey.AccessKey, keyRsp.ApiKey.SecretKey)
	tilog.Info(ctx, "CreateBearerToken GenBearerToken result", zap.Any("access_key", keyRsp.ApiKey.AccessKey), zap.Any("token", token), zap.Any("err", err))
	if err != nil {
		tilog.Error(ctx, "CreateBearerToken GenBearerToken error", zap.Any("err", err))
		return nil, err
	}
	rsp := convProviderKey2CentralKey(keyRsp.ApiKey, token)

	return rsp, nil
}

func (s *OrgSsoSvc) ListBearerTokens(ctx context.Context, req *central.ListBearerTokensReq) (*central.BearerTokens, error) {
	tilog.Info(ctx, "ListBearerTokens request", zap.Any("req", req))
	if req.Page <= 0 {
		req.Page = 1
	}
	if req.PerPage <= 0 {
		req.PerPage = 10
	}
	listKeyReq := &accountpb.GetApiKeysByResourceReq{
		ScopeType:   string(entity.RoleBindingScopeOrg),
		ResourceId:  req.OrgId,
		ApiKeyTypes: []string{entity.ApiKeyTypeBearerToken},
		PageNo:      req.Page,
		PageSize:    req.PerPage,
	}
	listKeyRsp, err := account.GetHandler().GetApiKeysByResource(ctx, listKeyReq)
	tilog.Info(ctx, "ListBearerTokens GetApiKeysByResource result", zap.Any("listKeyReq", listKeyReq), zap.Any("listKeyRsp", listKeyRsp), zap.Any("err", err))
	if err != nil {
		tilog.Error(ctx, "ListBearerTokens GetApiKeysByResource error", zap.Any("err", err))
		return nil, err
	}
	var items []*central.BearerToken
	for _, key := range listKeyRsp.ApiKeys {
		bearToken := convProviderKey2CentralKey(key, "")
		items = append(items, bearToken)
	}
	tokens := central.BearerTokens{
		Total: listKeyRsp.Total,
		Items: items,
	}
	return &tokens, nil
}

func convProviderKey2CentralKey(key *accountpb.ApiKey, secretKey string) *central.BearerToken {
	return &central.BearerToken{
		Id:          key.Id,
		PublicKey:   key.AccessKey,
		SecretKey:   secretKey,
		Role:        entity.RoleBearerTokenApiKeyScimOwner,
		Creator:     key.CreatedBy,
		CreatedAt:   key.CreatedAt,
		UpdatedAt:   key.UpdatedAt,
		Description: key.Description,
	}
}
