package repository

import (
	"context"
	"encoding/json"
	"fmt"
	adapter "git.gocasts.ir/ebhomengo/niki/adapter/redis"
	"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
	richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
	"git.gocasts.ir/ebhomengo/niki/types"
	"github.com/redis/go-redis/v9"
	"strconv"
	"time"
)

type Cache struct {
	client *adapter.Adapter
	config Config
}

func NewCache(client *adapter.Adapter, cfg Config) Cache {
	return Cache{client: client, config: cfg}
}

func (c Cache) cartKey(userID types.ID) string {
	return fmt.Sprintf("%s:%d", c.config.CacheKartKeyPrefix, userID)
}

func (c Cache) itemKey(itemID types.ID) string {
	return fmt.Sprintf("item:%d", itemID)
}

func (c Cache) kartIDsKey(userID types.ID) string {
	return fmt.Sprintf("cart:%d:items", userID)
}

func (c Cache) upsertCart(ctx context.Context, cart entity.Cart) error {
	cartKey := c.cartKey(cart.UserID)
	cartIDsKey := c.kartIDsKey(cart.UserID)

	pipe := c.client.Client().Pipeline()
	pipe.HSet(ctx, cartKey, map[string]interface{}{
		IDField:         cart.ID,
		UserIDField:     cart.UserID,
		StatusField:     cart.Status,
		TotalPriceField: cart.TotalPrice,
		ExpireAtField:   cart.ExpireAt,
		CreatedAtField:  cart.CreatedAt,
		UpdatedAtField:  cart.UpdatedAt,
	})

	pipe.Expire(ctx, cartKey, c.config.CacheTTL)

	pipe.Del(ctx, cartIDsKey)

	for _, i := range cart.Items {

		pipe.SAdd(ctx, cartIDsKey, i.ID)

		itemKey := c.itemKey(i.ID)
		jsonItem, err := json.Marshal(i)
		if err != nil {
			return richerror.New(richerror.Op(op("cache.upsertCart"))).
				WithKind(richerror.KindUnexpected).WithErr(err)
		}

		pipe.Set(ctx, itemKey, string(jsonItem), c.config.CacheTTL)
	}

	pipe.Expire(ctx, cartIDsKey, c.config.CacheTTL)

	_, err := pipe.Exec(ctx)
	if err != nil {
		return richerror.New(richerror.Op(op("cache.upsertCart"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	return nil
}

func (c Cache) getCart(ctx context.Context, userID types.ID) (entity.Cart, error) {
	cartKey := c.cartKey(userID)
	cartIDsKey := c.kartIDsKey(userID)

	pipe := c.client.Client().Pipeline()

	resCart := pipe.HGetAll(ctx, cartKey)
	resIDs := pipe.SMembers(ctx, cartIDsKey)

	_, err := pipe.Exec(ctx)
	if err != nil {
		return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	result := resCart.Val()

	if len(result) < 1 {
		return entity.Cart{}, nil
	}

	cart, err := parseCartFromRedis(result)
	if err != nil {
		return entity.Cart{}, err
	}

	if cart.ID < 1 {
		return entity.Cart{}, nil
	}

	ids := resIDs.Val()

	if len(ids) < 1 {
		return cart, nil
	}

	itemPipe := c.client.Client().Pipeline()

	itemResults := make([]*redis.StringCmd, len(ids))

	for i, idStr := range ids {
		id, _ := strconv.Atoi(idStr)
		itemKey := c.itemKey(types.ID(id))
		cmd := itemPipe.Get(ctx, itemKey)
		itemResults[i] = cmd
	}

	if _, err := itemPipe.Exec(ctx); err != nil {
		return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	for _, cmd := range itemResults {
		if err := cmd.Err(); err != nil {
			if err == redis.Nil {

				continue
			}

			return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
				WithKind(richerror.KindUnexpected).WithErr(err)
		}

		var item entity.Item

		if err := json.Unmarshal([]byte(cmd.Val()), &item); err != nil {
			return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
				WithKind(richerror.KindUnexpected).WithErr(err)
		}

		cart.Items = append(cart.Items, item)
	}

	return cart, nil
}

func parseCartFromRedis(result map[string]string) (entity.Cart, error) {
	if len(result) == 0 {
		return entity.Cart{}, nil
	}

	var cart entity.Cart

	cart.Items = []entity.Item{}

	for key, val := range result {
		switch key {
		case IDField:
			id, err := strconv.ParseInt(val, 10, 64)
			if err != nil {
				return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
					WithKind(richerror.KindUnexpected).WithErr(err)
			}
			cart.ID = types.ID(id)
		case UserIDField:
			uid, err := strconv.ParseInt(val, 10, 64)
			if err != nil {
				return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
					WithKind(richerror.KindUnexpected).WithErr(err)
			}
			cart.UserID = types.ID(uid)
		case StatusField:
			cart.Status = entity.CartStatus(val)
		case TotalPriceField:
			price, err := strconv.ParseFloat(val, 64)
			if err != nil {
				return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
					WithKind(richerror.KindUnexpected).WithErr(err)
			}
			cart.TotalPrice = price
		case ExpireAtField, CreatedAtField, UpdatedAtField:
			t, err := time.Parse(time.RFC3339, val)
			if err != nil {
				return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))).
					WithKind(richerror.KindUnexpected).WithErr(err)
			}
			switch key {
			case ExpireAtField:
				cart.ExpireAt = t
			case CreatedAtField:
				cart.CreatedAt = t
			case UpdatedAtField:
				cart.UpdatedAt = t
			}
		}
	}

	return cart, nil
}

func (c Cache) deleteCart(ctx context.Context, userID types.ID) error {
	cartKey := c.cartKey(userID)
	cartIDsKey := c.kartIDsKey(userID)
	pipe := c.client.Client().Pipeline()

	pipe.Del(ctx, cartKey)
	pipe.Del(ctx, cartIDsKey)

	_, err := pipe.Exec(ctx)
	if err != nil {
		return richerror.New(richerror.Op(op("cache-deleteCart"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	return nil
}
