package cart_client

import (
	"github.com/gofiber/fiber/v2"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
	"penahub.gitlab.yandexcloud.net/backend/penahub_common/log_mw"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/errors"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/broker/tariff"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/client"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/controller/http"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/repository"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/proto/discount"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/utils"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/utils/transfer"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/validate"
	"strings"
	"sync"
	"time"
)

type Deps struct {
	MiddleWare     *http.MiddleWare
	Logger         *zap.Logger
	AccountRepo    *repository.AccountRepository
	HistoryRepo    *repository.HistoryRepository
	HubAdminClient *client.HubadminClient
	DiscountClient *client.DiscountClient
	CurrencyClient *client.CurrencyClient
	Producer       *tariff.Producer
}

type CartController struct {
	middleWare     *http.MiddleWare
	logger         *zap.Logger
	accountRepo    *repository.AccountRepository
	historyRepo    *repository.HistoryRepository
	hubAdminClient *client.HubadminClient
	discountClient *client.DiscountClient
	currencyClient *client.CurrencyClient
	producer       *tariff.Producer
}

func NewCartController(deps Deps) *CartController {
	return &CartController{
		middleWare:     deps.MiddleWare,
		logger:         deps.Logger,
		accountRepo:    deps.AccountRepo,
		historyRepo:    deps.HistoryRepo,
		hubAdminClient: deps.HubAdminClient,
		discountClient: deps.DiscountClient,
		currencyClient: deps.CurrencyClient,
		producer:       deps.Producer,
	}
}

func (receiver *CartController) Remove(ctx *fiber.Ctx) error {
	userID, ok := receiver.middleWare.ExtractUserID(ctx)
	if !ok || userID == "" {
		return receiver.middleWare.NoAuth(ctx)
	}
	// todo проверить менять или нет
	id := ctx.Query("id")
	if id == "" {
		return receiver.middleWare.Error(ctx, fiber.StatusBadRequest, "empty item id")
	}

	cartItems, err := receiver.accountRepo.RemoveItemFromCart(ctx.Context(), userID, id)
	if err != nil {
		return receiver.middleWare.ErrorOld(ctx, err)
	}

	return ctx.Status(fiber.StatusOK).JSON(cartItems)
}

func (receiver *CartController) Add2cart(ctx *fiber.Ctx) error {
	userID, ok := receiver.middleWare.ExtractUserID(ctx)
	if !ok || userID == "" {
		return receiver.middleWare.NoAuth(ctx)
	}

	token, ok := receiver.middleWare.ExtractToken(ctx)
	if !ok || token == "" {
		return receiver.middleWare.NoAuth(ctx)
	}

	hlogger := log_mw.ExtractLogger(ctx)

	tariffID := ctx.Query("id")
	if tariffID == "" {
		return receiver.middleWare.Error(ctx, fiber.StatusBadRequest, "empty item id")
	}

	tariff, err := receiver.hubAdminClient.GetTariff(ctx.Context(), token, tariffID)
	if err != nil {
		return receiver.middleWare.ErrorOld(ctx, err)
	}

	if tariff == nil {
		return receiver.middleWare.Error(ctx, fiber.StatusNotFound, "tariff not found")
	}

	cartItems, err := receiver.accountRepo.AddItemToCart(ctx.Context(), userID, tariffID)
	if err != nil {
		return receiver.middleWare.ErrorOld(ctx, err)
	}

	hlogger.Emit(models.InfoAddToCart{
		CtxUserID:    userID,
		CtxAccountID: cartItems.ID,
		CtxTariffID:  tariffID,
	})

	return ctx.Status(fiber.StatusOK).JSON(cartItems)
}

