package regionalservice

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/pingcap/errors"
	cspb "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	regionalpb "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/v1"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/ticdc-service-ng/pkg/config"
	"github.com/tidbcloud/ticdc-service-ng/pkg/debug/portforward"
	"github.com/tidbcloud/ticdc-service-ng/pkg/util"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/emptypb"
)

type Client interface {
	// GetOneTiDBGroupByClusterID returns a tidb group by the clusterID
	GetOneTiDBGroupByClusterID(ctx context.Context, clusterID string) (*cspb.TiDBGroup, error)
	// GetTiDBClusterRole returns the role info of the TiDB cluster
	GetTiDBClusterRole(ctx context.Context, clusterID string) (*cspb.ProviderRole, error)
	// GetSharedPoolByID returns a shared pool by the shared pool ID
	GetSharedPoolByID(ctx context.Context, sharedPoolID string) (*cspb.SharedPool, error)
	// GetDataPlaneByID returns the appbox information by the id
	GetDataPlaneByID(ctx context.Context, ID string) (*regionalpb.DataPlane, error)
	ListDataPlane(ctx context.Context) (*regionalpb.ListDataPlanesResponse, error)
	CreateSinkEndpoint(ctx context.Context, request *regionalpb.CreatePrivateLinkEndpointRequest) (*regionalpb.PrivateLinkEndpoint, error)
	ValidatePrivateLinkEndpointZones(ctx context.Context, request *regionalpb.ValidatePrivateLinkEndpointZonesRequest) error
	DescribePrivateLinkEndpointServiceZones(ctx context.Context, request *regionalpb.DescribePrivateLinkEndpointServiceZonesRequest) (*regionalpb.DescribePrivateLinkEndpointServiceZonesResponse, error)
	GetPrivateLinkEndpoint(ctx context.Context, request *regionalpb.GetPrivateLinkEndpointRequest) (*regionalpb.PrivateLinkEndpoint, error)
	DeletePrivateLinkEndpoint(ctx context.Context, request *regionalpb.DeletePrivateLinkEndpointRequest) error
	ListPrivateLinkEndpoint(ctx context.Context, request *regionalpb.ListPrivateLinkEndpointsRequest) (*regionalpb.ListPrivateLinkEndpointsResponse, error)
}

var _ Client = &client{}

type client struct {
	clusterServiceCli   *resty.Client
	dataPlaneServiceCli *resty.Client
	unmarshalOpt        protojson.UnmarshalOptions
}

func NewRegionalServiceClient(cfg *config.Config) Client {
	var roundTripper http.RoundTripper
	if cfg.Debug.Enable && cfg.Debug.EnableRegionalPortForward && cfg.Debug.DataPlaneKubeConfig != "" {
		roundTripper = portforward.NewKubeTransport(portforward.NewPortForwardManager(cfg.Kubeconfig))
	}
	clusterServiceCli := resty.New().EnableTrace().SetTimeout(time.Second * 60).
		SetBaseURL(cfg.ClusterServiceBaseURL).SetTransport(roundTripper).
		SetJSONUnmarshaler(unmarshallerWrapper).SetJSONMarshaler(marshallerWrapper)
	dataPlaneServiceCli := resty.New().EnableTrace().SetTimeout(time.Second * 60).
		SetBaseURL(cfg.DataPlaneManagerServiceBaseURL).SetTransport(roundTripper).
		SetJSONUnmarshaler(unmarshallerWrapper).SetJSONMarshaler(marshallerWrapper)
	return &client{
		clusterServiceCli:   clusterServiceCli,
		dataPlaneServiceCli: dataPlaneServiceCli,
		unmarshalOpt: protojson.UnmarshalOptions{
			DiscardUnknown: true,
		},
	}
}

func marshallerWrapper(v interface{}) ([]byte, error) {
	m, ok := v.(proto.Message)
	if ok {
		return protojson.Marshal(m)
	} else {
		return json.Marshal(v)
	}
}

func unmarshallerWrapper(data []byte, v interface{}) error {
	m, ok := v.(proto.Message)
	if ok {
		return protojson.Unmarshal(data, m)
	} else {
		return json.Unmarshal(data, v)
	}
}

func (c *client) GetOneTiDBGroupByClusterID(ctx context.Context, clusterID string) (*cspb.TiDBGroup, error) {
	var groupList cspb.ListTiDBGroupsResponse
	err := c.callAPI(ctx, c.clusterServiceCli, "GET",
		fmt.Sprintf("/api/v1beta/clusters/%s/tidbGroups", clusterID), nil, nil, &groupList,
	)
	if err != nil {
		return nil, errors.Trace(err)
	}
	if len(groupList.TidbGroups) == 0 {
		return nil, &NotFoundError{
			Resource: "tidb group",
			ID:       clusterID,
			Msg:      fmt.Sprintf("no TiDB group found for cluster: %s", clusterID),
		}
	}
	return groupList.TidbGroups[0], nil
}

func (c *client) GetTiDBClusterRole(ctx context.Context, clusterID string) (*cspb.ProviderRole, error) {
	role := new(cspb.ProviderRole)
	err := c.callAPI(ctx, c.clusterServiceCli, "GET",
		fmt.Sprintf("/api/v1beta/clusters/%s/providerRole", clusterID), nil, nil, role,
	)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return role, nil
}

func (c *client) GetSharedPoolByID(ctx context.Context, sharedPoolID string) (*cspb.SharedPool, error) {
	var sharedPool cspb.SharedPool
	err := c.callAPI(ctx, c.clusterServiceCli, "GET",
		fmt.Sprintf("/api/v1beta/sharedPools/%s", sharedPoolID), nil, nil, &sharedPool,
	)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return &sharedPool, nil
}

