package regionalapi

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

	"github.com/tidbcloud/dataflow-service-ng/internal/config"
	"github.com/tidbcloud/dataflow-service-ng/pkg/grpc/grpcli"
	internal_http "github.com/tidbcloud/dataflow-service-ng/pkg/http"
	"github.com/tidbcloud/dataflow-service-ng/pkg/log"
	"github.com/tidbcloud/pkgv2/bizerr"
	global_commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

type Region struct {
	TenantID      uint64
	SharedPoolID  string
	Name          string
	CloudProvider string
}

type RegionInfo struct {
	Name        string
	Address     string
	LogicalName string
}

type RegionalClient interface {
	Get(
		ctx context.Context,
		region *Region, uriPath string, pathParams map[string]string, queryParams url.Values,
		dest interface{},
	) bizerr.BizError
	Post(
		ctx context.Context, region *Region, uriPath string,
		pathParams map[string]string, queryParams url.Values, body, dest interface{},
	) bizerr.BizError
	Put(
		ctx context.Context, region *Region, uriPath string, pathParams map[string]string,
		queryParams url.Values, body, dest interface{},
	) bizerr.BizError
	Delete(
		ctx context.Context, region *Region, uriPath string, pathParams map[string]string, queryParams url.Values,
	) bizerr.BizError
}

type regionalClientImpl struct {
	restyCli      *internal_http.RestyClient
	regionGrpcCli grpcli.RegionGrpcClient
}

func NewRegionalClient(regionGrpcCli grpcli.RegionGrpcClient) RegionalClient {
	restyCli := internal_http.NewRestyCli(
		internal_http.RestyWithProtoJson(),
	)
	return &regionalClientImpl{
		restyCli:      restyCli,
		regionGrpcCli: regionGrpcCli,
	}
}

func (m *regionalClientImpl) Get(
	ctx context.Context, region *Region, uriPath string,
	pathParams map[string]string, queryParams url.Values, dest interface{},
) bizerr.BizError {
	return m.callRegionalAPI(ctx, region, http.MethodGet, uriPath, pathParams, queryParams, nil, dest)
}

func (m *regionalClientImpl) Post(
	ctx context.Context, region *Region, uriPath string,
	pathParams map[string]string, queryParams url.Values, body, dest interface{},
) bizerr.BizError {
	return m.callRegionalAPI(ctx, region, http.MethodPost, uriPath, pathParams, queryParams, body, dest)
}

func (m *regionalClientImpl) Put(
	ctx context.Context, region *Region, uriPath string, pathParams map[string]string,
	queryParams url.Values, body, dest interface{},
) bizerr.BizError {
	return m.callRegionalAPI(ctx, region, http.MethodPut, uriPath, pathParams, queryParams, body, dest)
}

func (m *regionalClientImpl) Delete(
	ctx context.Context, region *Region, uriPath string, pathParams map[string]string, queryParams url.Values,
) bizerr.BizError {
	return m.callRegionalAPI(ctx, region, http.MethodDelete, uriPath, pathParams, queryParams, nil, nil)
}

func (m *regionalClientImpl) callRegionalAPI(
	ctx context.Context,
	region *Region,
	method, uriPath string,
	pathParams map[string]string,
	queryParams url.Values,
	body, dest interface{},
) bizerr.BizError {
	addr, err := m.getRegionAddr(ctx, region)
	if err != nil {
		return err.Chain("get region addr")
	}
	uri, uriErr := buildRegionalURI(addr, uriPath)
	if uriErr != nil {
		return bizerr.WrapErrorWithCode(uriErr, bizerr.ErrCodeUnknown, "parse regional endpoint")
	}

	resp, err := m.restyCli.Call(ctx, method, uri, pathParams, queryParams, body, ErrorResponse2BizErr)
	if err != nil {
		log.GetLogger(ctx).Error("call regional api error", zap.Error(err))
		return err
	}
	if dest == nil {
		return nil
	}
	if msg, ok := dest.(proto.Message); ok {
		opt := protojson.UnmarshalOptions{DiscardUnknown: true}
		if err := opt.Unmarshal(resp, msg); err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "unmarshal response")
		}
	} else {
		if err := json.Unmarshal(resp, dest); err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "unmarshal response")
		}
	}

	return nil
}

func (m *regionalClientImpl) getRegionAddr(
	ctx context.Context, region *Region,
) (string, bizerr.BizError) {
	// enable debug mode
	debugCfg := config.Global().DebugConfig
	if debugCfg.Enable && debugCfg.TiCDCServiceServer != "" {
		return debugCfg.TiCDCServiceServer, nil
	}
	var regionInfo *global_commonv1.Region
	var err bizerr.BizError

	if region.SharedPoolID != "" {
		poolID, _ := strconv.ParseUint(region.SharedPoolID, 10, 64)
		regionInfo, err = m.regionGrpcCli.GetRegionByResourcePoolID(ctx, poolID)
	} else {
		regionInfo, err = m.regionGrpcCli.GetRegionByTenantID(ctx, region.TenantID, region.CloudProvider, region.Name)
	}
	if err != nil {
		return "", err
	}
	if regionInfo.GetState() != global_commonv1.Region_ACTIVE {
		return "", bizerr.New(bizerr.ErrCodeUnknown, "region is not active")
	}
	return regionInfo.GetServiceAddress() + regionalApiPrefix, nil
}

func buildRegionalURI(regionalAddr, uriPath string) (string, error) {
	u, err := url.ParseRequestURI(regionalAddr)
	if err != nil {
		return "", err
	}
	regionalAddr = u.String()
	if strings.HasSuffix(regionalAddr, "/") {
		regionalAddr = strings.TrimSuffix(regionalAddr, "/")
	}
	if !strings.HasPrefix(uriPath, "/") {
		uriPath = "/" + uriPath
	}
	return regionalAddr + uriPath, nil
}
