package byocfixture

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

const (
	defaultPollInterval = 30 * time.Second
	defaultPollTimeout  = time.Hour
)

type Client struct {
	baseURL      string
	httpClient   *http.Client
	pollInterval time.Duration
	pollTimeout  time.Duration
}

type CreateOptions struct {
	OrganizationID string
	CloudProvider  string
	Region         string
	DisplayName    string
}

type int64Value int64

func (v *int64Value) UnmarshalJSON(data []byte) error {
	value := strings.Trim(string(data), `"`)
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil {
		return fmt.Errorf("parse integer %q: %w", value, err)
	}
	*v = int64Value(parsed)
	return nil
}

type nodeSpec struct {
	CPU       int64Value `json:"cpu"`
	MemoryGi  int64Value `json:"memoryGi"`
	IsDefault bool       `json:"isDefault"`
	Storages  []storage  `json:"storages"`
}

type nodeCountOptions struct {
	DefaultNodeCount int64Value `json:"defaultNodeCount"`
}

type nodeSpecResponse struct {
	NodeSpecs        []nodeSpec       `json:"nodeSpecs"`
	NodeCountOptions nodeCountOptions `json:"nodeCountOptions"`
}

type storage struct {
	Name          string     `json:"name"`
	DefaultSizeGi int64Value `json:"defaultSizeGi"`
}

type cidrRecommendation struct {
	CIDR string `json:"cidr"`
}

type cidrPlanResponse struct {
	RecommendationStatus string             `json:"recommendationStatus"`
	Recommendation       cidrRecommendation `json:"recommendation"`
}

type resourcePoolResponse struct {
	ResourcePoolID string `json:"resourcePoolId"`
	State          string `json:"state"`
}

type tidbSummary struct {
	TidbID string `json:"tidbId"`
}

type listTidbsResponse struct {
	Tidbs         []tidbSummary `json:"tidbs"`
	NextPageToken string        `json:"nextPageToken"`
}

type apiError struct {
	statusCode int
	body       string
}

func (e *apiError) Error() string {
	return fmt.Sprintf("API returned status %d: %s", e.statusCode, e.body)
}

func NewClient(baseURL string, httpClient *http.Client) (*Client, error) {
	if httpClient == nil {
		return nil, errors.New("HTTP client is required")
	}
	baseURL = strings.TrimSuffix(baseURL, "/")
	parsed, err := url.Parse(baseURL)
	if err != nil {
		return nil, fmt.Errorf("parse base URL: %w", err)
	}
	if parsed.Scheme != "https" && parsed.Scheme != "http" {
		return nil, errors.New("base URL must use HTTP or HTTPS")
	}
	if parsed.Host == "" {
		return nil, errors.New("base URL host is required")
	}
	return &Client{
		baseURL:      baseURL,
		httpClient:   httpClient,
		pollInterval: defaultPollInterval,
		pollTimeout:  defaultPollTimeout,
	}, nil
}

func (c *Client) SetPolling(interval, timeout time.Duration) {
	c.pollInterval = interval
	c.pollTimeout = timeout
}

func (c *Client) Create(ctx context.Context, options CreateOptions) (poolID string, err error) {
	if err := validateCreateOptions(options); err != nil {
		return "", err
	}

	nodeSpec, err := c.getNodeSpec(ctx, options)
	if err != nil {
		return "", err
	}
	cidr, err := c.planCIDR(ctx, options)
	if err != nil {
		return "", err
	}

	requestBody := map[string]any{
		"displayName": options.DisplayName,
		"regionId":    options.CloudProvider + "-" + options.Region,
		"vpcCidr":     cidr,
		"vcpuLimit": map[string]any{
			"unlimited": true,
		},
		"capacity": map[string]any{
			"tikv": map[string]any{
				"nodeSpec": map[string]any{
					"cpu":      int64(nodeSpec.CPU),
					"memoryGi": int64(nodeSpec.MemoryGi),
				},
				"nodeCount": int64(nodeSpec.NodeCount),
				"storages": []map[string]any{{
					"name":   "data",
					"sizeGi": int64(nodeSpec.StorageSizeGi),
				}},
			},
		},
	}

	var created resourcePoolResponse
	if err := c.doJSON(ctx, http.MethodPost, "/resourcePools", nil, requestBody, &created); err != nil {
		return "", fmt.Errorf("create resource pool: %w", err)
	}
	if err := validateResourcePoolID(created.ResourcePoolID); err != nil {
		return "", fmt.Errorf("create resource pool response: %w", err)
	}
	poolID = created.ResourcePoolID
	createdPoolID := poolID
	defer func() {
		if err != nil {
			cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), c.pollTimeout)
			defer cancel()
			_ = c.Cleanup(cleanupCtx, createdPoolID)
		}
	}()

	if err := c.waitForPoolState(ctx, poolID, "ACTIVE"); err != nil {
		return "", err
	}
	return poolID, nil
}

