package clusterproxy

import (
	"context"
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/pkgv2/infra/aws"

	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/httputil"
)

const (
	changePasswordPath  = "/change_password"
	querySQLBeta2PATH   = "/v1beta2/query"
	executeSQLBeta2PATH = "/v1beta2/execute"
	createAdminPath     = "/v1beta1/create_admin"
	auditLogCheckPath   = "/v1/audit-check"

	// authMethodPrivilegeDB internal use only. use privilege db to auth, only for clusters without SEM
	authMethodPrivilegeDB clusterproxy.AuthMethod = "privilegeDB"
)

// Set Auth0Domain to "" if you want to disable the auth0 authn.
type ProxyConfig struct {
	Auth0Domain       string
	Auth0ClientID     string
	Auth0ClientSecret string
	Auth0Audience     string
}

type Input struct {
	Version   string
	PcaARN    string
	Cfg       ProxyConfig
	ClusterID uint64
	BaseURL   string
	Timeout   int
}

type clusterProxyImp struct {
	operator *clusterproxy.ClusterOperator

	cfg       ProxyConfig
	clusterID uint64
	baseURL   string
	useAdmin  bool
	client    *resty.Client
}

func NewNextGenClusterProxy(
	ctx context.Context,
	input *Input,
	op *clusterproxy.ClusterOperator,
) (ClusterProxy, bizerr.BizError) {
	versions := strings.Split(input.Version, ".")
	majorVersion, err := strconv.Atoi(strings.TrimLeft(versions[0], "v"))
	if err != nil {
		return nil, bizerr.WrapError(err, "parse major version")
	}
	minorVersion, err := strconv.Atoi(versions[1])
	if err != nil {
		return nil, bizerr.WrapError(err, "parse minor version")
	}
	useAdmin := majorVersion >= 5 && (majorVersion != 5 || minorVersion >= 3)

	baseURL := input.BaseURL
	if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
		baseURL = "https://" + baseURL
	}

	var tlsConfig *tls.Config
	if strings.HasPrefix(baseURL, "https://") {
		// Use default TLS configuration, no need to set aws.ProxyTlsConfigV2
		tlsConfig = &tls.Config{
			MinVersion: tls.VersionTLS12,
		}
	}

	if input.Timeout == 0 {
		input.Timeout = 8
	}

	client := resty.
		New().
		SetTimeout(time.Duration(input.Timeout) * time.Second).
		SetTLSClientConfig(tlsConfig).
		SetAllowGetMethodPayload(true)

	tp, err := client.Transport()
	if err != nil {
		return nil, bizerr.WrapError(err, "get transport")
	}
	tp.DisableKeepAlives = true

	return &clusterProxyImp{
		clusterID: input.ClusterID,
		useAdmin:  useAdmin,
		cfg:       input.Cfg,
		baseURL:   baseURL,
		operator:  op,
		client:    client,
	}, nil
}

func NewClusterProxy(
	ctx context.Context,
	input *Input,
	op *clusterproxy.ClusterOperator,
) (ClusterProxy, bizerr.BizError) {
	// parse use admin
	versions := strings.Split(input.Version, ".")
	majorVersion, err := strconv.Atoi(strings.TrimLeft(versions[0], "v"))
	if err != nil {
		return nil, bizerr.WrapError(err, "buildClusterProxy parse major version")
	}
	minorVersion, err := strconv.Atoi(versions[1])
	if err != nil {
		return nil, bizerr.WrapError(err, "buildClusterProxy parse minor version")
	}
	useAdmin := majorVersion >= 5 && (majorVersion != 5 || minorVersion >= 3)

	// generate baseURL
	baseURL := input.BaseURL
	if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
		baseURL = "https://" + baseURL
	}

	var tlsConfig *tls.Config
	if strings.HasPrefix(baseURL, "https://") {
		tlsConfig, err = aws.ProxyTlsConfigV2(ctx, input.PcaARN)
		if err != nil {
			return nil, bizerr.WrapError(err, "buildClusterProxy get tls config")
		}
	}

	// set http client timeout as 8 seconds by default
	// backend can handler the return error instead of report frontend timeout
	if input.Timeout == 0 {
		input.Timeout = 8
	}

	client := resty.
		New().
		SetTimeout(time.Duration(input.Timeout) * time.Second).
		SetTLSClientConfig(tlsConfig).
		SetAllowGetMethodPayload(true)

	tp, err := client.Transport()
	if err != nil {
		return nil, bizerr.WrapError(err, "buildClusterProxy get transport")
	}
	tp.DisableKeepAlives = true

	return &clusterProxyImp{
		clusterID: input.ClusterID,
		useAdmin:  useAdmin,
		cfg:       input.Cfg,
		baseURL:   baseURL,
		operator:  op,
		client:    client,
	}, nil
}

