package client

import (
	"context"
	"encoding/json"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/pingcap/errors"
	"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
	// GetClusterID returns the cluster ID of the cluster proxy
	GetClusterID() uint64
}

// 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, clusterID uint64, proxyEndpoint string, opts ...Option) (Client, error)
}

// CreateClientFunc is the function type for creating cluster proxy client
type CreateClientFunc func(ctx context.Context, clusterID uint64, proxyEndpoint string, opts ...Option) (Client, error)

func (f CreateClientFunc) CreateClient(ctx context.Context, clusterID uint64, proxyEndpoint string, opts ...Option) (Client, error) {
	return f(ctx, clusterID, proxyEndpoint, opts...)
}

type Auth func(*resty.Request) error

type Option func(p *clientImpl)

func WithAuth(auth Auth) Option {
	return func(p *clientImpl) {
		p.auth = auth
	}
}

// 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
	clusterID uint64
	baseURL   string
	auth      Auth
	endpoint  string
	timeout   int
}

// CreateClient creates a new instance of the cluster proxy client
func CreateClient(ctx context.Context,
	clusterID uint64,
	proxyEndpoint string,
	opts ...Option,
) (Client, error) {
	logger := tilog.WithContextL(ctx)

	p := &clientImpl{
		clusterID: clusterID,
		// 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(proxyEndpoint)
		proxyEndpoint = p.endpoint + uri.Path
	}
	// generate baseURL, the dataplane always expose http interface now
	if !strings.HasPrefix(proxyEndpoint, "http://") {
		proxyEndpoint = "http://" + proxyEndpoint
	}
	p.baseURL = proxyEndpoint

	transport := &http.Transport{
		DisableKeepAlives: true,
	}
	p.cli = resty.New().EnableTrace().SetTimeout(time.Duration(p.timeout) * time.Second).
		SetTransport(transport)

	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) GetClusterID() uint64 {
	return p.clusterID
}

type ResponseError struct {
	StatusCode int
	Err        error
}

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