package auth0

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	OauthTokenUrlFormat             = "https://%s/oauth/token"
	DeleteMfaMethodsUrlFormat       = "https://%s/api/v2/users/%s/authentication-methods"
	EnrollMfaAuthenticatorUrlFormat = "https://%s/mfa/associate"
	RegenerateRecoveryCodeUrlFormat = "https://%s/api/v2/users/%s/recovery-code-regeneration"

	VerifySuccessMsg = "Identity check success."
	VerifyFailMsg    = "Identity check failed."

	MfaRequiredError = "mfa_required"
)

func VerifyEmailPassword(ctx context.Context, email, password string) (*VerifyRet, error) {
	if email == "" || password == "" {
		return nil, errs.NewBadRequest("email or password is blank", nil)
	}

	ssoConfig := config.Global().SSOConfig
	// generate request
	verifyEmailPasswordUrl := fmt.Sprintf(OauthTokenUrlFormat, ssoConfig.Auth0Domain)
	values := url.Values{}
	values.Set("grant_type", "password")
	values.Set("username", email)
	values.Set("password", password)
	values.Set("client_id", ssoConfig.M2mAppClientID)
	values.Set("client_secret", ssoConfig.M2mAppClientSecret)

	// call
	httpCode, body, err := callAuth02Verify(ctx, verifyEmailPasswordUrl, values, "VerifyEmailPassword")
	if err != nil {
		tilog.Error(ctx, "VerifyEmailPassword callAuth02Verify error", zap.Any("err", err))
		return nil, err
	}

	// handle response
	return parseAuth0VerifyRet(ctx, httpCode, body, "VerifyEmailPassword")
}

func callAuth02Verify(ctx context.Context, url string, values url.Values, logMsg string) (httpCode int, body []byte, err error) {
	tilog.Info(ctx, fmt.Sprintf("%s request", logMsg), zap.Any("url", url), zap.Any("values", values))
	rsp, err := customHttpClient.PostForm(url, values)
	if err != nil {
		tilog.Warn(ctx, fmt.Sprintf("%s post form error", logMsg), zap.Any("err", err))
		return 0, nil, err
	}
	defer func() {
		_ = rsp.Body.Close()
	}()
	httpCode = rsp.StatusCode
	body, err = io.ReadAll(rsp.Body)
	if err != nil {
		tilog.Warn(ctx, fmt.Sprintf("%s read body error", logMsg), zap.Any("err", err))
		return 0, nil, err
	}
	tilog.Info(ctx, fmt.Sprintf("%s response", logMsg), zap.Any("httpCode", httpCode), zap.Any("rspBody", string(body)))
	return httpCode, body, nil
}

func parseAuth0VerifyRet(ctx context.Context, httpCode int, body []byte, logMsg string) (*VerifyRet, error) {
	if httpCode >= http.StatusOK && httpCode <= http.StatusIMUsed {
		tokenRet, err := parseAccessTokenRet(body)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("%s parseAccessTokenRet error", logMsg), zap.Any("err", err))
			return nil, errs.NewInternalUnknown(VerifyFailMsg, nil)
		}
		return &VerifyRet{
			OK:             true,
			Msg:            VerifySuccessMsg,
			AccessTokenRet: tokenRet,
		}, nil
	}

	errorRet, err := parseErrorRet(body)
	if err != nil {
		tilog.Error(ctx, fmt.Sprintf("%s parseErrorRet error", logMsg), zap.Any("err", err))
		return nil, errs.NewInternalUnknown(VerifyFailMsg, nil)
	}
	ret := &VerifyRet{
		OK:       false,
		Msg:      VerifyFailMsg,
		ErrorRet: errorRet,
	}
	return ret, nil
}

func parseAccessTokenRet(body []byte) (*AccessTokenRet, error) {
	accessTokenRet := &AccessTokenRet{}
	err := json.Unmarshal(body, accessTokenRet)
	if err != nil {
		return nil, err
	}
	return accessTokenRet, nil
}

func parseErrorRet(body []byte) (*ErrorRet, error) {
	errorRet := &ErrorRet{}
	err := json.Unmarshal(body, errorRet)
	if err != nil {
		return nil, err
	}
	return errorRet, nil
}