func (p *clusterProxyImp) SetOperator(operator *clusterproxy.ClusterOperator) bizerr.BizError {
	p.operator = operator
	return nil
}

func (p *clusterProxyImp) ExecuteSQL(ctx context.Context, sql string) bizerr.BizError {
	m := &struct {
		Operator  *clusterproxy.ClusterOperator `json:"operator"`
		ClusterID uint64                        `json:"clusterID"`
		Query     string                        `json:"query"`
	}{
		Operator:  p.amendOperator(ctx, p.operator),
		ClusterID: p.clusterID,
		Query:     sql,
	}
	respBytes, err := p.request(ctx, http.MethodPost, p.baseURL, executeSQLBeta2PATH, m)
	if err != nil {
		return err
	}

	resp := &clusterproxy.ExecuteResponse{}
	if err := json.Unmarshal(respBytes, resp); err != nil {
		return bizerr.WrapError(err, "unmarshal response")
	}

	if resp.ErrNumber != 0 {
		return bizerr.New(bizerr.ErrCodeClusterProxy, SqlErrFormat, resp.ErrNumber, resp.ErrMessage)
	}

	return nil
}

func (p *clusterProxyImp) ExecuteMultiSQL(ctx context.Context, sqls []string) bizerr.BizError {
	m := &struct {
		Operator  *clusterproxy.ClusterOperator `json:"operator"`
		ClusterID uint64                        `json:"clusterID"`
		Queries   []string                      `json:"queries"`
	}{
		Operator:  p.amendOperator(ctx, p.operator),
		ClusterID: p.clusterID,
		Queries:   sqls,
	}
	respBytes, err := p.request(ctx, http.MethodPost, p.baseURL, executeSQLBeta2PATH, m)
	if err != nil {
		return err
	}

	resp := &clusterproxy.ExecuteResponse{}
	if err := json.Unmarshal(respBytes, resp); err != nil {
		return bizerr.WrapError(err, "unmarshal response")
	}

	if resp.ErrNumber != 0 {
		return bizerr.New(bizerr.ErrCodeClusterProxy, SqlErrFormat, resp.ErrNumber, resp.ErrMessage)
	}

	return nil
}

func (p *clusterProxyImp) InitCloudAdminPassword(ctx context.Context, cloudAdminPwd string) bizerr.BizError {
	if !p.useAdmin {
		return nil
	}
	m := &struct {
		CloudAdminPassword string `json:"cloudAdminPassword"`
	}{
		CloudAdminPassword: base64.URLEncoding.EncodeToString([]byte(cloudAdminPwd)),
	}
	resp, err := p.request(ctx, http.MethodPost, p.baseURL, createAdminPath, m)
	if err != nil {
		return err.Chain(string(resp))
	}

	return nil
}

func (p *clusterProxyImp) ChangePassword(ctx context.Context, username, newPwd string) bizerr.BizError {
	m := &struct {
		Operator    *clusterproxy.ClusterOperator `json:"operator"`
		Username    string                        `json:"username"`
		ClusterID   uint64                        `json:"clusterID"`
		NewPassword string                        `json:"newPassword"`
	}{
		Operator:    p.amendOperator(ctx, p.operator),
		Username:    username,
		ClusterID:   p.clusterID,
		NewPassword: base64.URLEncoding.EncodeToString([]byte(newPwd)),
	}
	resp, err := p.request(ctx, http.MethodPost, p.baseURL, changePasswordPath, m)
	if err != nil {
		return err.Chain(string(resp))
	}

	return nil
}

