// Package regionalapi add by blacknc <blacknc@163.com> at 2024/09/02 15:39:15
package regionalapi

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

	"github.com/pingcap/errors"
	"github.com/tidbcloud/dataflow-service/pkg/log"
	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	regionalpb "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"go.uber.org/zap"
)

// TCIDMHTTPClient Used to send requests to tcidm regional server
type TCIDMHTTPClient interface {

	// import cr api
	CreateImportIgnoreExists(ctx context.Context, in *cloudv1.Import) error
	GetImport(ctx context.Context, ns, name string) (*cloudv1.Import, error)
	DelImportIgnoreNotExists(ctx context.Context, ns, name string) error

	CreateDMJobIgnoreExists(ctx context.Context, in *v1alpha1.DMJob) error
	UpdateDMJob(ctx context.Context, in *v1alpha1.DMJob) error
	GetDMJob(ctx context.Context, ns, name string) (*v1alpha1.DMJob, error)
	DelDMJobIgnoreNotExists(ctx context.Context, ns, name string) error

	CreatePrivateLinkEndpointIgnoreExists(ctx context.Context, in *v1alpha1.PrivateLinkEndpoint) error
	DeletePrivateLinkEndpointIgnoreExists(ctx context.Context, ns, name string) error
	GetPrivateLinkEndpoint(ctx context.Context, ns, name string) (*v1alpha1.PrivateLinkEndpoint, error)

	ListVPCs(ctx context.Context, orgID uint64, projectID uint64, clusterID uint64, statuses []VPCStatus) ([]RegionalNetwork, error)
	GetClusterRole(ctx context.Context, clusterID uint64) (*regionalpb.ClusterRole, error)

	// proxy request to tcidm regional server
	ProxyGET(ctx context.Context, path string, header map[string]string) ([]byte, error)
	ProxyPOST(ctx context.Context, path string, data any, header map[string]string) ([]byte, error)
	ProxyPUT(ctx context.Context, path string, data any, header map[string]string) ([]byte, error)
	ProxyDELETE(ctx context.Context, url string, header map[string]string) ([]byte, error)

	SetResponseHandler(handler ResponseHandler) TCIDMHTTPClient
}

type tcidmHTTPClientImpl struct {
	endpoint        string
	httpcli         HTTPRequester
	responseHandler ResponseHandler
}

func NewTCIDMHTTPClient(endpoint string) TCIDMHTTPClient {
	return &tcidmHTTPClientImpl{
		endpoint:        endpoint,
		httpcli:         NewHttpRequester(nil, "tcidm_regional"),
		responseHandler: tcidmResponseHandler,
	}
}

type tcidmResponse struct {
	Code    int             `json:"code"`
	Message string          `json:"message"`
	Data    json.RawMessage `json:"data"`
}

func (r *tcidmResponse) Error() error {
	if r.Code != 0 {
		return fmt.Errorf("tcidm request failed, code:%d, message:%s", r.Code, r.Message)
	}
	return nil
}

func (r *tcidmResponse) Unmarshal(target any) error {
	return json.Unmarshal(r.Data, target)
}

func (c *tcidmHTTPClientImpl) buildRequestURL(path string, querys ...string) string {
	requrl := c.endpoint + filepath.Join("/api/v1/tcidm", path)
	if len(querys) > 0 {
		values := url.Values{}
		for i := 0; i < len(querys)-1; i += 2 {
			values.Add(querys[i], querys[i+1])
		}
		if len(querys) > 0 {
			requrl += "?" + values.Encode()
		}
	}
	return requrl
}

func (c *tcidmHTTPClientImpl) CreateImportIgnoreExists(ctx context.Context, in *cloudv1.Import) error {
	logger := log.GetLogger(ctx)
	_, err := c.ProxyPOST(ctx, "/import/cr", in, nil)
	if err != nil && !TCIDMIsAlreadyExists(err) {
		logger.Error("failed to create import CR", zap.String("namespace", in.Namespace), zap.String("name", in.Name), zap.Error(err))
		return err
	}
	return nil
}

func (c *tcidmHTTPClientImpl) GetImport(ctx context.Context, ns, name string) (*cloudv1.Import, error) {
	logger := log.GetLogger(ctx)
	url := c.buildRequestURL("/import/cr", "namespace", ns, "name", name)
	data, err := c.ProxyGET(ctx, url, nil)
	if err != nil {
		logger.Error("failed to get import CR", zap.String("ns", ns), zap.String("name", name), zap.Error(err))
		return nil, err
	}
	var importCR cloudv1.Import
	if err := json.Unmarshal(data, &importCR); err != nil {
		return nil, err
	}
	return &importCR, nil
}

func (c *tcidmHTTPClientImpl) DelImportIgnoreNotExists(ctx context.Context, ns, name string) error {
	logger := log.GetLogger(ctx)
	url := c.buildRequestURL("/import/cr", "namespace", ns, "name", name)
	_, err := c.ProxyDELETE(ctx, url, nil)
	if err != nil && !TCIDMIsNotFound(err) {
		logger.Error("failed to delete import CR", zap.String("ns", ns), zap.String("name", name), zap.Error(err))
		return err
	}
	return nil
}

