package api

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"sync"

	"github.com/pingcap/errors"
)

const (
	grantType = "client_credentials"
	// bizType just to serve as default value for the required parameter, no specific meaning here.
	bizTypeValue = "tidbcloud"

	// Request parameter keys
	vendorKey      = "vendor"
	regionKey      = "region"
	tenantKey      = "tenantID"
	projectKey     = "projectID"
	applicationKey = "applicationID"
	bizTypeKey     = "bizType"
	ttlKey         = "ttl"

	// FIXME(@xuyifan02): specify monitorSpecID in the future.
	monitorSpecIDValue = "spec1"

	// FIXME(@xuyifan02): refactor this snippet after `bizType` parameter takes effect in the future.
	infraClusterID   = "k8s-infra"
	infraAlertTempID = "d5025c09-d861-417c-9cfe-797bd97ce0c3"
	tidbAlertTempID  = "95072d4e-5676-4084-bdf1-09aca85881f2"
	dmApplicationID  = "datamigration"
	dmAlertTempID    = "data-migration-data-plane-rule"

	pausePath  = "pause"
	resumePath = "resume"

	defaultTTLDayAfterO11yInfraDeletion = 1
)

// clientManager is a singleton class that manages the o11y client api token.
// Safe to use concurrently.
type clientManager struct {
	*http.Client

	accessToken string
	// Read-write mutex for current token access and refresh.
	mu sync.RWMutex

	auth0RequestBody Auth0RequestBody
	auth0GetTokenURL string

	o11yOpenAPIAddress  string
	createO11yInfraPath string
	deleteO11yInfraPath string
	getO11yInfraPath    string
	pauseO11yInfraPath  string
	resumeO11yInfraPath string
}

func buildUrl(address string, paths ...string) (*url.URL, error) {
	urlObj, err := url.Parse(address)
	if err != nil {
		return nil, errors.Annotate(err, "failed to parse address")
	}
	fullPath := append([]string{urlObj.Path}, paths...)
	urlObj.Path = path.Join(fullPath...)
	return urlObj, nil
}

// refreshAccessToken refreshes the access token from auth0 service.
// It takes the write mutex before refreshing the token.
func (m *clientManager) refreshAccessToken(ctx context.Context) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	payload, err := json.Marshal(m.auth0RequestBody)
	if err != nil {
		return errors.Annotate(err, "failed to marshal auth0 request body")
	}
	req, err := http.NewRequestWithContext(ctx, "POST", m.auth0GetTokenURL, bytes.NewReader(payload))
	if err != nil {
		return errors.Annotate(err, "failed to create request")
	}
	req.Header.Add("content-type", "application/json")
	res, err := m.Do(req)
	if err != nil {
		return errors.Annotate(err, "failed to do request")
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return errors.Annotate(err, "failed to read response body")
	}
	if res.StatusCode != 200 {
		return errors.Errorf("failed to get access token from auth0, status code: %d, body: %s", res.StatusCode, string(body))
	}
	var auth0Response Auth0Response
	if err := json.Unmarshal(body, &auth0Response); err != nil {
		return errors.Annotate(err, "failed to unmarshal auth0 response")
	}
	m.accessToken = auth0Response.AccessToken
	return nil
}

// getTokenThenDoRequest gets the access token from manager and do the request.
// It takes the read mutex before getting token, and release the read mutex after the request is done.
// If there is a refreshAccessToken call undergoing, it will block until the refresh is done.
func (m *clientManager) getTokenThenDoRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	// Ensure bearer token to request.
	req.Header.Add("authorization", fmt.Sprintf("Bearer %s", m.accessToken))
	return m.Do(req.WithContext(ctx))
}