func (p *clusterProxyImp) QuerySQL(ctx context.Context, sql string) ([]byte, bizerr.BizError) {
	m := &struct {
		Operator  *clusterproxy.ClusterOperator `json:"operator"`
		ClusterID uint64                        `json:"clusterID"`
		Query     string                        `json:"query"`
	}{
		Operator:  p.amendOperator(ctx, p.operator),
		ClusterID: p.clusterID,
		Query:     sql,
	}
	respBytes, err := p.request(ctx, http.MethodGet, p.baseURL, querySQLBeta2PATH, m)
	if err != nil {
		return nil, err
	}

	resp := &clusterproxy.QuerySQLResponse{}
	if err := json.Unmarshal(respBytes, resp); err != nil {
		return nil, bizerr.WrapError(err, "unmarshal response")
	}

	if resp.ErrNumber != 0 {
		return nil, bizerr.New(bizerr.ErrCodeClusterProxy, SqlErrFormat, resp.ErrNumber, resp.ErrMessage)
	}

	return resp.Result, nil
}

func (p *clusterProxyImp) request(
	ctx context.Context,
	method, baseUrl, path string,
	requestBody any,
) ([]byte, bizerr.BizError) {
	data, err := json.Marshal(requestBody)
	if err != nil {
		return nil, bizerr.WrapError(err, "marshal request")
	}

	var token string
	// Skip the auth0 token request if the auth0 config is empty.
	// Dev or staging may disable the auth0 authn, to save money.
	if p.cfg.Auth0Domain != "" && p.cfg.Auth0ClientID != "" && p.cfg.Auth0ClientSecret != "" {
		token, err = auth0.GetClientToken(
			p.cfg.Auth0Domain,
			p.cfg.Auth0ClientID,
			p.cfg.Auth0ClientSecret,
			p.cfg.Auth0Audience,
		)
		if err != nil {
			return nil, bizerr.WrapError(err, "get token")
		}
	}

	resp, err := httputil.DoRequest(ctx, p.client, method, baseUrl, path, func(req *resty.Request) {
		req.SetBody(data)
		req.ForceContentType("application/json")

		if token != "" {
			req.SetAuthToken(token)
		}
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "do request")
	}

	if resp.StatusCode() >= 400 {
		return resp.Body(), bizerr.New(bizerr.ErrCodeClusterProxy, "Request fail, %d", resp.StatusCode())
	}

	return resp.Body(), nil
}

func (p *clusterProxyImp) amendOperator(
	_ context.Context,
	operator *clusterproxy.ClusterOperator,
) *clusterproxy.ClusterOperator {
	out := &clusterproxy.ClusterOperator{
		Username:   operator.Username,
		AuthMethod: operator.AuthMethod,
		Credential: base64.URLEncoding.EncodeToString([]byte(operator.Credential)),
	}
	if !p.useAdmin {
		out.AuthMethod = authMethodPrivilegeDB
		out.Credential = ""
	}

	return out
}

// AuditLogCheck is for next-gen only, which uses service-proxy to do the check.
func (p *clusterProxyImp) AuditLogCheck(
	ctx context.Context,
	audtlogReq *AuditCheckModel,
) (bool, bizerr.BizError) {
	_, err := p.request(ctx, http.MethodPost, p.baseURL, auditLogCheckPath, audtlogReq)
	if err != nil {
		if e, yes := bizerr.AsCode(err, bizerr.ErrCodeClusterProxy); yes {
			return false, bizerr.New(bizerr.ErrCodeParams, "Audit log check failed, %s", e.ErrMsg())
		}
		return false, err
	}
	return true, nil
}
