package vercel

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/go-resty/resty/v2"
	"github.com/google/go-querystring/query"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"
)

var Tracer = otel.Tracer("vercel-client")

type APIClient interface {
	ExchangeAccessToken(ctx context.Context, code string, redirectUri string, clientID string, clientSecret string) (*IntegrationAccessTokenVercelAPIResponse, error)
	RetrieveProjects(ctx context.Context, params RetrieveProjectQueryParameters, accessToken string) (*RetrieveProjectResponse, error)
	RetrieveIntegrationConfiguration(ctx context.Context, ID string, teamID string, accessToken string) (*RetrieveIntegrationConfigurationResponse, error)
	CreateEnvironmentVariables(ctx context.Context, projectID string, teamID string, descriptions []EnvironmentDescription, accessToken string) ([]EnvironmentInfo, error)
	DeleteEnvironmentVariables(ctx context.Context, projectID string, teamID string, envsToDelete []string, accessToken string) error
	GetProject(ctx context.Context, projectID, teamID, accessToken string) (*GetProjectResponse, error)
	CreateCheck(ctx context.Context, deploymentID string, teamID string, accessToken string) (*CreateCheckResponse, error)
	UpdateCheck(ctx context.Context, deploymentID string, checkID string, body UpdateCheckBody, teamID string, accessToken string) error
	UpdateEnvironmentVariables(ctx context.Context, projectID string, teamID string, envs map[string]EnvironmentDescription, accessToken string) error
	GetDeployment(ctx context.Context, deploymentID string, teamID string, accessToken string) (*GetDeploymentResponse, error)
}

type apiClientImpl struct {
}

func NewAPIClient() APIClient {
	return &apiClientImpl{}
}

