package api

import (
	"context"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"strconv"

	"github.com/duke-git/lancet/v2/slice"
	accountpb "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/thirdparty/account"

	tokenSession "github.com/tidbcloud/account/internel/svc/token"

	orgsvc "github.com/tidbcloud/account/internel/svc/org"
	"github.com/tidbcloud/account/internel/svc/org/tenantctx"
	rbacsvc "github.com/tidbcloud/account/internel/svc/rbac"
	user2 "github.com/tidbcloud/account/internel/svc/user"

	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	FailedStatus      = "failed"
	TokenExpiredError = "expired"
	SystemError       = "system"
	ErrorType         = "err_type"

	enableAliCloudFlagName = "enable_alibaba_cloud_orgs"
)

func parseRemoteIP(req *http.Request) string {
	header := req.Header.Get(auditlog.HeaderXForwardedFor)

	return auditlog.ParseOperatorIP(header)
}

/*
* redirect after accept invitation
 */
func InvitationVerificationHandler(session *tokenSession.Session) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		tilog.Info(ctx, "api call", zap.String("method", r.Method), zap.String("url", r.URL.Path))
		baseUrl := fmt.Sprintf("https://%s/invite_callback?", config.Global().ServerConfig.FrontendDNS)
		vars := r.URL.Query()
		params := url.Values{}
		if len(vars["token"]) != 1 {
			params.Add("status", FailedStatus)
			params.Add(ErrorType, SystemError)
			tilog.Info(ctx, "failed get invitation token", zap.Any("vars", vars))
			tilog.Info(ctx, "Redirect："+fmt.Sprintf("%s,code=%d", baseUrl+params.Encode(), http.StatusFound))
			http.Redirect(w, r, baseUrl+params.Encode(), http.StatusFound)
			return
		}

		token := vars["token"][0]
		userInviteToken, err := session.ValidateUserInviteToken(ctx, token, true)

		frontendDNS := config.Global().ServerConfig.FrontendDNS
		if userInviteToken != nil && userInviteToken.TenantID.Uint64() != 0 {
			enable, _ := checkOrgEnableAliCloud(ctx, userInviteToken.TenantID.Uint64())
			if enable {
				frontendDNS = config.Global().ServerConfig.AlicloudSignUpDomain
				baseUrl = fmt.Sprintf("https://%s/invite_callback?", frontendDNS)
			}
		}

		if err != nil {
			if session.IsActivatedError(err) {
				baseUrl = fmt.Sprintf("https://%s", frontendDNS)
				http.Redirect(w, r, baseUrl+params.Encode(), http.StatusFound)
				return
			}
			params.Add("status", FailedStatus)
			var errType string
			if session.IsTokenExpiredError(err) {
				errType = TokenExpiredError
			} else {
				errType = SystemError
			}
			params.Add(ErrorType, errType)
			// add email to resend verification email when token expired
			if userInviteToken != nil {
				params.Add("email", userInviteToken.Email)
			}
			tilog.Info(ctx, "failed to validate invitation token", zap.String("feature", "invitation"), zap.String("errorType", errType), zap.Any("err", err.Error()))
			tilog.Info(ctx, "Redirect："+fmt.Sprintf("%s,code=%d", baseUrl+params.Encode(), http.StatusFound))
			http.Redirect(w, r, baseUrl+params.Encode(), http.StatusFound)
			return
		}

		email := userInviteToken.Email
		tenantID := userInviteToken.TenantID

		orgSvc, _ := orgsvc.GetOrgSvc()
		userSvc, _ := user2.GetUserSvc()
		inviteOrg, err := orgSvc.GetOrgByID(ctx, tenantID.Uint64())
		if err != nil {
			return
		}
		roleBindingSvc, _ := rbacsvc.GetRbacRoleBindingSvc()
		user, err := userSvc.GetUserByEmail(ctx, email)
		if err != nil {
			return
		}
		var existed = false
		if user.Status == entity.UserStatusActive {
			existed = true
		}
		filter := role.RoleBindingFilter{
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{user.ID},
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
			ScopeIDs:   []entity.ID{inviteOrg.ID},
			Status:     []string{entity.RoleBindingStatusActive},
		}
		userOrgRoleBinding, err := roleBindingSvc.ListRoleBindings(ctx, filter)
		if err != nil {
			tilog.Error(ctx, "accept invitation ,fail ListRoleBindings ", zap.Error(err))
			return
		}

		if len(userOrgRoleBinding) > 0 {
			details := auditlog.Details{
				auditlog.KeyUserEmail: email,
				auditlog.KeyError:     err,
			}
			eventType := auditlog.EventConfirmJoinProject
			if userOrgRoleBinding[0].Role == entity.RoleOrgOwner {
				eventType = auditlog.EventConfirmJoinOrg
			}
			auditlog.NewEvent(ctx, eventType).SetOperatorIP(parseRemoteIP(r)).SetOperatorEmail(email).SetOperatorID(inviteOrg.ID.Uint64()).
				SetOperatorType(auditlog.OperatorTypeUser).SetResult(err == nil).SetDetails(details.Build()).SetOrgID(tenantID.Uint64()).Submit() //nolint:errcheck
		}

		if err != nil {
			tilog.Info(ctx, "failed to get user or inviteOrg by email", zap.String("feature", "invitation"), zap.String("email", email), zap.Uint64("orgID", tenantID.Uint64()), zap.Error(err))
		}

		tilog.Info(ctx, "invitation-verification: validate invitation token", zap.String("feature", "invitation"), zap.String("email", email), zap.Uint64("tenantID", tenantID.Uint64()))
		params.Add("status", "success")
		params.Add("org_id", fmt.Sprintf("%d", tenantID.Uint64()))

		params.Add("email", email)
		params.Add("token", token)
		params.Add("existed", fmt.Sprintf("%t", existed))
		if inviteOrg != nil {
			params.Add("org_name", inviteOrg.Name)
		}
		// if inviteOrg is msp_customer  add   msp=xx in the url
		if inviteOrg.OrgTag == string(entity.OrgTagMspCustomer) && inviteOrg.ThirdPartyAccountCode != nil {
			mspId, err := strconv.ParseUint(*inviteOrg.ThirdPartyAccountCode, 10, 64)
			if err != nil {
				tilog.Error(ctx, "invitation-verification redirect fail ,get msp id err"+baseUrl+params.Encode())
			}
			mspOrg, err := orgSvc.GetOrgByID(ctx, mspId)
			if err != nil {
				tilog.Error(ctx, "invitation-verification redirect fail ,get msp inviteOrg err"+baseUrl+params.Encode())
			}
			params.Add("msp", mspOrg.Name)
			tilog.Info(ctx, "the inviteOrg is msp_customer,  add  msp param in the redirect url", zap.Any("msp", mspOrg.Name))
		}
		if inviteOrg.OrgTag == string(entity.OrgTagMsp) || inviteOrg.OrgTag == string(entity.OrgTagAwsCppo) {
			frontendDNS = fmt.Sprintf("%s.%s", config.PartnerDomainPrefix, config.Global().ServerConfig.FrontendDNS)
		}
		inviteSuccessUrl := ""
		if inviteOrg.OrgType == entity.OrgTypeEnterprise {
			inviteSuccessUrl = fmt.Sprintf("https://%s/enterprise/invite?", config.Global().ServerConfig.FrontendDNS)
			params.Add("company-name", inviteOrg.CompanyName)
		} else {
			baseUrl = fmt.Sprintf("https://%s/invite_callback?", frontendDNS)
			inviteSuccessUrl = baseUrl
		}
		tilog.Info(ctx, "invitation-verification Redirect"+baseUrl+params.Encode())
		http.Redirect(w, r, inviteSuccessUrl+params.Encode(), http.StatusFound)
	}
}

