package handler

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"

	"github.com/tidbcloud/pkgv2/auth/util"

	"github.com/tidbcloud/account/thirdparty/billing2"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

// azure doc: https://learn.microsoft.com/en-us/partner-center/marketplace-offers/partner-center-portal/pc-saas-fulfillment-life-cycle

type azureAccountProviderImpl struct {
}

const AzureTokenKey = "token"

var (
	azureCtxTraceKey util.ContextKey = "azure_ctx_local_trace"
)

func (provider *azureAccountProviderImpl) GetTokenFromRedirect(c *gin.Context) (*string, error) {
	ctx := c.Request.Context()

	token := c.Query(AzureTokenKey)
	tilog.Info(ctx, "azure account provider get token from redirect", zap.Any("token", token))
	if token == "" {
		return nil, errors.New("invalid azure token")
	}

	decodeToken, err := url.QueryUnescape(token)
	if err != nil {
		tilog.Error(ctx, "azure account provider unescape token error", zap.Any("token", token), zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "azure account provider get unescape token", zap.Any("token", decodeToken))

	return &decodeToken, nil
}

func (provider *azureAccountProviderImpl) GetFreeTrialFromRedirect(c *gin.Context) (bool, error) {
	return false, nil
}

func (provider *azureAccountProviderImpl) ResolveToken(token *string) (*string, error) {
	trace := uuid.New().String()
	ctx := context.WithValue(context.Background(), azureCtxTraceKey, trace)
	tilog.Info(ctx, "azure account provider resolve token start", zap.Any("trace", trace), zap.Any("token", token))

	if token == nil || *token == "" {
		tilog.Warn(ctx, "azure account provider resolve token fail, token is blank", zap.Any("trace", trace))
		return nil, errors.New("token is blank")
	}

	subscription, err := resolveASubscription(ctx, *token)
	tilog.Info(ctx, "azure account provider resolve token get a subscription result", zap.Any("trace", trace), zap.Any("token", token), zap.Any("subscription", subscription), zap.Any("err", err))
	if err != nil {
		tilog.Error(ctx, "azure account provider resolve token get a subscription error", zap.Any("trace", trace), zap.Any("token", token), zap.Any("err", err))
		return nil, err
	}
	return &subscription.ID, nil

}

func (provider *azureAccountProviderImpl) ApproveCode(code *string) error {
	trace := uuid.New().String()
	ctx := context.WithValue(context.Background(), azureCtxTraceKey, trace)

	if code == nil || *code == "" {
		return errors.New("code is blank")
	}

	err := activateASubscription(ctx, *code)
	if err != nil {
		tilog.Error(ctx, "azure account provider approve code activate a subscription error", zap.Any("trace", trace), zap.Any("subscriptionID", code), zap.Any("err", err))
		return err
	}
	tilog.Info(ctx, "azure account provider approve code activate a subscription success", zap.Any("trace", trace), zap.Any("subscriptionID", code))
	return nil
}

func (provider *azureAccountProviderImpl) ProcessTenantSubscribeChange(ctx context.Context, processor ProcessTenantSubscribeChangeFunc) error {
	return nil
}

const (
	activateASubscriptionUrlFormat = "https://marketplaceapi.microsoft.com/api/saas/subscriptions/%s/activate?api-version=%s"
	resolveASubscriptionUrlFormat  = "https://marketplaceapi.microsoft.com/api/saas/subscriptions/resolve?api-version=%s"
	authorizationHeaderValueFormat = "Bearer %s"
	apiVersion                     = "2018-08-31"
)

var (
	azureHttpClient = &http.Client{
		Timeout: time.Second * 6,
	}
)

func activateASubscription(ctx context.Context, subscriptionId string) error {
	trace := getTraceFromContext(ctx)
	if trace == "" {
		trace = uuid.New().String()
	}

	activateUrl := fmt.Sprintf(activateASubscriptionUrlFormat, subscriptionId, apiVersion)
	accessToken, err := getAzureAccessToken(ctx)
	if err != nil {
		return err
	}
	authorization := fmt.Sprintf(authorizationHeaderValueFormat, accessToken)

	req, err := http.NewRequestWithContext(ctx, "POST", activateUrl, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("x-ms-requestid", uuid.New().String())
	req.Header.Set("authorization", authorization)

	tilog.Info(ctx, "activate a subscription request", zap.Any("trace", trace), zap.Any("url", activateUrl), zap.Any("subscriptionId", subscriptionId), zap.Any("authorization", authorization))

	rsp, err := azureHttpClient.Do(req)
	if err != nil {
		tilog.Info(ctx, "activate a subscription error", zap.Any("trace", trace), zap.Any("err", err))
		return err
	}
	defer func() {
		_ = rsp.Body.Close()
	}()
	httpCode := rsp.StatusCode
	if httpCode != 200 {
		tilog.Info(ctx, "activate a subscription return http code not 200", zap.Any("trace", trace), zap.Any("httpCode", httpCode))
		return fmt.Errorf("activate a subscription rsp code is %d", httpCode)
	}
	tilog.Info(ctx, "activate a subscription success", zap.Any("trace", trace), zap.Any("subscriptionId", subscriptionId))
	return nil
}

func getTraceFromContext(ctx context.Context) string {
	value := ctx.Value(azureCtxTraceKey)
	if value == nil {
		return ""
	}
	str, ok := value.(string)
	if !ok {
		return ""
	}
	return str
}

func resolveASubscription(ctx context.Context, token string) (*resolveSubscriptionRet, error) {
	trace := getTraceFromContext(ctx)
	if trace == "" {
		trace = uuid.New().String()
	}

	resolveUrl := fmt.Sprintf(resolveASubscriptionUrlFormat, apiVersion)
	accessToken, err := getAzureAccessToken(ctx)
	if err != nil {
		return nil, err
	}
	authorization := fmt.Sprintf(authorizationHeaderValueFormat, accessToken)

	req, err := http.NewRequestWithContext(ctx, "POST", resolveUrl, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("x-ms-requestid", uuid.New().String())
	req.Header.Set("authorization", authorization)
	req.Header.Set("x-ms-marketplace-token", token)

	tilog.Info(ctx, "resolve a subscription request", zap.Any("trace", trace), zap.Any("url", resolveUrl), zap.Any("token", token), zap.Any("authorization", authorization))

	rsp, err := azureHttpClient.Do(req)
	if err != nil {
		tilog.Info(ctx, "resolve a subscription error", zap.Any("trace", trace), zap.Any("err", err))
		return nil, err
	}
	defer func() {
		_ = rsp.Body.Close()
	}()
	httpCode := rsp.StatusCode
	body, err := io.ReadAll(rsp.Body)
	tilog.Info(ctx, "resolve a subscription return http code and body", zap.Any("trace", trace), zap.Any("httpCode", httpCode), zap.Any("body", string(body)), zap.Any("err", err))
	if httpCode != 200 {
		return nil, fmt.Errorf("resolve a subscription, rsp code is %d", httpCode)
	}
	if err != nil {
		return nil, err
	}
	var ret resolveSubscriptionRet
	err = json.Unmarshal(body, &ret)
	if err != nil {
		tilog.Info(ctx, "resolve a subscription parse body error", zap.Any("trace", trace), zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "resolve a subscription parse body result", zap.Any("trace", trace), zap.Any("ret", ret))
	return &ret, nil
}

type resolveSubscriptionRet struct {
	ID               string `json:"id"`
	SubscriptionName string `json:"subscriptionName"`
	OfferId          string `json:"offerId"`
	PlanId           string `json:"planId"`
	Quantity         int    `json:"quantity"`
	Subscription     struct {
		ID                     string `json:"id"`
		PublisherId            string `json:"publisherId"`
		OfferId                string `json:"offerId"`
		SaasSubscriptionStatus string `json:"saasSubscriptionStatus"`
		Beneficiary            struct {
			EmailId  string `json:"emailId"`
			ObjectId string `json:"objectId"`
			TenantId string `json:"tenantId"`
			Puid     string `json:"puid"`
		} `json:"beneficiary"`
		Purchaser struct {
			EmailId  string `json:"emailId"`
			ObjectId string `json:"objectId"`
			TenantId string `json:"tenantId"`
			Puid     string `json:"puid"`
		} `json:"purchaser"`
	} `json:"subscription"`
}

func getAzureAccessToken(ctx context.Context) (string, error) {
	azureAccessToken, err := billing2.GetHandler().GetAzureAccessToken(ctx)
	if err != nil {
		return "", err
	}
	return azureAccessToken.AccessToken, nil
}