func (c *client) GetDataPlaneByID(ctx context.Context, ID string) (*regionalpb.DataPlane, error) {
	var dataplane regionalpb.DataPlane
	if err := c.callAPI(ctx, c.dataPlaneServiceCli, "GET",
		fmt.Sprintf("/api/v1/dataplanes/%s", ID), nil, nil, &dataplane,
	); err != nil {
		return nil, errors.Trace(err)
	}
	return &dataplane, nil
}

func (c *client) ListDataPlane(ctx context.Context) (*regionalpb.ListDataPlanesResponse, error) {
	var dataplanes regionalpb.ListDataPlanesResponse
	if err := c.callAPI(ctx, c.dataPlaneServiceCli, "GET",
		"/api/v1/dataplanes", nil, nil, &dataplanes,
	); err != nil {
		return nil, errors.Trace(err)
	}
	return &dataplanes, nil
}

func (c *client) CreateSinkEndpoint(ctx context.Context, request *regionalpb.CreatePrivateLinkEndpointRequest) (*regionalpb.PrivateLinkEndpoint, error) {
	var endpoint regionalpb.PrivateLinkEndpoint
	if err := c.callAPI(ctx, c.dataPlaneServiceCli, "POST",
		fmt.Sprintf("/api/v1/dataplanes/%s/privateLinkEndpoints", request.DataPlaneId),
		map[string]string{
			"id": request.Id,
		},
		request.PrivateLinkEndpoint, &endpoint,
	); err != nil {
		return nil, errors.Trace(err)
	}
	return &endpoint, nil
}

func (c *client) ValidatePrivateLinkEndpointZones(ctx context.Context, request *regionalpb.ValidatePrivateLinkEndpointZonesRequest) error {
	var empty emptypb.Empty
	if err := c.callAPI(ctx, c.dataPlaneServiceCli, "POST",
		fmt.Sprintf("/api/v1/dataplanes/%s/privateLinkEndpoints:validateZones", request.DataPlaneId),
		nil, request, &empty,
	); err != nil {
		return errors.Trace(err)
	}
	return nil
}

func (c *client) DescribePrivateLinkEndpointServiceZones(ctx context.Context, request *regionalpb.DescribePrivateLinkEndpointServiceZonesRequest) (*regionalpb.DescribePrivateLinkEndpointServiceZonesResponse, error) {
	var resp regionalpb.DescribePrivateLinkEndpointServiceZonesResponse
	if err := c.callAPI(ctx, c.dataPlaneServiceCli, "GET",
		fmt.Sprintf("/api/v1/dataplanes/%s/privateLinkEndpointServices:describeZones", request.DataPlaneId),
		map[string]string{
			"serviceName":   request.ServiceName,
			"serviceRegion": request.ServiceRegion,
		},
		nil, &resp,
	); err != nil {
		return nil, errors.Trace(err)
	}
	return &resp, nil
}

func (c *client) GetPrivateLinkEndpoint(ctx context.Context, request *regionalpb.GetPrivateLinkEndpointRequest) (*regionalpb.PrivateLinkEndpoint, error) {
	var endpoint regionalpb.PrivateLinkEndpoint
	if err := c.callAPI(ctx, c.dataPlaneServiceCli, "GET",
		fmt.Sprintf("/api/v1/dataplanes/%s/privateLinkEndpoints/%s", request.DataPlaneId, request.Id),
		nil, nil, &endpoint,
	); err != nil {
		return nil, errors.Trace(err)
	}
	return &endpoint, nil
}

func (c *client) ListPrivateLinkEndpoint(ctx context.Context, request *regionalpb.ListPrivateLinkEndpointsRequest) (*regionalpb.ListPrivateLinkEndpointsResponse, error) {
	var endpoint regionalpb.ListPrivateLinkEndpointsResponse
	if err := c.callAPI(ctx, c.dataPlaneServiceCli, "GET",
		fmt.Sprintf("/api/v1/dataplanes/%s/privateLinkEndpoints", request.DataPlaneId),
		nil, nil, &endpoint,
	); err != nil {
		return nil, errors.Trace(err)
	}
	return &endpoint, nil
}

func (c *client) DeletePrivateLinkEndpoint(ctx context.Context, request *regionalpb.DeletePrivateLinkEndpointRequest) error {
	var empty emptypb.Empty
	if err := c.callAPI(ctx, c.dataPlaneServiceCli, "DELETE",
		fmt.Sprintf("/api/v1/dataplanes/%s/privateLinkEndpoints/%s", request.DataPlaneId, request.Id),
		nil, nil, &empty,
	); err != nil {
		return errors.Trace(err)
	}
	return nil
}

func (c *client) callAPI(
	ctx context.Context,
	cli *resty.Client,
	method, uriPath string,
	queryParams map[string]string,
	body interface{}, dest proto.Message,
) error {
	req := cli.R().SetContext(ctx).SetBody(body)
	req.SetQueryParams(queryParams)
	resp, err := req.Execute(method, uriPath)

	logger := tilog.WithContextL(ctx).With(
		zap.String("method", method),
		zap.String("url", uriPath),
		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 &util.StatusError{
			StatusCode: resp.StatusCode(),
			Status:     resp.Status(),
			Body:       util.TruncateBody(resp.Body()),
		}
	}
	logger.Info("http request OK")
	if dest == nil {
		return nil
	}
	if err := c.unmarshalOpt.Unmarshal(resp.Body(), dest); err != nil {
		return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "unmarshal response")
	}
	return nil
}