func checkOrgEnableAliCloud(ctx context.Context, orgID uint64) (bool, error) {
	req := &accountpb.ListHitFeatureFlagNamesReq{
		OrgId: orgID,
	}
	tilog.Info(ctx, "checkOrgEnableAliCloud request", zap.Any("req", req))
	rsp, err := account.GetHandler().ListHitFeatureFlagNames(ctx, req)
	tilog.Info(ctx, "checkOrgEnableAliCloud response", zap.Any("rsp", rsp), zap.Any("err", err))
	if err != nil {
		return false, err
	}
	if slice.Contain(rsp.FlagNames, enableAliCloudFlagName) {
		return true, nil
	}
	return false, nil
}

func RegistrationVerificationHandler(session *tokenSession.Session, tenantModel *tenantctx.Model) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		logger := tilog.WithContextL(ctx)
		logger.Info("api call", zap.String("method", r.Method), zap.String("url", r.URL.Path))
		baseUrl := fmt.Sprintf("https://%s/signup?", config.Global().ServerConfig.FrontendDNS)
		vars := r.URL.Query()
		// if register from oauth ,redirect return_url
		if len(vars["return_url"]) > 0 {
			returnUrl := vars["return_url"][0]
			// Base64 URL DecodeString
			s, err := base64.URLEncoding.DecodeString(returnUrl)
			if err != nil {
				tilog.Warn(ctx, "URLEncoding.DecodeString err", zap.Error(err))
			}
			returnUrl = string(s)
			tilog.Info(ctx, "RegistrationVerificationHandler get returnUrl from email url: "+returnUrl)
			FrontendDomain := config.Global().ServerConfig.FrontendDNS

			//https://deploy-preview-3028--staging-tidbcloud.netlify.app/oauth/verify/callback?returnTo=/oauth/signin?returnTo=%2Foauth%2Fauthorize%3Fresponse_type%3Dcode%26client_id%3Dcode-test-staging%26redirect_uri%3Dhttps%253A%252F%252Fexample.com%252Foauth%252Fcallback%26scope%3Dorg%3Aowner%2Bproject%3Aowner%26state%3Drandom?email=wlt-test96%40snapmail.cc&status=success"]
			baseUrl = fmt.Sprintf("https://%s/oauth/verify/callback?returnTo=%s&", FrontendDomain, returnUrl)
			tilog.Info(ctx, "RegistrationVerificationHandler set redirect  baseUrl : "+baseUrl)

		}

		tilog.Info(ctx, "RegistrationVerificationHandler", zap.Any("vars", vars))
		params := url.Values{}
		if len(vars["token"]) != 1 {
			params.Add("status", FailedStatus)
			params.Add(ErrorType, SystemError)
			tilog.Info(ctx, "get token from verification email url , Redirect: "+baseUrl+params.Encode())
			http.Redirect(w, r, baseUrl+params.Encode(), http.StatusFound)
			return
		}
		token := vars["token"][0]

		user := &user2.User{} // jiajunhuang: why pass a empty user here?

		//register in email ，when user click email link, validate email Verification
		orgID, email, channel, err := tenantModel.User(ctx, user).RegistrationVerification(ctx, token, parseRemoteIP(r))
		tilog.Info(ctx, "RegistrationVerification result: ", zap.Any("email", email), zap.Any("channel", channel))
		if orgID != 0 {
			enable, _ := checkOrgEnableAliCloud(ctx, orgID)
			if enable {
				baseUrl = fmt.Sprintf("https://%s/signup?", config.Global().ServerConfig.AlicloudSignUpDomain)
			}
		}
		if err != nil {
			var errType string
			if session.IsTokenExpiredError(err) {
				errType = TokenExpiredError
			} else {
				errType = SystemError
			}
			tilog.Warn(ctx, "failed to registration verification", zap.String("feature", "signup"), zap.String("errorType", errType), zap.Error(err))
			params.Add("status", FailedStatus)
			params.Add(ErrorType, errType)
			tilog.Info(ctx, "validate url token  fail, Redirect: "+baseUrl+params.Encode())
			http.Redirect(w, r, baseUrl+params.Encode(), http.StatusFound)
		}
		result := err == nil
		err = database.Transaction(func(tx *gorm.DB) error {
			dbUser, err := user2.GetUserByEmail(tx, *email)
			if err != nil {
				return err
			}
			details := auditlog.Details{auditlog.KeyUserEmail: dbUser.Email, auditlog.KeyError: err}
			tilog.Info(ctx, "send audit log for registration verification", zap.Any("user", dbUser))
			return auditlog.NewEvent(ctx, auditlog.EventActivation).SetOperatorIP(parseRemoteIP(r)).SetOperatorEmail(*email).SetOperatorID(dbUser.ID.Uint64()).
				SetOperatorType(auditlog.OperatorTypeUser).SetResult(result).SetDetails(details.Build()).Submit()
		})
		if err != nil {
			tilog.Error(ctx, "failed to save auditlog", zap.String("email", *email), zap.Error(err))
		}
		params.Add("status", "success")
		// register from oss insight set utm_campaign
		if *channel {
			params.Add("utm_campaign", "chat2query_202301")
			tilog.Info(ctx, "register user form channel:"+*email)
		}
		if email != nil {
			params.Add("email", *email)
		}
		tilog.Info(ctx, "validate url token  success, Redirect: "+baseUrl+params.Encode())
		http.Redirect(w, r, baseUrl+params.Encode(), http.StatusFound)
	}
}