func (c *Client) Cleanup(ctx context.Context, poolID string) error {
	if err := validateResourcePoolID(poolID); err != nil {
		return err
	}

	clusterIDs, err := c.listAllTidbIDs(ctx, poolID)
	if err != nil {
		return fmt.Errorf("list TiDB instances in resource pool: %w", err)
	}
	for _, clusterID := range clusterIDs {
		path := "/tidbs/" + url.PathEscape(clusterID)
		if err := c.doJSON(ctx, http.MethodDelete, path, nil, nil, nil); err != nil {
			var responseErr *apiError
			if !errors.As(err, &responseErr) || responseErr.statusCode != http.StatusNotFound {
				return fmt.Errorf("delete TiDB %s: %w", clusterID, err)
			}
		}
	}
	if err := c.waitForNoTidbs(ctx, poolID); err != nil {
		return err
	}

	deadline := time.Now().Add(c.pollTimeout)
	for {
		err := c.doJSON(ctx, http.MethodDelete, "/resourcePools/"+url.PathEscape(poolID), nil, nil, nil)
		if err == nil {
			break
		}
		var responseErr *apiError
		if errors.As(err, &responseErr) && responseErr.statusCode == http.StatusNotFound {
			return nil
		}
		if time.Now().After(deadline) {
			return fmt.Errorf("delete resource pool %s: %w", poolID, err)
		}
		if err := wait(ctx, c.pollInterval); err != nil {
			return err
		}
	}

	return c.waitForPoolAbsence(ctx, poolID)
}

type selectedNodeSpec struct {
	CPU           int64Value
	MemoryGi      int64Value
	NodeCount     int64Value
	StorageSizeGi int64Value
}

func (c *Client) getNodeSpec(ctx context.Context, options CreateOptions) (selectedNodeSpec, error) {
	query := url.Values{}
	query.Set("organizationId", options.OrganizationID)
	query.Set("componentType", "NODE_SPEC_COMPONENT_TYPE_TIKV")
	query.Set("pageSize", "100")
	path := "/regions/" + url.PathEscape(options.CloudProvider+"-"+options.Region) + "/nodeSpecs"
	var response nodeSpecResponse
	if err := c.doJSON(ctx, http.MethodGet, path, query, nil, &response); err != nil {
		return selectedNodeSpec{}, fmt.Errorf("list TiKV node specs: %w", err)
	}
	if len(response.NodeSpecs) == 0 {
		return selectedNodeSpec{}, errors.New("no TiKV node specification is available")
	}
	selectedSpec := &response.NodeSpecs[0]
	for i := range response.NodeSpecs {
		if response.NodeSpecs[i].IsDefault {
			selectedSpec = &response.NodeSpecs[i]
			break
		}
	}
	var dataStorage *storage
	for i := range selectedSpec.Storages {
		if selectedSpec.Storages[i].Name == "data" {
			dataStorage = &selectedSpec.Storages[i]
			break
		}
	}
	if dataStorage == nil {
		return selectedNodeSpec{}, errors.New("TiKV node specification has no data storage")
	}
	selected := selectedNodeSpec{
		CPU:           selectedSpec.CPU,
		MemoryGi:      selectedSpec.MemoryGi,
		NodeCount:     response.NodeCountOptions.DefaultNodeCount,
		StorageSizeGi: dataStorage.DefaultSizeGi,
	}
	if selected.CPU <= 0 || selected.MemoryGi <= 0 || selected.NodeCount <= 0 || selected.StorageSizeGi <= 0 {
		return selectedNodeSpec{}, errors.New("TiKV node specification contains non-positive values")
	}
	return selected, nil
}

func (c *Client) planCIDR(ctx context.Context, options CreateOptions) (string, error) {
	path := "/regions/" + url.PathEscape(options.CloudProvider+"-"+options.Region) + ":planResourcePoolCidr"
	var response cidrPlanResponse
	if err := c.doJSON(ctx, http.MethodGet, path, nil, nil, &response); err != nil {
		return "", fmt.Errorf("plan resource-pool CIDR: %w", err)
	}
	if response.RecommendationStatus != "AVAILABLE" || response.Recommendation.CIDR == "" {
		return "", fmt.Errorf("resource-pool CIDR recommendation is unavailable: %s", response.RecommendationStatus)
	}
	return response.Recommendation.CIDR, nil
}

func (c *Client) waitForPoolState(ctx context.Context, poolID, expectedState string) error {
	deadline := time.Now().Add(c.pollTimeout)
	for {
		var response resourcePoolResponse
		err := c.doJSON(ctx, http.MethodGet, "/resourcePools/"+url.PathEscape(poolID), nil, nil, &response)
		if err == nil && response.State == expectedState {
			return nil
		}
		if err == nil && response.State == "FAILED" {
			return fmt.Errorf("resource pool %s entered FAILED state", poolID)
		}
		if time.Now().After(deadline) {
			if err != nil {
				return fmt.Errorf("wait for resource pool %s state %s: %w", poolID, expectedState, err)
			}
			return fmt.Errorf("resource pool %s did not reach %s", poolID, expectedState)
		}
		if err := wait(ctx, c.pollInterval); err != nil {
			return err
		}
	}
}

