package client

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"net/http"
	"net/url"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/pingcap/errors"
	cspb "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

var (
	// DefaultClientCreator is the default cluster proxy client creator
	DefaultClientCreator CreateClientFunc = CreateClient
	_                    Client           = &clientImpl{}
)

// set http client timeout as 8 seconds by default
const defaultTimeout = 8

// Client is the interface for cluster proxy client
type Client interface {
	// Request sends a request to the cluster proxy
	// the method is the http method, e.g. GET, POST, PUT, DELETE
	// the relatedURL is the relative URL of the request, it will be appended to the base URL, and the final URL is the base URL + relatedURL
	// the body is the request body, it will be marshaled to JSON
	// the dest is the response body, it will be unmarshaled from JSON
	Request(ctx context.Context, method string, relative string, body interface{}, dest interface{}) error
	// GetTiDBGroup returns the tidb group of the cluster
	GetTiDBGroup() *cspb.TiDBGroup
}

// Creator is the interface for creating cluster proxy client
type Creator interface {
	// CreateClient creates a new instance of the cluster proxy client
	// the clusterID is the cluster ID of the cluster proxy
	// the proxyEndpoint is the base URL of the cluster proxy
	// the timeout is the timeout for the http client
	// the cfg is the configuration for the cluster proxy client
	CreateClient(ctx context.Context, group *cspb.TiDBGroup, authToke, apiGateWay string, opts ...Option) (Client, error)
}

// CreateClientFunc is the function type for creating cluster proxy client
type CreateClientFunc func(ctx context.Context, group *cspb.TiDBGroup, authToke, apiGateWay string, opts ...Option) (Client, error)

func (f CreateClientFunc) CreateClient(ctx context.Context, group *cspb.TiDBGroup, authToken, apiGateWay string, opts ...Option) (Client, error) {
	return f(ctx, group, authToken, apiGateWay, opts...)
}

type Auth func(*resty.Request) error

type Option func(p *clientImpl)

// Specify endpoint manually
func WithEndpoint(ep string) Option {
	return func(p *clientImpl) {
		p.endpoint = ep
	}
}

func WithTimeout(timeout int) Option {
	return func(p *clientImpl) {
		p.timeout = timeout
	}
}

type clientImpl struct {
	cli      *resty.Client
	group    *cspb.TiDBGroup
	baseURL  string
	auth     Auth
	endpoint string
	timeout  int
}

// CreateClient creates a new instance of the cluster proxy client
func CreateClient(ctx context.Context,
	group *cspb.TiDBGroup,
	authToken string,
	apiGateWay string,
	opts ...Option,
) (Client, error) {
	logger := tilog.WithContextL(ctx)

	p := &clientImpl{
		group: group,
		// backend can handler the return error instead of report frontend timeout
		timeout: defaultTimeout,
	}
	for _, opt := range opts {
		opt(p)
	}

	if p.endpoint != "" { // overlay endpoint for local development
		uri, _ := url.Parse(apiGateWay)
		apiGateWay = p.endpoint + uri.Path
	}
	p.baseURL = apiGateWay

	transport := &http.Transport{
		DisableKeepAlives: true,
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	}
	p.cli = resty.New().EnableTrace().SetTimeout(time.Duration(p.timeout) * time.Second).
		SetTransport(transport)
	p.cli.SetAuthScheme("Bearer").SetAuthToken(authToken)
	logger.Info("create cluster proxy", zap.String("baseURL", p.baseURL))

	return p, nil
}

func (p *clientImpl) Request(ctx context.Context, method string, url string, body interface{}, dest interface{}) error {
	url = p.baseURL + url
	req := p.cli.R().SetContext(ctx).SetBody(body)
	if p.auth != nil {
		if err := p.auth(req); err != nil {
			return err
		}
	}

	resp, err := req.Execute(method, url)

	logger := tilog.WithContextL(ctx).With(
		zap.String("method", method),
		zap.String("url", url),
		zap.Any("http_trace", req.TraceInfo()))
	if err != nil {
		logger.Error("http request error", zap.Any("resp", resp), zap.Error(err))
		return errors.Trace(err)
	}
	if !resp.IsSuccess() {
		logger.Warn("http request not success",
			zap.String("status", resp.Status()),
			zap.Any("resp", resp))
		return errors.New(resp.String())
	}
	logger.Info("http request OK")

	if dest == nil {
		return nil
	}
	if err = json.Unmarshal(resp.Body(), dest); err != nil {
		return errors.Trace(err)
	}
	return nil
}

func (p *clientImpl) GetTiDBGroup() *cspb.TiDBGroup {
	return p.group
}

type ResponseError struct {
	StatusCode int
	Err        error
}

func (e *ResponseError) Error() string {
	return e.Err.Error()
}
