package handler

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"cloud.google.com/go/pubsub"
	"github.com/form3tech-oss/jwt-go"
	"github.com/gin-gonic/gin"
	"github.com/lestrrat/go-jwx/jwk"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/pkg/infra/provider/gcp/auth"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"golang.org/x/oauth2"
	"google.golang.org/api/option"
)

type Map map[string]interface{}

type gcpAccountProviderImpl struct {
}

const GCPTokenKey = "x-gcp-marketplace-token"
const JWTContextKey = "user"

func (provider *gcpAccountProviderImpl) GetTokenFromRedirect(c *gin.Context) (*string, error) {
	err := c.Request.ParseForm()
	if err != nil {
		return nil, errors.Trace(err)
	}
	token := c.Request.Form.Get(GCPTokenKey)
	if token == "" {
		return nil, errors.New("gcp marketplace token not found")
	}
	customerID, err := provider.ExtractAccountCode(&token)
	if err != nil {
		return nil, err
	}
	return customerID, nil
}

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

func getClientTS() (oauth2.TokenSource, error) {
	ts, err := auth.DefaultTokenSource()
	if err != nil {
		return nil, err
	}
	marketPlaceSA := config.Global().MarketPlaceConfig.GCPMarketPlaceSA
	if marketPlaceSA == "" {
		return nil, errors.New("invalid marketplace sa")
	}
	return auth.Impersonate(marketPlaceSA, ts)
}

func getGCPClient() (*http.Client, error) {
	ts, err := getClientTS()
	if err != nil {
		return nil, err
	}
	client := oauth2.NewClient(context.Background(), ts)
	return client, nil
}

func call(url, method string, input, output interface{}, client *http.Client) error {
	var inputMarshalled io.Reader
	if input == nil {
		inputMarshalled = nil
	} else {
		inputBytes, err := json.Marshal(input)
		if err != nil {
			return errors.Trace(err)
		}
		inputMarshalled = bytes.NewReader(inputBytes)
	}

	req, err := http.NewRequest(method, url, inputMarshalled)
	if err != nil {
		return errors.Trace(err)
	}
	resp, err := client.Do(req)
	if err != nil {
		return errors.Trace(err)
	}
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return errors.Trace(err)
	}
	resp.Body.Close() // nolint: errcheck

	if resp.StatusCode != 200 {
		return errors.Trace(fmt.Errorf("call gcp marketplace service resp code err %d, resp: %s", resp.StatusCode, string(bodyBytes)))
	}
	if output != nil {
		err = json.Unmarshal(bodyBytes, output)
		if err != nil {
			return errors.Trace(err)
		}
	}
	return nil
}

func doRequest(apiPath string, method string, input interface{}, output interface{}) error {
	ctx := context.Background()
	client, err := getGCPClient()
	if err != nil {
		return err
	}
	baseUrl := config.Global().MarketPlaceConfig.GCPProcurementBaseUrl
	apiPath = strings.TrimLeft(apiPath, "/")
	url := fmt.Sprintf("%s/%s", strings.TrimRight(baseUrl, "/"), strings.TrimLeft(apiPath, "/"))
	tilog.Info(ctx, "start call gcp marketplace service", zap.String("method", method), zap.String("url", url))
	err = call(url, method, input, output, client)
	if err != nil {
		tilog.Info(ctx, "failed call gcp marketplace service", zap.String("method", method), zap.String("url", url), zap.Error(err))
		return err
	}
	tilog.Info(ctx, "success call gcp marketplace service", zap.String("method", method), zap.String("url", url))
	return nil
}

func postAccountApproval(accountID string) error {
	partnerID := config.Global().MarketPlaceConfig.GCPProject
	url := fmt.Sprintf("/v1/providers/%s/accounts/%s:approve", partnerID, accountID)
	err := doRequest(url, "POST", Map{
		"approvalName": "signup",
	}, nil)
	if err != nil {
		ctx := context.Background()
		tilog.Error(ctx, "failed to approve gcp marketplace accountID", zap.String("accountID", accountID), zap.Error(err))
		return errs.NewInternalServerError("Failed to approve your gcp marketplace accountID", err)
	}
	return nil
}