// doRequest sends the request and returns the response. It encapsulates the access token refresh logic.
// Param "body" is necessary, since the request body cannot be read twice. One must set the body each time before do request.
func (m *clientManager) doRequest(ctx context.Context, method, url string, body []byte) (*http.Response, error) {
	var req1 *http.Request
	if len(body) > 0 {
		req1, _ = http.NewRequest(method, url, bytes.NewReader(body))
		req1.Header.Add("content-type", "application/json")
	} else {
		req1, _ = http.NewRequest(method, url, nil)
	}
	resp1, err := m.getTokenThenDoRequest(ctx, req1)
	if err != nil {
		return nil, errors.Annotate(err, "failed to do request")
	}
	// 401 unauthorized, 402, or 403 forbidden could mean the access token is expired, we need to refresh it.
	if 401 <= resp1.StatusCode && resp1.StatusCode <= 403 {
		defer resp1.Body.Close()
		if err := m.refreshAccessToken(ctx); err != nil {
			return nil, errors.Annotate(err, "failed to refresh access token")
		}
		var req2 *http.Request
		if len(body) > 0 {
			body2 := make([]byte, 0, len(body))
			copy(body2, body)
			req2, _ = http.NewRequest(method, url, bytes.NewReader(body2))
			req2.Header.Add("content-type", "application/json")
		} else {
			req2, _ = http.NewRequest(method, url, nil)
		}
		resp2, err := m.getTokenThenDoRequest(ctx, req2)
		if err != nil {
			return nil, errors.Annotate(err, "failed to do request")
		}
		return resp2, nil
	}
	return resp1, err
}

// createAWSO11yInfra creates o11y infra for the cluster in autonomous vpc.
func (m *clientManager) createAWSO11yInfra(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType, alertTempID, monitorSpecID string, mode ShareMode, muteAlert bool, agent AgentEnvironment) (*ObservabilityCluster, error) {
	u, err := buildUrl(m.o11yOpenAPIAddress, m.createO11yInfraPath)
	if err != nil {
		return nil, errors.Annotate(err, "failed to build url")
	}
	reqBody := CreateRequest{}
	reqBody.ID = ""
	reqBody.Vendor = provider
	reqBody.Region = region
	reqBody.TenantID = truncateID(tenantID)
	reqBody.ProjectID = truncateID(projectID)
	reqBody.ApplicationID = truncateID(clusterID)
	reqBody.BizType = bizType
	reqBody.Name = clusterID
	reqBody.ShareMode = mode
	reqBody.MonitorSpecID = monitorSpecID
	if monitorSpecID == "" {
		reqBody.MonitorSpecID = monitorSpecIDValue
	}
	reqBody.RuleTemplateID = alertTempID
	reqBody.MuteAlert = muteAlert
	reqBody.AgentEnvironment = agent

	payload, err := json.Marshal(reqBody)
	if err != nil {
		return nil, errors.Annotate(err, "failed to marshal create o11y infra request body")
	}
	res, err := m.doRequest(ctx, "POST", u.String(), payload)
	if err != nil {
		return nil, errors.Annotate(err, "failed to do request")
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, errors.Annotate(err, "failed to read response body")
	}
	// 409 is duplicated creation. We treat duplicated creation as success.
	if (res.StatusCode < 200 || res.StatusCode > 299) && res.StatusCode != 409 {
		return nil, errors.Errorf("failed to create o11y infra, status code: %d, body: %s", res.StatusCode, string(body))
	}
	var resp ModifyResponse
	// Must get ObservabilityCluster response, or else fails
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, errors.Annotatef(err, "failed to unmarshal ModifyResponse, body: %s", string(body))
	}
	if resp.Data == nil {
		return nil, errors.Errorf("failed to create o11y infra, response data is nil, body: %s", string(body))
	}
	return resp.Data, nil
}

func (m *clientManager) deleteO11yInfra(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error) {
	u, err := buildUrl(m.o11yOpenAPIAddress, m.deleteO11yInfraPath, o11yInfraID)
	if err != nil {
		return nil, errors.Annotate(err, "failed to build url")
	}
	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()
	res, err := m.doRequest(ctx, "DELETE", u.String(), nil)
	if err != nil {
		return nil, errors.Annotate(err, "failed to do request")
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, errors.Annotate(err, "failed to read response body")
	}
	// 404 is already deleted. We treat already deleted as success.
	if (res.StatusCode < 200 || res.StatusCode > 299) && res.StatusCode != 404 {
		return nil, errors.Errorf("failed to delete o11y infra, status code: %d, body: %s", res.StatusCode, string(body))
	}
	var resp ModifyResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, errors.Annotate(err, "failed to unmarshal ModifyResponse")
	}
	return resp.Data, nil
}

