package common

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

	"github.com/go-resty/resty/v2"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

const DefaultControlPlaneTimeout = 30 * time.Second

type ControlPlaneClient struct {
	httpCli *resty.Client
}

type ControlPlaneAPIError struct {
	StatusCode int
	Status     string
	Body       string
}

func (e *ControlPlaneAPIError) Error() string {
	return fmt.Sprintf("control plane request failed: status=%s body=%s", e.Status, e.Body)
}

func NewControlPlaneClient(baseURL string, timeout time.Duration) *ControlPlaneClient {
	if timeout <= 0 {
		timeout = DefaultControlPlaneTimeout
	}
	cli := resty.New().
		SetBaseURL(strings.TrimRight(baseURL, "/")).
		SetTimeout(timeout).
		SetJSONMarshaler(marshalProtoJSON).
		SetJSONUnmarshaler(unmarshalProtoJSON)
	return &ControlPlaneClient{httpCli: cli}
}

func (c *ControlPlaneClient) GetRuConfig(ctx context.Context, sharedPoolID string) (*apiv1.RuConfig, error) {
	resp := new(apiv1.RuConfig)
	if err := c.do(
		ctx,
		http.MethodGet,
		fmt.Sprintf("/api/v1beta/ruConfigs/%s", url.PathEscape(sharedPoolID)),
		map[string]string{"scope": apiv1.RuConfig_SHARED_POOL.String()},
		nil,
		nil,
		resp,
	); err != nil {
		return nil, err
	}
	return resp, nil
}

func (c *ControlPlaneClient) GetSharedPool(ctx context.Context, sharedPoolID string) (*apiv1.SharedPool, error) {
	resp := new(apiv1.SharedPool)
	if err := c.do(
		ctx,
		http.MethodGet,
		fmt.Sprintf("/api/v1beta/sharedPools/%s", url.PathEscape(sharedPoolID)),
		nil,
		nil,
		nil,
		resp,
	); err != nil {
		return nil, err
	}
	return resp, nil
}

func (c *ControlPlaneClient) CreateTiKVCapacityReservation(
	ctx context.Context,
	req *apiv1.CreateTiKVCapacityReservationRequest,
	trigger string,
) (*apiv1.TiKVCapacityReservation, error) {
	resp := new(apiv1.TiKVCapacityReservation)
	headers := map[string]string{}
	if trigger != "" {
		headers["Grpc-Metadata-X-Autoscale-Trigger"] = trigger
	}
	if err := c.do(
		ctx,
		http.MethodPost,
		fmt.Sprintf("/api/v1beta/sharedPools/%s/tikvCapacityReservations", url.PathEscape(req.GetSharedPoolId())),
		nil,
		headers,
		req,
		resp,
	); err != nil {
		return nil, err
	}
	return resp, nil
}

func (c *ControlPlaneClient) GetTiKVCapacityReservation(
	ctx context.Context,
	req *apiv1.GetTiKVCapacityReservationRequest,
) (*apiv1.TiKVCapacityReservation, error) {
	resp := new(apiv1.TiKVCapacityReservation)
	if err := c.do(
		ctx,
		http.MethodGet,
		fmt.Sprintf(
			"/api/v1beta/sharedPools/%s/tikvCapacityReservations/%s/%s",
			url.PathEscape(req.GetSharedPoolId()),
			url.PathEscape(req.GetBizType().String()),
			url.PathEscape(req.GetReservationId()),
		),
		nil,
		nil,
		nil,
		resp,
	); err != nil {
		return nil, err
	}
	return resp, nil
}

func (c *ControlPlaneClient) DeleteTiKVCapacityReservation(
	ctx context.Context,
	req *apiv1.DeleteTiKVCapacityReservationRequest,
) error {
	return c.do(
		ctx,
		http.MethodDelete,
		fmt.Sprintf(
			"/api/v1beta/sharedPools/%s/tikvCapacityReservations/%s/%s",
			url.PathEscape(req.GetSharedPoolId()),
			url.PathEscape(req.GetBizType().String()),
			url.PathEscape(req.GetReservationId()),
		),
		nil,
		nil,
		nil,
		nil,
	)
}

// ListSharedPools returns all shared pools in a single call (no pagination).
// If the API adds page tokens in the future, this must be updated to paginate.
func (c *ControlPlaneClient) ListSharedPools(ctx context.Context) (*apiv1.ListSharedPoolsResponse, error) {
	resp := new(apiv1.ListSharedPoolsResponse)
	if err := c.do(
		ctx,
		http.MethodGet,
		"/api/v1beta/sharedPools",
		nil,
		nil,
		nil,
		resp,
	); err != nil {
		return nil, err
	}
	return resp, nil
}

func (c *ControlPlaneClient) ScaleSharedPool(
	ctx context.Context,
	req *apiv1.ScaleSharedPoolRequest,
	trigger string,
) (*apiv1.SharedPool, error) {
	headers := map[string]string{}
	if trigger != "" {
		headers["Grpc-Metadata-X-Autoscale-Trigger"] = trigger
	}
	resp := new(apiv1.SharedPool)
	if err := c.do(
		ctx,
		http.MethodPost,
		fmt.Sprintf("/api/v1beta/sharedPools/%s:scale", url.PathEscape(req.GetSharedPoolId())),
		nil,
		headers,
		req,
		resp,
	); err != nil {
		return nil, err
	}
	return resp, nil
}

func (c *ControlPlaneClient) do(
	ctx context.Context,
	method string,
	path string,
	queryParams map[string]string,
	headers map[string]string,
	body proto.Message,
	dest proto.Message,
) error {
	req := c.httpCli.R().SetContext(ctx)
	if len(queryParams) > 0 {
		req.SetQueryParams(queryParams)
	}
	if len(headers) > 0 {
		req.SetHeaders(headers)
	}
	if body != nil {
		req.SetBody(body)
	}
	resp, err := req.Execute(method, path)
	if err != nil {
		return err
	}
	if !resp.IsSuccess() {
		return &ControlPlaneAPIError{
			StatusCode: resp.StatusCode(),
			Status:     resp.Status(),
			Body:       strings.TrimSpace(string(resp.Body())),
		}
	}
	if dest == nil {
		return nil
	}
	return protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(resp.Body(), dest)
}

func marshalProtoJSON(v any) ([]byte, error) {
	if msg, ok := v.(proto.Message); ok {
		return protojson.Marshal(msg)
	}
	return json.Marshal(v)
}

func unmarshalProtoJSON(data []byte, v any) error {
	if msg, ok := v.(proto.Message); ok {
		return protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(data, msg)
	}
	return json.Unmarshal(data, v)
}