func getEntitlementInfo(entitlementID string) (*EntitlementInfo, error) {
	partnerID := config.Global().MarketPlaceConfig.GCPProject
	url := fmt.Sprintf("/v1/providers/%s/entitlements/%s", partnerID, entitlementID)
	output := &EntitlementInfo{}
	err := doRequest(url, "GET", nil, &output)
	if err != nil {
		ctx := context.Background()
		tilog.Error(ctx, "failed to get entitlement info", zap.String("entitlementID", entitlementID), zap.Error(err))
		return nil, err
	}
	ss := strings.Split(output.Account, "/")
	output.Account = ss[len(ss)-1]
	return output, nil
}

func approveEntitlement(entitlementID string) error {
	partnerID := config.Global().MarketPlaceConfig.GCPProject
	url := fmt.Sprintf("/v1/providers/%s/entitlements/%s:approve", partnerID, entitlementID)
	err := doRequest(url, "POST", nil, nil)
	if err != nil && !strings.Contains(fmt.Sprintf("%v", err), "FAILED_PRECONDITION") { // whitelist, gcp marketplace account subscribed but not registered
		ctx := context.Background()
		tilog.Error(ctx, "failed to approve gcp marketplace entitlement", zap.String("entitlementID", entitlementID), zap.Error(err))
	}
	return err
}

func getSigningKey(token *jwt.Token) (interface{}, error) {
	ctx := context.Background()
	keyID, ok := token.Header["kid"].(string)
	if !ok {
		return nil, errors.New("expecting JWT header to have string kid")
	}
	claims, ok := token.Claims.(jwt.MapClaims)

	if !ok {
		return nil, errors.New("unable to get JWT payload")
	}

	iss, ok := claims["iss"].(string)

	if iss == "" || !ok {
		return nil, errors.New("unable to get JWT payload")
	}

	issSub := strings.Replace(iss, "x509", "jwk", -1)

	tilog.Info(ctx, fmt.Sprintf("Getting keys from %s", issSub))

	keySet, err := jwk.FetchHTTP(issSub)

	if err != nil {
		tilog.Error(ctx, fmt.Sprintf("Error fetching keyset from ISS %s: %#v", issSub, err))
		return nil, errors.Trace(err)
	}

	tilog.Info(ctx, fmt.Sprintf("KeySet found %#v", keySet))

	if key := keySet.LookupKeyID(keyID); len(key) == 1 {
		return key[0].Materialize()
	}

	return nil, errors.New("unable to find key")

}

var signKeyFunc jwt.Keyfunc = getSigningKey

var postAccountApprovalFunc = postAccountApproval

func (provider *gcpAccountProviderImpl) ExtractAccountCode(token *string) (*string, error) {
	jwtToken, err := jwt.Parse(*token, signKeyFunc)

	if err != nil {
		return nil, errors.Trace(err)
	}

	claims := jwtToken.Claims.(jwt.MapClaims)

	if claims["iss"] != config.Global().MarketPlaceConfig.GCPISSURL {
		return nil, errors.Errorf("Wrong gcp iss url %v", claims["iss"])
	}

	customerID := claims["sub"].(string)

	if customerID == "" {
		return nil, errors.Errorf("Wrong customer id %v", customerID)
	}
	return &customerID, nil
}

func (provider *gcpAccountProviderImpl) ResolveToken(token *string) (*string, error) {
	customerID := token

	return customerID, nil
}

func (provider *gcpAccountProviderImpl) ApproveCode(code *string) error {
	customerID := code

	err := postAccountApprovalFunc(*customerID)
	return err
}

