package o11y

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/go-resty/resty/v2"
	gocache "github.com/patrickmn/go-cache"
	"github.com/pingcap/log"
	"go.uber.org/zap"
)

type UpdateCustomerAlertAuthRequest struct {
	Password string `json:"password"`
	Username string `json:"username"`
}

type UpdateCustomerAlertAuthResponse struct {
	Success bool   `json:"success"`
	Message string `json:"message"`
}

type Client struct {
	httpClient *resty.Client

	globalServiceEndpoint string // should include scheme

	auth0TokenCache   *gocache.Cache // 86400 seconds
	auth0ClientID     string
	auth0ClientSecret string
}

func New(
	globalServiceEndpoint,
	clientID,
	clientSecret string,
) (*Client, error) {
	if len(clientID) == 0 {
		return nil, fmt.Errorf("clientID is empty")
	}
	if len(clientSecret) == 0 {
		return nil, fmt.Errorf("clientSecret is empty")
	}

	httpCli := resty.New().
		SetTransport(http.DefaultTransport).
		SetTimeout(60 * time.Second).
		OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
			if !r.IsSuccess() {
				return fmt.Errorf("call o11y service failed, url: %s, method: %s, status: %s, body: %s",
					r.Request.URL, r.Request.Method, r.Status(), string(r.Body()))
			}
			return nil
		})
	c := &Client{
		httpClient:            httpCli,
		globalServiceEndpoint: globalServiceEndpoint,
		auth0TokenCache:       gocache.New(86400*time.Second, 86400*time.Second),
		auth0ClientID:         clientID,
		auth0ClientSecret:     clientSecret,
	}
	fmt.Printf("get client config, client_id: %s, client_secret: %s", clientID, clientSecret)

	return c, nil
}

func (cli *Client) UpdateCustomerAlertAuth(ctx context.Context, orgID string, req UpdateCustomerAlertAuthRequest) error {
	result := UpdateCustomerAlertAuthResponse{}

	accessToken, err := cli.getAccessToken(ctx)
	if err != nil {
		return fmt.Errorf("get access token failed: %v", err)
	}

	url := fmt.Sprintf("%s/api/v1/alert-auth", cli.globalServiceEndpoint)
	resp, err := cli.httpClient.R().
		SetHeader("X-Biz-Type", "byoc").
		SetHeader("X-Org-Id", orgID).
		SetAuthToken(accessToken).
		SetBody(req).
		SetResult(&result).
		Put(url)
	if err != nil {
		return fmt.Errorf("request failed: %v", err)
	}

	if !resp.IsSuccess() {
		return fmt.Errorf("request failed: %d, response: %s",
			resp.StatusCode(), resp.Body())
	}

	return nil
}

// curl -X POST --url 'https://tidb-soc2.us.auth0.com/oauth/token' \
// --header 'content-type: application/x-www-form-urlencoded' \
// --data grant_type=client_credentials \
// --data client_id=$client_id \
// --data client_secret=$client_secret \
// --data audience=https://tidb-soc2.us.auth0.com/api/v2/
// fetch new access token from auth0 service.
func (cli *Client) fetchAccessTokenFromAuth0(ctx context.Context) (string, error) {
	resp, err := cli.httpClient.R().
		SetHeader("Content-Type", "application/x-www-form-urlencoded").
		SetFormData(map[string]string{
			"grant_type":    auth0GrantType,
			"client_id":     cli.auth0ClientID,
			"client_secret": cli.auth0ClientSecret,
			"audience":      auth0Audience,
		}).
		SetResult(&FetchAuth0TokenData{}).
		Post(auth0URL)
	if err != nil {
		return "", fmt.Errorf("request failed: %v", err)
	}

	if !resp.IsSuccess() {
		return "", fmt.Errorf("request failed: %d, response: %s", resp.StatusCode(), resp.Body())
	}

	tokenResp, ok := resp.Result().(*FetchAuth0TokenData)
	if !ok {
		return "", fmt.Errorf("parse the response failed")
	}

	return tokenResp.AccessToken, nil
}

func (cli *Client) getAccessToken(ctx context.Context) (string, error) {
	if token, err := cli.getAccessTokenFromCache(ctx); err == nil && len(token) > 0 {
		return token, nil
	}
	// fetch new access token from auth0 service.
	token, err := cli.fetchAccessTokenFromAuth0(ctx)
	if err != nil {
		return "", err
	}
	// do not abort the process on error
	if err := cli.setAccessTokenToCache(ctx, token); err != nil {
		log.Warn("set auth0 access token failed", zap.Error(err))
	}
	return token, nil
}

func (cli *Client) getAccessTokenFromCache(_ context.Context) (string, error) {
	item, found := cli.auth0TokenCache.Get(auth0TokenCacheKey)
	if !found || item == nil {
		return "", nil
	}
	token, ok := item.(string)
	if !ok {
		return "", nil
	}
	return token, nil
}

func (cli *Client) setAccessTokenToCache(_ context.Context, accessToken string) error {
	cli.auth0TokenCache.Set(auth0TokenCacheKey, accessToken, 86300*time.Second)
	return nil
}