func (c *tcidmHTTPClientImpl) CreateDMJobIgnoreExists(ctx context.Context, in *v1alpha1.DMJob) error {
	logger := log.GetLogger(ctx)
	_, err := c.ProxyPOST(ctx, "/dmjob/cr", in, nil)
	if err != nil && !TCIDMIsAlreadyExists(err) {
		logger.Error("failed to create DM job CR", zap.String("namespace", in.Namespace), zap.String("name", in.Name), zap.Error(err))
		return err
	}
	return nil
}

func (c *tcidmHTTPClientImpl) UpdateDMJob(ctx context.Context, in *v1alpha1.DMJob) error {
	logger := log.GetLogger(ctx)
	_, err := c.ProxyPUT(ctx, "/dmjob/cr", in, nil)
	if err != nil {
		logger.Error("failed to update DM job CR", zap.String("namespace", in.Namespace), zap.String("name", in.Name), zap.Error(err))
	}
	return err
}

func (c *tcidmHTTPClientImpl) GetDMJob(ctx context.Context, ns, name string) (*v1alpha1.DMJob, error) {
	logger := log.GetLogger(ctx)
	url := c.buildRequestURL("/dmjob/cr", "namespace", ns, "name", name)
	data, err := c.ProxyGET(ctx, url, nil)
	if err != nil {
		logger.Error("failed to get DM job CR", zap.String("ns", ns), zap.String("name", name), zap.Error(err))
		return nil, err
	}
	var dmJobCR v1alpha1.DMJob
	if err := json.Unmarshal(data, &dmJobCR); err != nil {
		return nil, err
	}
	return &dmJobCR, nil
}

func (c *tcidmHTTPClientImpl) DelDMJobIgnoreNotExists(ctx context.Context, ns, name string) error {
	logger := log.GetLogger(ctx)
	url := c.buildRequestURL("/dmjob/cr", "namespace", ns, "name", name)
	_, err := c.ProxyDELETE(ctx, url, nil)
	if err != nil && !TCIDMIsNotFound(err) {
		logger.Error("failed to delete DM job CR", zap.String("ns", ns), zap.String("name", name), zap.Error(err))
		return err
	}
	return nil
}

func (c *tcidmHTTPClientImpl) CreatePrivateLinkEndpointIgnoreExists(ctx context.Context, in *v1alpha1.PrivateLinkEndpoint) error {
	logger := log.GetLogger(ctx)
	_, err := c.ProxyPOST(ctx, "/privatelinkendpoint/cr", in, nil)
	if err != nil && !TCIDMIsAlreadyExists(err) {
		logger.Error("failed to create private link endpoint CR", zap.String("namespace", in.Namespace), zap.String("name", in.Name), zap.Error(err))
		return err
	}
	return nil
}

func (c *tcidmHTTPClientImpl) DeletePrivateLinkEndpointIgnoreExists(ctx context.Context, ns, name string) error {
	logger := log.GetLogger(ctx)
	url := c.buildRequestURL("/privatelinkendpoint/cr", "namespace", ns, "name", name)
	_, err := c.ProxyDELETE(ctx, url, nil)
	if err != nil && !TCIDMIsNotFound(err) {
		logger.Error("failed to delete private link endpoint CR", zap.String("ns", ns), zap.String("name", name), zap.Error(err))
		return err
	}
	return nil
}

func (c *tcidmHTTPClientImpl) GetPrivateLinkEndpoint(ctx context.Context, ns, name string) (*v1alpha1.PrivateLinkEndpoint, error) {
	logger := log.GetLogger(ctx)
	url := c.buildRequestURL("/privatelinkendpoint/cr", "namespace", ns, "name", name)
	data, err := c.ProxyGET(ctx, url, nil)
	if err != nil {
		logger.Error("failed to get private link endpoint CR", zap.String("ns", ns), zap.String("name", name), zap.Error(err))
		return nil, err
	}
	var plEndoint v1alpha1.PrivateLinkEndpoint
	if err := json.Unmarshal(data, &plEndoint); err != nil {
		return nil, err
	}
	return &plEndoint, nil
}

func (c *tcidmHTTPClientImpl) ListVPCs(ctx context.Context, orgID uint64, projectID uint64, clusterID uint64, statuses []VPCStatus) ([]RegionalNetwork, error) {
	logger := log.GetLogger(ctx)
	in := map[string]any{
		"tenant_id":  fmt.Sprintf("%d", orgID),
		"project_id": fmt.Sprintf("%d", projectID),
		"cluster_id": fmt.Sprintf("%d", clusterID),
		"statuses":   statuses,
	}
	data, err := c.ProxyPOST(ctx, "/vpc/list", in, nil)
	if err != nil {
		logger.Error("failed to list VPCs", zap.Uint64("orgID", orgID), zap.Uint64("clusterID", clusterID), zap.Error(err))
		return nil, err
	}
	var resp ListVPCResponse
	if err := json.Unmarshal(data, &resp); err != nil {
		return nil, err
	}
	return resp.VPC, nil
}