func (*apiClientImpl) ExchangeAccessToken(ctx context.Context, code string, redirectUri string, clientID string, clientSecret string) (_ *IntegrationAccessTokenVercelAPIResponse, err error) {
	ctx, span := Tracer.Start(ctx, "apiClientImpl.ExchangeAccessToken")
	defer func() {
		tracing.EndSpan(span, err)
	}()
	tilog.Debug(ctx, "ExchangeAccessToken", zap.String("code", code), zap.String("redirectUri", redirectUri), zap.String("clientID", clientID), zap.String("clientSecret", clientSecret))

	var vercelAPIResult IntegrationAccessTokenVercelAPIResponse
	client := resty.New()
	response, err := client.
		R().
		SetHeader("Content-Type", "application/x-www-form-urlencoded").
		SetFormData(map[string]string{
			"code":          code,
			"redirect_uri":  redirectUri,
			"client_id":     clientID,
			"client_secret": clientSecret,
		}).
		SetResult(&vercelAPIResult).
		Post("https://api.vercel.com/v2/oauth/access_token")
	if err != nil {
		return nil, errors.Trace(err)
	}

	if response.IsSuccess() {
		return &vercelAPIResult, nil
	} else {
		return nil, errors.Errorf("Failed to get access token from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
	}
}

func (*apiClientImpl) RetrieveProjects(ctx context.Context, params RetrieveProjectQueryParameters, accessToken string) (_ *RetrieveProjectResponse, err error) {
	ctx, span := Tracer.Start(ctx, "apiClientImpl.RetrieveProjects")
	defer func() {
		tracing.EndSpan(span, err)
	}()
	tilog.Debug(ctx, "RetrieveProjects", zap.Any("params", params), zap.String("accessToken", accessToken))

	var result RetrieveProjectResponse
	client := resty.New()
	v, _ := query.Values(params)
	response, err := client.
		R().SetQueryString(v.Encode()).
		SetAuthToken(accessToken).
		SetResult(&result).
		Get("https://api.vercel.com/v9/projects")
	if err != nil {
		return nil, errors.Trace(err)
	}

	if response.IsSuccess() {
		return &result, nil
	} else {
		return nil, errors.Errorf("Failed to retrieve projects from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
	}
}

func (*apiClientImpl) GetProject(ctx context.Context, projectID, teamID, accessToken string) (_ *GetProjectResponse, err error) {
	ctx, span := Tracer.Start(ctx, "apiClientImpl.GetProject")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	tilog.Debug(ctx, "GetProject", zap.String("projectID", projectID), zap.String("teamID", teamID), zap.String("accessToken", accessToken))
	params := TeamIDQueryParameter{
		TeamID: teamID,
	}

	var result GetProjectResponse
	client := resty.New()
	v, _ := query.Values(params)
	response, err := client.
		R().SetQueryString(v.Encode()).
		SetAuthToken(accessToken).
		SetResult(&result).
		Get(fmt.Sprintf("https://api.vercel.com/v9/projects/%s", projectID))
	if err != nil {
		return nil, errors.Trace(err)
	}

	if response.IsSuccess() {
		return &result, nil
	} else {
		return nil, errors.Errorf("Failed to get project from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
	}
}

func (*apiClientImpl) RetrieveIntegrationConfiguration(ctx context.Context, ID string, teamID string, accessToken string) (_ *RetrieveIntegrationConfigurationResponse, err error) {
	_, span := Tracer.Start(ctx, "apiClientImpl.RetrieveIntegrationConfiguration")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	params := TeamIDQueryParameter{
		TeamID: teamID,
	}

	var result RetrieveIntegrationConfigurationResponse
	client := resty.New()
	v, _ := query.Values(params)
	response, err := client.
		R().SetQueryString(v.Encode()).
		SetAuthToken(accessToken).
		SetResult(&result).
		Get("https://api.vercel.com/v1/integrations/configuration/" + ID)
	if err != nil {
		return nil, errors.Trace(err)
	}

	if response.IsSuccess() {
		return &result, nil
	} else {
		return nil, errors.Errorf("Failed to retrieve integration configuration from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
	}
}

func (*apiClientImpl) CreateEnvironmentVariables(ctx context.Context, projectID string, teamID string, descriptions []EnvironmentDescription, accessToken string) (_ []EnvironmentInfo, err error) {
	_, span := Tracer.Start(ctx, "apiClientImpl.CreateEnvironmentVariables")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	params := TeamIDQueryParameter{
		TeamID: teamID,
	}
	arrayJson, err := json.Marshal(descriptions)
	if err != nil {
		return nil, err
	}

	client := resty.New()
	v, _ := query.Values(params)
	var result []EnvironmentInfo
	response, err := client.
		R().SetQueryString(v.Encode()).
		SetAuthToken(accessToken).
		SetBody(arrayJson).
		SetResult(&result).
		SetHeader("Content-Type", "application/json").
		Post("https://api.vercel.com/v9/projects/" + projectID + "/env")

	if err != nil {
		return nil, err
	}

	if response.IsSuccess() {
		return result, nil
	} else if response.StatusCode() >= 400 && response.StatusCode() < 500 {
		return nil, bizerr.Restore(ErrorCodeVercelApiUnrecoverableError, fmt.Sprintf("Failed to create envs from Vercel API, code: %s, body: %s", response.Status(), string(response.Body())))
	} else {
		return nil, errors.Errorf("Failed to create environment variables from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
	}
}

func (*apiClientImpl) UpdateEnvironmentVariables(ctx context.Context, projectID string, teamID string, envs map[string]EnvironmentDescription, accessToken string) (err error) {
	_, span := Tracer.Start(ctx, "apiClientImpl.UpdateEnvironmentVariables")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	params := TeamIDQueryParameter{
		TeamID: teamID,
	}
	client := resty.New()
	v, _ := query.Values(params)

	g := new(errgroup.Group)
	for key, value := range envs {
		envID := key
		envValue := value
		g.Go(func() error {
			bodyJson, err := json.Marshal(envValue)
			if err != nil {
				tilog.Error(ctx, fmt.Sprintf("Failed to marshal environment variable %s", envID), zap.Error(err), zap.Any("envValue", envValue))
				return err
			}
			response, err := client.
				R().SetQueryString(v.Encode()).
				SetAuthToken(accessToken).
				SetBody(bodyJson).
				SetHeader("Content-Type", "application/json").
				Patch("https://api.vercel.com/v9/projects/" + projectID + "/env/" + envID)
			if err != nil {
				tilog.Error(ctx, fmt.Sprintf("Failed to update environment variable %s from Vercel API", envID), zap.Error(err))
				return err
			}

			if response.IsSuccess() {
				return nil
			} else if response.StatusCode() == 404 {
				tilog.Error(ctx, fmt.Sprintf("Environment variable %s not found for project %s", envID, projectID), zap.String("code", response.Status()), zap.String("body", string(response.Body())))
				return bizerr.Restore(ErrorCodeVercelApiNotFoundError, fmt.Sprintf("Environment variable not found for project , code: %s, body: %s", response.Status(), string(response.Body())))
			} else if response.StatusCode() >= 400 && response.StatusCode() < 500 {
				tilog.Error(ctx, fmt.Sprintf("Failed to update environment variable %s from Vercel API", envID), zap.String("code", response.Status()), zap.String("body", string(response.Body())))
				return bizerr.Restore(ErrorCodeVercelApiUnrecoverableError, fmt.Sprintf("Failed to update envs from Vercel API, code: %s, body: %s", response.Status(), string(response.Body())))
			} else {
				tilog.Error(ctx, fmt.Sprintf("Failed to update environment variable %s from Vercel API", envID), zap.String("code", response.Status()), zap.String("body", string(response.Body())))
				return errors.Errorf("Failed to update environment variables from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
			}
		})
	}

	if err := g.Wait(); err != nil {
		return err
	} else {
		return nil
	}
}

func (*apiClientImpl) DeleteEnvironmentVariables(ctx context.Context, projectID string, teamID string, envsToDelete []string, accessToken string) (err error) {
	ctx, span := Tracer.Start(ctx, "apiClientImpl.DeleteEnvironmentVariables")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	params := TeamIDQueryParameter{
		TeamID: teamID,
	}
	client := resty.New()
	v, _ := query.Values(params)

	g := new(errgroup.Group)
	for _, environment := range envsToDelete {
		env := environment
		g.Go(func() error {
			response, err := client.
				R().SetQueryString(v.Encode()).
				SetAuthToken(accessToken).
				Delete("https://api.vercel.com/v9/projects/" + projectID + "/env/" + env)
			if err != nil {
				tilog.Error(ctx, fmt.Sprintf("Failed to delete environment variable %s from Vercel API", env), zap.Error(err))
				return err
			}

			if response.IsSuccess() {
				return nil
			} else if response.StatusCode() == 404 || response.StatusCode() == 403 {
				// TODO yangxin Quick fix: Vercel now returns a 403 error when attempting to delete a non-existing environment variable. Ignore it for now.
				// User may have deleted the environment variable manually
				tilog.Warn(ctx, fmt.Sprintf("Environment variable %s not found for project %s", env, projectID), zap.String("code", response.Status()), zap.String("body", string(response.Body())))
				return nil
			} else {
				tilog.Error(ctx, fmt.Sprintf("Failed to delete environment variable %s from Vercel API", env), zap.String("code", response.Status()), zap.String("body", string(response.Body())))
				return errors.Errorf("Failed to delete environment variable %s from Vercel API, code: %s, body: %s", env, response.Status(), string(response.Body()))
			}
		})

	}
	if err := g.Wait(); err != nil {
		return err
	} else {
		return nil
	}
}

func (*apiClientImpl) CreateCheck(ctx context.Context, deploymentID string, teamID string, accessToken string) (c *CreateCheckResponse, err error) {
	_, span := Tracer.Start(ctx, "apiClientImpl.CreateCheck")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	params := TeamIDQueryParameter{
		TeamID: teamID,
	}

	var result CreateCheckResponse
	client := resty.New()
	v, _ := query.Values(params)
	response, err := client.
		R().SetQueryString(v.Encode()).
		SetAuthToken(accessToken).
		SetResult(&result).
		SetBody(map[string]interface{}{"blocking": true, "name": "TiDB Serverless Branching Check"}).
		Post(fmt.Sprintf("https://api.vercel.com/v1/deployments/%s/checks", deploymentID))
	if err != nil {
		return nil, errors.Trace(err)
	}

	if response.IsSuccess() {
		return &result, nil
	} else if response.StatusCode() >= 400 && response.StatusCode() < 500 {
		return nil, bizerr.Restore(ErrorCodeVercelApiUnrecoverableError, fmt.Sprintf("Failed to create check from Vercel API, code: %s, body: %s", response.Status(), string(response.Body())))
	} else {
		return nil, errors.Errorf("Failed to create check from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
	}
}

func (*apiClientImpl) UpdateCheck(ctx context.Context, deploymentID string, checkID string, body UpdateCheckBody, teamID string, accessToken string) (err error) {
	_, span := Tracer.Start(ctx, "apiClientImpl.UpdateCheck")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	params := TeamIDQueryParameter{
		TeamID: teamID,
	}

	var result RetrieveIntegrationConfigurationResponse
	client := resty.New()
	v, _ := query.Values(params)
	response, err := client.
		R().SetQueryString(v.Encode()).
		SetAuthToken(accessToken).
		SetResult(&result).
		SetBody(body).
		Patch(fmt.Sprintf("https://api.vercel.com/v1/deployments/%s/checks/%s", deploymentID, checkID))
	if err != nil {
		return errors.Trace(err)
	}

	if response.IsSuccess() {
		return nil
	} else if response.StatusCode() >= 400 && response.StatusCode() < 500 {
		return bizerr.Restore(ErrorCodeVercelApiUnrecoverableError, fmt.Sprintf("Failed to update check from Vercel API, code: %s, body: %s", response.Status(), string(response.Body())))
	} else {
		return errors.Errorf("Failed to update check from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
	}
}

func (*apiClientImpl) GetDeployment(ctx context.Context, deploymentID string, teamID string, accessToken string) (_ *GetDeploymentResponse, err error) {
	_, span := Tracer.Start(ctx, "apiClientImpl.GetDeployment")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	params := TeamIDQueryParameter{
		TeamID: teamID,
	}

	var result GetDeploymentResponse
	client := resty.New()
	v, _ := query.Values(params)
	response, err := client.
		R().SetQueryString(v.Encode()).
		SetAuthToken(accessToken).
		SetResult(&result).
		Get("https://api.vercel.com/v13/deployments/" + deploymentID)
	if err != nil {
		return nil, errors.Trace(err)
	}

	if response.IsSuccess() {
		return &result, nil
	} else if response.StatusCode() >= 400 && response.StatusCode() < 500 {
		return nil, bizerr.Restore(ErrorCodeVercelApiUnrecoverableError, fmt.Sprintf("Failed to get deployment from Vercel API, code: %s, body: %s", response.Status(), string(response.Body())))
	} else {
		return nil, errors.Errorf("Failed to get deployment from Vercel API, code: %s, body: %s", response.Status(), string(response.Body()))
	}
}