func VerifyOtpCode(ctx context.Context, mfaToken, otpCode string) (*VerifyRet, error) {
	if mfaToken == "" || otpCode == "" {
		return nil, errs.NewBadRequest("mfa_token or otp_code is blank", nil)
	}

	ssoConfig := config.Global().SSOConfig
	// generate request
	verifyOtpCodeUrl := fmt.Sprintf(OauthTokenUrlFormat, ssoConfig.Auth0Domain)
	values := url.Values{}
	values.Set("grant_type", "http://auth0.com/oauth/grant-type/mfa-otp")
	values.Set("mfa_token", mfaToken)
	values.Set("otp", otpCode)
	values.Set("client_id", ssoConfig.M2mAppClientID)
	values.Set("client_secret", ssoConfig.M2mAppClientSecret)

	// call
	httpCode, body, err := callAuth02Verify(ctx, verifyOtpCodeUrl, values, "VerifyOtpCode")
	if err != nil {
		tilog.Error(ctx, "VerifyOtpCode callAuth02Verify error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("call auth0 verify error", err)
	}

	// handle response
	return parseAuth0VerifyRet(ctx, httpCode, body, "VerifyOtpCode")
}

func VerifyRecoveryCode(ctx context.Context, mfaToken, recoveryCode string) (*VerifyRet, error) {
	if mfaToken == "" || recoveryCode == "" {
		return nil, errs.NewBadRequest("mfa_token or recovery_code is blank", nil)
	}

	ssoConfig := config.Global().SSOConfig
	// generate request
	verifyOtpCodeUrl := fmt.Sprintf(OauthTokenUrlFormat, ssoConfig.Auth0Domain)
	values := url.Values{}
	values.Set("grant_type", "http://auth0.com/oauth/grant-type/mfa-recovery-code")
	values.Set("mfa_token", mfaToken)
	values.Set("recovery_code", recoveryCode)
	values.Set("client_id", ssoConfig.M2mAppClientID)
	values.Set("client_secret", ssoConfig.M2mAppClientSecret)

	// call
	httpCode, body, err := callAuth02Verify(ctx, verifyOtpCodeUrl, values, "VerifyRecoveryCode")
	if err != nil {
		tilog.Error(ctx, "VerifyRecoveryCode callAuth02Verify error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("call auth0 verify error", err)
	}

	// handle response
	return parseAuth0VerifyRet(ctx, httpCode, body, "VerifyRecoveryCode")

}

func RegenerateRecoveryCode(ctx context.Context, auth0UserID string) (string, error) {
	if auth0UserID == "" {
		return "", errs.NewBadRequest("auth0_user_id is blank", nil)
	}

	// generate request
	accessToken, err := GetAuth0AccessToken(ctx)
	if err != nil {
		tilog.Error(ctx, "RegenerateRecoveryCode GetAuth0AccessToken error", zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		return "", err
	}
	regenRecoveryCodeUrl := fmt.Sprintf(RegenerateRecoveryCodeUrlFormat, config.Global().SSOConfig.Auth0Domain, auth0UserID)
	req, _ := http.NewRequest("POST", regenRecoveryCodeUrl, nil)
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))

	tilog.Info(ctx, "RegenerateRecoveryCode request", zap.Any("url", regenRecoveryCodeUrl), zap.Any("accessToken", accessToken))
	// call
	rsp, err := customHttpClient.Do(req)
	if err != nil {
		tilog.Error(ctx, "RegenerateRecoveryCode regen error", zap.Any("err", err))
		return "", errs.NewInternalUnknown("regenerate recovery code error", err)
	}
	defer func() {
		_ = rsp.Body.Close()
	}()
	httpCode := rsp.StatusCode
	body, err := io.ReadAll(rsp.Body)
	if err != nil {
		tilog.Error(ctx, "RegenerateRecoveryCode read body error", zap.Any("err", err))
		return "", errs.NewInternalUnknown("read body error", err)
	}
	tilog.Info(ctx, "RegenerateRecoveryCode response", zap.Any("httpCode", httpCode), zap.Any("body", string(body)))

	// handle response
	if httpCode != http.StatusOK {
		return "", errs.NewInternalUnknown("regenerate recovery code fail", nil)
	}
	tokenRet, err := parseAccessTokenRet(body)
	if err != nil {
		tilog.Error(ctx, "RegenerateRecoveryCode parseAccessTokenRet error", zap.Any("err", err))
		return "", errs.NewInternalUnknown("parse auth0 response error", err)
	}
	return tokenRet.RecoveryCode, nil
}

func EnrollOtpAuthenticator(ctx context.Context, mfaToken string) (*EnrolAuthenticatorRet, error) {
	if mfaToken == "" {
		return nil, errs.NewBadRequest("mfa_token is blank", nil)
	}

	enrollAuthenticatorUrl := fmt.Sprintf(EnrollMfaAuthenticatorUrlFormat, config.Global().SSOConfig.Auth0Domain)
	mapBody := map[string][]string{
		"authenticator_types": {"otp"},
	}
	marshal, _ := json.Marshal(&mapBody)
	jsonBody := string(marshal)
	body := strings.NewReader(jsonBody)
	req, _ := http.NewRequest("POST", enrollAuthenticatorUrl, body)
	req.Header.Set("authorization", fmt.Sprintf("Bearer %s", mfaToken))
	req.Header.Set("content-type", "application/json")

	tilog.Info(ctx, "EnrollOtpAuthenticator request", zap.Any("url", enrollAuthenticatorUrl), zap.Any("mfaToken", mfaToken))
	// call
	rsp, err := customHttpClient.Do(req)
	if err != nil {
		tilog.Error(ctx, "EnrollOtpAuthenticator do error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("request auth0 error", err)
	}
	defer func() {
		_ = rsp.Body.Close()
	}()
	httpCode := rsp.StatusCode
	data, err := io.ReadAll(rsp.Body)
	if err != nil {
		tilog.Error(ctx, "EnrollOtpAuthenticator read body error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("read body error", err)
	}
	tilog.Info(ctx, "EnrollOtpAuthenticator response", zap.Any("httpCode", httpCode), zap.Any("body", string(data)))

	// handle response
	if httpCode == http.StatusOK {
		enrollRet := &EnrolAuthenticatorRet{}
		err = json.Unmarshal(data, enrollRet)
		if err != nil {
			return nil, err
		}
		return enrollRet, nil
	}

	errorRet, err := parseErrorRet(data)
	if err != nil {
		return nil, err
	}
	return nil, errs.NewBadRequest(errorRet.ErrorDesc, nil)

}

func DeleteMfaMethods(ctx context.Context, auth0UserID string) error {
	if auth0UserID == "" {
		return errs.NewBadRequest("auth0_user_id is blank", nil)
	}

	// generate request
	accessToken, err := GetAuth0AccessToken(ctx)
	if err != nil {
		tilog.Error(ctx, "DeleteMfaMethods GetAuth0AccessToken error", zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		return err
	}
	deleteMfaMethodsUrl := fmt.Sprintf(DeleteMfaMethodsUrlFormat, config.Global().SSOConfig.Auth0Domain, auth0UserID)
	req, _ := http.NewRequest("DELETE", deleteMfaMethodsUrl, nil)
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))

	tilog.Info(ctx, "DeleteMfaMethods request", zap.Any("url", deleteMfaMethodsUrl), zap.Any("accessToken", accessToken))
	// call
	rsp, err := customHttpClient.Do(req)
	if err != nil {
		tilog.Error(ctx, "DeleteMfaMethods delete error", zap.Any("err", err))
		return errs.NewInternalUnknown("request auth0 error", err)
	}
	defer func() {
		_ = rsp.Body.Close()
	}()
	httpCode := rsp.StatusCode
	body, err := io.ReadAll(rsp.Body)
	if err != nil {
		tilog.Error(ctx, "DeleteMfaMethods read body error", zap.Any("err", err))
		return errs.NewInternalUnknown("read body error", err)
	}
	tilog.Info(ctx, "DeleteMfaMethods response", zap.Any("httpCode", httpCode), zap.Any("body", string(body)))

	// handle response
	if httpCode >= http.StatusOK && httpCode <= http.StatusIMUsed {
		return nil
	}
	return errs.NewInternalUnknown("Delete fail.", nil)

}

type VerifyRet struct {
	OK             bool
	Msg            string
	AccessTokenRet *AccessTokenRet
	ErrorRet       *ErrorRet
}

type ErrorRet struct {
	Error     string `json:"error"`
	ErrorDesc string `json:"error_description"`
	MfaToken  string `json:"mfa_token"`
}

type EnrolAuthenticatorRet struct {
	AuthenticatorType string   `json:"authenticator_type"`
	Secret            string   `json:"secret"`
	BarcodeUri        string   `json:"barcode_uri"`
	RecoveryCodes     []string `json:"recovery_codes"`
}