func (m *clientManager) listO11yInfra(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType string) ([]ClusterBaseInfo, error) {
	u, err := buildUrl(m.o11yOpenAPIAddress, m.getO11yInfraPath)
	if err != nil {
		return nil, errors.Annotate(err, "failed to build url")
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, provider)
	q.Set(regionKey, region)
	q.Set(tenantKey, truncateID(tenantID))
	q.Set(projectKey, truncateID(projectID))
	q.Set(applicationKey, truncateID(clusterID))
	q.Set(bizTypeKey, bizType)
	u.RawQuery = q.Encode()
	res, err := m.doRequest(ctx, "GET", u.String(), nil)
	if err != nil {
		return nil, errors.Annotate(err, "failed to do request")
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, errors.Annotate(err, "failed to read response body")
	}
	if res.StatusCode < 200 || res.StatusCode > 299 {
		return nil, errors.Errorf("failed to list o11y infra, status code: %d, body: %s", res.StatusCode, string(body))
	}
	var resp ListResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, errors.Annotate(err, "failed to unmarshal GetResponse")
	}
	return resp.Data.Clusters, nil
}

func (m *clientManager) getO11yInfra(ctx context.Context, provider, region, applicationID string) (*ObservabilityCluster, error) {
	u, err := buildUrl(m.o11yOpenAPIAddress, m.getO11yInfraPath, applicationID)
	if err != nil {
		return nil, errors.Annotate(err, "failed to build url")
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, provider)
	q.Set(regionKey, region)
	u.RawQuery = q.Encode()
	res, err := m.doRequest(ctx, "GET", u.String(), nil)
	if err != nil {
		return nil, errors.Annotate(err, "failed to do request")
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, errors.Annotate(err, "failed to read response body")
	}
	if res.StatusCode < 200 || res.StatusCode > 299 {
		return nil, errors.Errorf("failed to get o11y infra, status code: %d, body: %s", res.StatusCode, string(body))
	}
	var resp GetResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, errors.Annotate(err, "failed to unmarshal GetResponse")
	}
	return resp.Data, nil
}

func (m *clientManager) pauseO11yInfra(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error) {
	u, err := buildUrl(m.o11yOpenAPIAddress, m.pauseO11yInfraPath, o11yInfraID, pausePath)
	if err != nil {
		return nil, errors.Annotate(err, "failed to build url")
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, provider)
	q.Set(regionKey, region)
	u.RawQuery = q.Encode()
	res, err := m.doRequest(ctx, "PUT", u.String(), nil)
	if err != nil {
		return nil, errors.Annotate(err, "failed to do request")
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, errors.Annotate(err, "failed to read response body")
	}
	if res.StatusCode < 200 || res.StatusCode > 299 {
		return nil, errors.Errorf("failed to pause o11y infra, status code: %d, body: %s", res.StatusCode, string(body))
	}
	var resp ResumeResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, errors.Annotate(err, "failed to unmarshal GetResponse")
	}
	return resp.Data, nil
}

func (m *clientManager) resumeO11yInfra(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error) {
	u, err := buildUrl(m.o11yOpenAPIAddress, m.resumeO11yInfraPath, o11yInfraID, resumePath)
	if err != nil {
		return nil, errors.Annotate(err, "failed to build url")
	}
	q := u.Query()
	// Set values for the required parameters.
	q.Set(vendorKey, provider)
	q.Set(regionKey, region)
	u.RawQuery = q.Encode()
	res, err := m.doRequest(ctx, "PUT", u.String(), nil)
	if err != nil {
		return nil, errors.Annotate(err, "failed to do request")
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, errors.Annotate(err, "failed to read response body")
	}
	if res.StatusCode < 200 || res.StatusCode > 299 {
		return nil, errors.Errorf("failed to resume o11y infra, status code: %d, body: %s", res.StatusCode, string(body))
	}
	var resp ResumeResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, errors.Annotate(err, "failed to unmarshal GetResponse")
	}
	return resp.Data, nil
}

// truncateID truncates the given ID to 20 characters.
func truncateID(id string) string {
	if len(id) <= 20 {
		return id
	}
	return id[:20]
}