func (c *tcidmHTTPClientImpl) GetClusterRole(ctx context.Context, clusterID uint64) (*regionalpb.ClusterRole, error) {
	logger := log.GetLogger(ctx)
	data, err := c.ProxyGET(ctx, fmt.Sprintf("/cluster/role?cluster_id=%d", clusterID), nil)
	if err != nil {
		logger.Error("failed to get cluster role", zap.Uint64("clusterID", clusterID), zap.Error(err))
		return nil, err
	}
	var resp regionalpb.ClusterRole
	if err := json.Unmarshal(data, &resp); err != nil {
		return nil, err
	}
	return &resp, nil
}

func (c *tcidmHTTPClientImpl) ProxyGET(ctx context.Context, url string, header map[string]string) ([]byte, error) {

	if !strings.HasPrefix(url, c.endpoint) {
		url = c.buildRequestURL(url)
	}
	opts := &HTTPRequesterOption{
		Before: []RequestHandler{
			tcidmRequestHeaderHandler(header),
		},
		After: c.responseHandler,
	}
	return c.httpcli.GET(ctx, url, opts)
}

func (c *tcidmHTTPClientImpl) ProxyDELETE(ctx context.Context, url string, header map[string]string) ([]byte, error) {

	if !strings.HasPrefix(url, c.endpoint) {
		url = c.buildRequestURL(url)
	}
	opts := &HTTPRequesterOption{
		Before: []RequestHandler{
			tcidmRequestHeaderHandler(header),
		},
		After: c.responseHandler,
	}
	return c.httpcli.DELETE(ctx, url, opts)
}

func (c *tcidmHTTPClientImpl) ProxyPOST(ctx context.Context, url string, data any, header map[string]string) ([]byte, error) {
	return c.proxyPOSTANDPUT(ctx, http.MethodPost, url, data, header)
}

func (c *tcidmHTTPClientImpl) ProxyPUT(ctx context.Context, url string, data any, header map[string]string) ([]byte, error) {
	return c.proxyPOSTANDPUT(ctx, http.MethodPut, url, data, header)
}

func (c *tcidmHTTPClientImpl) proxyPOSTANDPUT(ctx context.Context, method string, url string, data any, header map[string]string) ([]byte, error) {

	if !strings.HasPrefix(url, c.endpoint) {
		url = c.buildRequestURL(url)
	}
	opts := &HTTPRequesterOption{
		Before: []RequestHandler{
			tcidmRequestHeaderHandler(header),
		},
		After: c.responseHandler,
	}

	var body io.Reader
	if b, ok := data.(io.Reader); ok {
		body = b
	} else {
		bs, err := json.Marshal(data)
		if err != nil {
			return nil, err
		}
		body = bytes.NewReader(bs)
	}

	var (
		err error
		out []byte
	)
	switch method {
	case http.MethodPost:
		out, err = c.httpcli.POST(ctx, url, body, opts)
	case http.MethodPut:
		out, err = c.httpcli.PUT(ctx, url, body, opts)
	default:
		err = errors.Errorf("tcidmHTTPClientImpl.proxyPOSTANDPUT got unsupported method:%s", method)
	}
	return out, err
}

func (c *tcidmHTTPClientImpl) SetResponseHandler(handler ResponseHandler) TCIDMHTTPClient {
	c.responseHandler = handler
	return c
}

func TCIDMIsNotFound(err error) bool {
	return strings.Contains(err.Error(), "not found")
}

func TCIDMIsAlreadyExists(err error) bool {
	return strings.Contains(err.Error(), "already exists")
}

func tcidmResponseHandler(resp *http.Response) ([]byte, error) {
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		return nil, fmt.Errorf("http request failed, method:%s, url:%s, "+
			"response_status:%s response_body:%s",
			resp.Request.Method, resp.Request.URL.String(), resp.Status, data)
	} else if len(data) == 0 {
		return nil, fmt.Errorf("http request response empty body, method:%s, url:%s, "+
			"response_status:%s",
			resp.Request.Method, resp.Request.URL.String(), resp.Status)
	}

	var out tcidmResponse
	if err := json.Unmarshal(data, &out); err != nil {
		return nil, err
	} else if err := out.Error(); err != nil {
		return nil, err
	}
	return out.Data, nil
}

func RawResponseHandler(resp *http.Response) ([]byte, error) {
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		return nil, fmt.Errorf("http request failed, method:%s, url:%s, "+
			"response_status:%s response_body:%s",
			resp.Request.Method, resp.Request.URL.String(), resp.Status, data)
	}
	return data, nil
}

func tcidmRequestHeaderHandler(header map[string]string) RequestHandler {
	return func(req *http.Request) error {
		for k, v := range header {
			req.Header.Set(k, v)
		}
		return nil
	}
}
