package o11ycli

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/pingcap/log"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/o11ycli/api"
)

const (
	vendorKey                           = "vendor"
	regionKey                           = "region"
	tenantKey                           = "tenantID"
	projectKey                          = "projectID"
	applicationKey                      = "applicationID"
	bizTypeKey                          = "bizType"
	ttlKey                              = "ttl"
	defaultTTLDayAfterO11yInfraDeletion = 1
)

type RegionalClient struct {
	httpClient      *http.Client
	serviceEndpoint string
}

var _ Interface = &RegionalClient{}

func NewRegionalClient(endpoint string) *RegionalClient {
	return &RegionalClient{
		serviceEndpoint: endpoint,
		httpClient: &http.Client{
			Transport: &http.Transport{
				// nolint:gosec
				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
			},
			Timeout: 10 * time.Second,
		},
	}
}

func (c *RegionalClient) CreateCluster(ctx context.Context, in *api.CreateRequest) (*api.ObservabilityCluster, error) {
	payload, err := json.Marshal(in)
	if err != nil {
		return nil, err
	}

	resp, err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/clusters/", c.serviceEndpoint), payload) //nolint:bodyclose
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}
	// 409 is duplicated creation. We treat duplicated creation as success.
	if (resp.StatusCode < 200 || resp.StatusCode > 299) && resp.StatusCode != 409 {
		return nil, fmt.Errorf("failed to create o11y infra, status code: %d, body: %s", resp.StatusCode, string(body))
	}
	var out api.ModifyResponse
	// Must get ObservabilityCluster response, or else fails
	if err := json.Unmarshal(body, &out); err != nil {
		return nil, fmt.Errorf("failed to unmarshal ModifyResponse: %v, body: %s", err, string(body))
	}
	if out.Data == nil {
		return nil, fmt.Errorf("failed to create o11y infra, response data is nil, body: %s", string(body))
	}
	return out.Data, nil
}

func (c *RegionalClient) ListClusters(ctx context.Context, req *api.ListRequest) ([]api.ClusterBaseInfo, error) {
	u, err := url.Parse(fmt.Sprintf("%s/api/v1/clusters/", c.serviceEndpoint))
	if err != nil {
		return nil, err
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, req.Vendor)
	q.Set(regionKey, req.Region)
	q.Set(tenantKey, truncateID(req.TenantID))
	q.Set(projectKey, truncateID(req.ProjectID))
	q.Set(applicationKey, truncateID(req.ClusterID))
	q.Set(bizTypeKey, req.BizType)
	u.RawQuery = q.Encode()
	res, err := c.doRequest(ctx, "GET", u.String(), nil) //nolint:bodyclose
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	if res.StatusCode < 200 || res.StatusCode > 299 {
		return nil, fmt.Errorf("failed to list o11y infra, status code: %d, body: %s", res.StatusCode, string(body))
	}
	var resp api.ListResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, fmt.Errorf("failed to unmarshal GetResponse: %w", err)
	}
	return resp.Data.Clusters, nil
}

func (c *RegionalClient) UpdateClusterAgent(ctx context.Context, in *api.UpdateAgentRequest) error {
	payload, err := json.Marshal(in)
	if err != nil {
		return err
	}

	resp, err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/clusters/%s/agent-observed", c.serviceEndpoint, in.ID), payload) //nolint:bodyclose
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read response body: %w", err)
	}
	// 409 is duplicated creation. We treat duplicated creation as success.
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return fmt.Errorf("failed to update cluster agent, status code: %d, body: %s", resp.StatusCode, string(body))
	}
	return nil
}

func (c *RegionalClient) GetCluster(ctx context.Context, provider, region, o11yInfraID string) (*api.ObservabilityCluster, error) {
	u, err := url.Parse(fmt.Sprintf("%s/api/v1/clusters/%s", c.serviceEndpoint, o11yInfraID))
	if err != nil {
		return nil, err
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, provider)
	q.Set(regionKey, region)
	u.RawQuery = q.Encode()
	resp, err := c.doRequest(ctx, "GET", u.String(), nil) //nolint:bodyclose
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return nil, fmt.Errorf("failed to delete o11y infra, status code: %d, body: %s", resp.StatusCode, string(body))
	}
	var out api.GetResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, fmt.Errorf("failed to unmarshal GetResponse: %w", err)
	}
	return out.Data, nil
}

func (c *RegionalClient) DeleteCluster(ctx context.Context, provider, region, o11yInfraID string) error {
	u, err := url.Parse(fmt.Sprintf("%s/api/v1/clusters/%s", c.serviceEndpoint, o11yInfraID))
	if err != nil {
		return err
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, provider)
	q.Set(regionKey, region)
	q.Set(ttlKey, strconv.Itoa(defaultTTLDayAfterO11yInfraDeletion))
	u.RawQuery = q.Encode()
	resp, err := c.doRequest(ctx, "DELETE", u.String(), nil) //nolint:bodyclose
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	// 404 is already deleted. We treat already deleted as success.
	if (resp.StatusCode < 200 || resp.StatusCode > 299) && resp.StatusCode != 404 {
		return fmt.Errorf("failed to delete o11y infra, status code: %d, body: %s", resp.StatusCode, string(body))
	}

	return nil
}

func (c *RegionalClient) PauseCluster(ctx context.Context, provider, region, o11yInfraID string) error {
	u, err := url.Parse(fmt.Sprintf("%s/api/v1/clusters/%s/pause", c.serviceEndpoint, o11yInfraID))
	if err != nil {
		return err
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, provider)
	q.Set(regionKey, region)
	u.RawQuery = q.Encode()
	resp, err := c.doRequest(ctx, "PUT", u.String(), nil) //nolint:bodyclose
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return fmt.Errorf("failed to delete o11y infra, status code: %d, body: %s", resp.StatusCode, string(body))
	}
	return nil
}

func (c *RegionalClient) ResumeCluster(ctx context.Context, provider, region, o11yInfraID string) error {
	u, err := url.Parse(fmt.Sprintf("%s/api/v1/clusters/%s/resume", c.serviceEndpoint, o11yInfraID))
	if err != nil {
		return err
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, provider)
	q.Set(regionKey, region)
	u.RawQuery = q.Encode()
	resp, err := c.doRequest(ctx, "PUT", u.String(), nil) //nolint:bodyclose
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return fmt.Errorf("failed to resume o11y infra, status code: %d, body: %s", resp.StatusCode, string(body))
	}
	return nil
}

func (c *RegionalClient) doRequest(ctx context.Context, method, url string, body []byte) (*http.Response, error) {
	var req *http.Request
	var err error
	if len(body) > 0 {
		if req, err = http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)); err != nil {
			return nil, err
		}
		req.Header.Add("content-type", "application/json")
	} else {
		if req, err = http.NewRequestWithContext(ctx, method, url, nil); err != nil {
			return nil, err
		}
	}

	token, err := GetStsToken(ctx)
	if err != nil {
		GetStsTokenCache().Flush()
		log.Info("failed to get sts token, skip set Authorization header")
	} else {
		req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to do request: %w", err)
	}

	return resp, err
}

func truncateID(id string) string {
	if len(id) <= 20 {
		return id
	}
	return id[:20]
}