func (receiver *CartController) Pay(ctx *fiber.Ctx) error {
	userID, ok := receiver.middleWare.ExtractUserID(ctx)
	if !ok || userID == "" {
		return receiver.middleWare.NoAuth(ctx)
	}

	accessToken, ok := receiver.middleWare.ExtractToken(ctx)
	if !ok || accessToken == "" {
		return receiver.middleWare.NoAuth(ctx)
	}

	hlogger := log_mw.ExtractLogger(ctx)

	account, err := receiver.accountRepo.FindByUserID(ctx.Context(), userID)
	if err != nil {
		return receiver.middleWare.ErrorOld(ctx, err)
	}

	receiver.logger.Info("account for pay", zap.Any("acc", account))

	if len(account.Cart) == 0 {
		return ctx.Status(fiber.StatusNotAcceptable).SendString("cart is empty")
	}

	tariffs, err := receiver.hubAdminClient.GetTariffs(ctx.Context(), accessToken, account.Cart)
	if err != nil {
		return receiver.middleWare.ErrorOld(ctx, err)
	}

	receiver.logger.Info("tariffs for pay", zap.Any("acc", tariffs))

	tariffsAmount := utils.CalculateCartPurchasesAmount(tariffs)

	discountResponse, err := receiver.discountClient.Apply(ctx.Context(), &discount.ApplyDiscountRequest{
		UserInformation: &discount.UserInformation{
			ID:                  account.UserID,
			Type:                string(account.Status),
			PurchasesAmount:     uint64(account.Wallet.Spent),
			CartPurchasesAmount: tariffsAmount,
		},
		Products: transfer.TariffsToProductInformations(tariffs),
		Date:     timestamppb.New(time.Now()),
	})
	if err != nil {
		return receiver.middleWare.ErrorOld(ctx, err)
	}

	receiver.logger.Info("discountResponse for pay", zap.Any("acc", discount.ApplyDiscountRequest{
		UserInformation: &discount.UserInformation{
			ID:                  account.UserID,
			Type:                string(account.Status),
			PurchasesAmount:     uint64(account.Wallet.Spent),
			CartPurchasesAmount: tariffsAmount,
		},
		Products: transfer.TariffsToProductInformations(tariffs),
		Date:     timestamppb.New(time.Now()),
	}))

	if account.Wallet.Money < int64(discountResponse.Price) {
		hlogger.Emit(models.InfoPayCart{
			CtxUserID:    userID,
			CtxAccountID: account.ID,
			KeySuccess:   false,
			CtxPrice:     int64(discountResponse.Price - uint64(account.Wallet.Money)),
			CtxTariff:    strings.Join(account.Cart, ","),
			CtxDiscount:  strings.Join(utils.GetAppliedDiscountsIDs(discountResponse.AppliedDiscounts), ","),
			CtxRowPrice:  int64(tariffsAmount),
			CtxRowData:   utils.MarshalRawDetails(models.RawDetails{Tariffs: tariffs, Price: int64(discountResponse.Price)}),
		})
		return receiver.middleWare.Error(ctx, fiber.StatusPaymentRequired, "insufficient funds: %d", int64(discountResponse.Price)-account.Wallet.Money)
	}

	for _, applied := range discountResponse.AppliedDiscounts {
		if applied.Condition.User != nil && *applied.Condition.User != "" {
			_, err := receiver.discountClient.DeleteDiscount(ctx.Context(), &discount.GetDiscountByIDRequest{
				ID: applied.ID,
			})

			if err != nil {
				return receiver.middleWare.Error(ctx, fiber.StatusInternalServerError, "failed delete discount by id:%s", applied.ID)
			}
		}
	}

	// WithdrawAccountWalletMoney

	request := models.WithdrawAccountWallet{
		Money:   int64(discountResponse.Price),
		Account: account,
	}

	if validate.IsStringEmpty(request.Account.Wallet.Currency) {
		request.Account.Wallet.Currency = models.InternalCurrencyKey
	}

	var updatedAccount *models.Account

	if request.Account.Wallet.Currency == models.InternalCurrencyKey {
		accountx, err := receiver.accountRepo.ChangeWallet(ctx.Context(), request.Account.UserID, &models.Wallet{
			Cash:            request.Account.Wallet.Cash - request.Money,
			Money:           request.Account.Wallet.Money - request.Money,
			Spent:           request.Account.Wallet.Spent + request.Money,
			PurchasesAmount: request.Account.Wallet.PurchasesAmount,
			Currency:        request.Account.Wallet.Currency,
		})
		if err != nil {
			return receiver.middleWare.ErrorOld(ctx, err)
		}
		updatedAccount = accountx
	} else {
		cash, err := receiver.currencyClient.Translate(ctx.Context(), &models.TranslateCurrency{
			Money: request.Money,
			From:  models.InternalCurrencyKey,
			To:    request.Account.Wallet.Currency,
		})
		if err != nil {
			return receiver.middleWare.ErrorOld(ctx, err)
		}

		accountx, err := receiver.accountRepo.ChangeWallet(ctx.Context(), request.Account.UserID, &models.Wallet{
			Cash:            request.Account.Wallet.Cash - cash,
			Money:           request.Account.Wallet.Money - request.Money,
			Spent:           request.Account.Wallet.Spent + request.Money,
			PurchasesAmount: request.Account.Wallet.PurchasesAmount,
			Currency:        request.Account.Wallet.Currency,
		})
		if err != nil {
			return receiver.middleWare.ErrorOld(ctx, err)
		}

		updatedAccount = accountx
	}

	if _, err := receiver.historyRepo.Insert(ctx.Context(), &models.History{
		Key:     models.CustomerHistoryKeyPayCart,
		UserID:  account.UserID,
		Comment: "Успешная оплата корзины",
		RawDetails: models.RawDetails{
			Tariffs: tariffs,
			Price:   int64(discountResponse.Price),
		},
	}); err != nil {
		return receiver.middleWare.ErrorOld(ctx, err)
	}

	// TODO: обработать ошибки при отправке сообщений

	sendErrors := make([]errors.Error, 0)
	waitGroup := sync.WaitGroup{}
	mutex := sync.Mutex{}

	for _, tariff := range tariffs {
		waitGroup.Add(1)

		go func(currentTariff models.Tariff) {
			defer waitGroup.Done()

			if err := receiver.producer.Send(ctx.Context(), userID, &currentTariff); err != nil {
				mutex.Lock()
				defer mutex.Unlock()
				sendErrors = append(sendErrors, err)
			}
		}(tariff)
	}

	waitGroup.Wait()

	if len(sendErrors) > 0 {
		for _, err := range sendErrors {
			receiver.logger.Error("failed to send tariffs to broker on <Pay> of <CartService>", zap.Error(err))
		}
		return receiver.middleWare.Error(ctx, fiber.StatusInternalServerError, "failed to send tariffs to broker")
	}

	if _, err := receiver.accountRepo.ClearCart(ctx.Context(), account.UserID); err != nil {
		return receiver.middleWare.ErrorOld(ctx, err)
	}

	updatedAccount.Cart = []string{}

	hlogger.Emit(models.InfoPayCart{
		CtxUserID:    userID,
		CtxAccountID: updatedAccount.ID,
		KeySuccess:   true,
		CtxPrice:     int64(discountResponse.Price),
		CtxTariff:    strings.Join(account.Cart, ","),
		CtxDiscount:  strings.Join(utils.GetAppliedDiscountsIDs(discountResponse.AppliedDiscounts), ","),
		CtxRowPrice:  int64(tariffsAmount),
		CtxRowData:   utils.MarshalRawDetails(models.RawDetails{Tariffs: tariffs, Price: int64(discountResponse.Price)}),
	})

	return ctx.Status(fiber.StatusOK).JSON(updatedAccount)
}