func getGCPPubSub(ctx context.Context) (*pubsub.Subscription, *pubsub.Client, error) {
	project := config.Global().MarketPlaceConfig.GCPProject
	ts, err := getClientTS()
	if err != nil {
		tilog.Error(ctx, "failed get client ts", zap.String("GCPProjecttID", project), zap.Error(err))
		return nil, nil, err
	}
	client, err := pubsub.NewClient(ctx, project, option.WithTokenSource(ts))
	if err != nil {
		tilog.Error(ctx, "Error creating pubsub client", zap.String("GCPProjecttID", project), zap.Error(err))
		return nil, nil, err
	}
	subscription := client.Subscription(config.Global().MarketPlaceConfig.GCPPubSubTopic)
	return subscription, client, nil
}

const (
	GCPMarketplaceStatusPendingCreate = "ENTITLEMENT_CREATION_REQUESTED"
	GCPMarketplaceStatusPendingCancel = "ENTITLEMENT_CANCELLING" //nolint: misspell
)

type PubSubMsg struct {
	EventId     string          `json:"eventId"`
	EventType   string          `json:"eventType"`
	Entitlement EntitlementMeta `json:"entitlement"`
}

type EntitlementMeta struct {
	Id string `json:"id"`
}

type EntitlementInfo struct {
	Account string `json:"account"`
	Product string `json:"product"`
}

func processOneGCPSubscribeChange(ctx context.Context, msgBody []byte, processor ProcessTenantSubscribeChangeFunc) (bool, error) {
	m := PubSubMsg{}
	err := json.Unmarshal(msgBody, &m)
	if err != nil {
		return false, errors.Errorf("Invalid marketplace sqs msg body. %v", string(msgBody))
	}
	action := m.EventType
	change := SubscribeChange{}
	switch action {
	case GCPMarketplaceStatusPendingCreate:
		change.SubscribeStatus = SubscribeStatusSubscribed
	case GCPMarketplaceStatusPendingCancel:
		change.SubscribeStatus = SubscribeStatusUnsubscribed
	default:
		return true, nil
	}
	info, err := getEntitlementInfo(m.Entitlement.Id)
	if err != nil {
		return false, err
	}

	// other product pub sub
	if info.Product != config.Global().MarketPlaceConfig.GCPProductCode {
		tilog.Info(ctx, "other product PubSub messages, just ignore it")
		return false, nil
	}

	if action == GCPMarketplaceStatusPendingCreate {
		err = approveEntitlement(m.Entitlement.Id)
		if err != nil {
			return false, err
		}
	}

	// getEntitlementInfo(change.)
	change.AccountProvider = GCPAccountProvider
	change.AccountCode = info.Account
	return processor(ctx, change)
}

func (provider *gcpAccountProviderImpl) ProcessTenantSubscribeChange(ctx context.Context, processor ProcessTenantSubscribeChangeFunc) error {
	if !config.Global().MarketPlaceConfig.EnableGCPBackground {
		tilog.Info(ctx, "Skip process gcp tenant subscribe by config")
		return nil
	}

	subscription, client, err := getGCPPubSub(ctx)
	if err != nil {
		return err
	}

	defer client.Close() // nolint: errcheck

	ctx, cancel := context.WithTimeout(ctx, time.Second*5)
	defer cancel()

	tilog.Info(ctx, fmt.Sprintf("Begin receiving messages from subscription %s", subscription.String()))
	errRcv := subscription.Receive(ctx, func(ctx context.Context, msg *pubsub.Message) {
		tilog.Info(ctx, "received one gcp msg", zap.String("msg", string(msg.Data)))
		processed, err := processOneGCPSubscribeChange(ctx, msg.Data, processor)
		if err != nil || !processed {
			tilog.Info(ctx, "gcp msg dropped", zap.Bool("processed", processed), zap.Error(err))
			msg.Nack()
		} else {
			tilog.Info(ctx, "gcp msg acked")
			msg.Ack()
		}
	})
	if errRcv != nil {
		return errors.Trace(errRcv)
	}
	tilog.Info(ctx, fmt.Sprintf("End receiving message from subscription %s", subscription.String()))
	return nil

}