func (c *Client) waitForNoTidbs(ctx context.Context, poolID string) error {
	deadline := time.Now().Add(c.pollTimeout)
	for {
		clusterIDs, err := c.listAllTidbIDs(ctx, poolID)
		if err == nil && len(clusterIDs) == 0 {
			return nil
		}
		if time.Now().After(deadline) {
			if err != nil {
				return fmt.Errorf("wait for TiDB cleanup in resource pool %s: %w", poolID, err)
			}
			return fmt.Errorf("resource pool %s still has TiDB instances: %v", poolID, clusterIDs)
		}
		if err := wait(ctx, c.pollInterval); err != nil {
			return err
		}
	}
}

func (c *Client) waitForPoolAbsence(ctx context.Context, poolID string) error {
	deadline := time.Now().Add(c.pollTimeout)
	for {
		err := c.doJSON(ctx, http.MethodGet, "/resourcePools/"+url.PathEscape(poolID), nil, nil, nil)
		var responseErr *apiError
		if errors.As(err, &responseErr) && responseErr.statusCode == http.StatusNotFound {
			return nil
		}
		if time.Now().After(deadline) {
			if err != nil {
				return fmt.Errorf("wait for resource pool %s deletion: %w", poolID, err)
			}
			return fmt.Errorf("resource pool %s still exists", poolID)
		}
		if err := wait(ctx, c.pollInterval); err != nil {
			return err
		}
	}
}

func (c *Client) listAllTidbIDs(ctx context.Context, poolID string) ([]string, error) {
	var result []string
	pageToken := ""
	for {
		query := url.Values{}
		query.Set("resourcePoolIds", poolID)
		query.Set("pageSize", "100")
		if pageToken != "" {
			query.Set("pageToken", pageToken)
		}
		var response listTidbsResponse
		if err := c.doJSON(ctx, http.MethodGet, "/tidbs", query, nil, &response); err != nil {
			return nil, err
		}
		for _, tidb := range response.Tidbs {
			if tidb.TidbID != "" {
				result = append(result, tidb.TidbID)
			}
		}
		if response.NextPageToken == "" {
			return result, nil
		}
		pageToken = response.NextPageToken
	}
}

func (c *Client) doJSON(
	ctx context.Context,
	method, path string,
	query url.Values,
	requestBody any,
	responseBody any,
) error {
	var body io.Reader
	if requestBody != nil {
		encoded, err := json.Marshal(requestBody)
		if err != nil {
			return fmt.Errorf("encode request: %w", err)
		}
		body = bytes.NewReader(encoded)
	}
	requestURL := c.baseURL + path
	if len(query) > 0 {
		requestURL += "?" + query.Encode()
	}
	request, err := http.NewRequestWithContext(ctx, method, requestURL, body)
	if err != nil {
		return fmt.Errorf("build request: %w", err)
	}
	request.Header.Set("Accept", "application/json")
	if requestBody != nil {
		request.Header.Set("Content-Type", "application/json")
	}
	response, err := c.httpClient.Do(request)
	if err != nil {
		return fmt.Errorf("send request: %w", err)
	}
	defer response.Body.Close()
	data, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
	if err != nil {
		return fmt.Errorf("read response: %w", err)
	}
	if response.StatusCode != http.StatusOK {
		return &apiError{statusCode: response.StatusCode, body: strings.TrimSpace(string(data))}
	}
	if responseBody != nil && len(data) > 0 {
		if err := json.Unmarshal(data, responseBody); err != nil {
			return fmt.Errorf("decode response: %w", err)
		}
	}
	return nil
}

func validateCreateOptions(options CreateOptions) error {
	if options.OrganizationID == "" || options.CloudProvider == "" || options.Region == "" || options.DisplayName == "" {
		return errors.New("organization ID, cloud provider, region, and display name are required")
	}
	if _, err := strconv.ParseUint(options.OrganizationID, 10, 64); err != nil {
		return fmt.Errorf("invalid organization ID: %w", err)
	}
	if !isSafeName(options.CloudProvider) || !isSafeName(options.Region) || !isSafeName(options.DisplayName) {
		return errors.New("cloud provider, region, and display name may contain only letters, digits, and hyphens")
	}
	return nil
}

func validateResourcePoolID(poolID string) error {
	id, err := strconv.ParseUint(poolID, 10, 64)
	if err != nil || id == 0 {
		return fmt.Errorf("invalid resource pool ID %q", poolID)
	}
	return nil
}

func isSafeName(value string) bool {
	for _, character := range value {
		if (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') ||
			(character >= '0' && character <= '9') || character == '-' {
			continue
		}
		return false
	}
	return value != ""
}

func wait(ctx context.Context, duration time.Duration) error {
	timer := time.NewTimer(duration)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}
