package repository

import (
	"context"
	"encoding/json"
	"fmt"
	"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"
	"strings"
	"time"
)

const (
	FieldNumber     = 5
	UserIDField     = "user_id"
	CreatedAtField  = "created_at"
	ExpireAtField   = "expire_at"
	TotalPriceField = "total_price"
)

type Config struct {
	KartKeyPrefix string        `koanf:"kart_key_prefix"`
	TTL           time.Duration `koanf:"ttl"`
}

type Repo struct {
	client *redis.Client
	config Config
}

func New(client *redis.Client, cfg Config) Repo {
	return Repo{client: client, config: cfg}
}

func (r Repo) cartKey(userID types.ID) string {
	return r.config.KartKeyPrefix + fmt.Sprintf("%d", userID)
}

func (r Repo) itemKey(productID types.ID) string {
	return fmt.Sprintf("item:%d", productID)
}

func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error {
	const op = "shoppingbasketapp.repository.AddItem"

	cartKey := r.cartKey(userID)
	itemKey := r.itemKey(item.ProductID)
	now := time.Now().UnixNano()

	itemJson, _ := json.Marshal(item)

	exists, _ := r.client.Exists(ctx, cartKey).Result()

	if exists == 0 {
		r.client.HSet(ctx, cartKey, map[string]interface{}{
			UserIDField:     userID,
			itemKey:         string(itemJson),
			TotalPriceField: item.Price * types.Price(item.Quantity),
			CreatedAtField:  now,
			ExpireAtField:   now + r.config.TTL.Nanoseconds(),
		})
	} else {
		existsItem, _ := r.client.HGet(ctx, cartKey, itemKey).Result()
		if existsItem != "" {
			var i entity.Item
			if err := json.Unmarshal([]byte(existsItem), &i); err != nil {
				return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
			}

			item.Quantity += i.Quantity
			itemJson, _ = json.Marshal(item)
		}

		r.client.HSet(ctx, cartKey, itemKey, string(itemJson))
		r.client.HSet(ctx, cartKey, ExpireAtField, now+r.config.TTL.Nanoseconds())
		if err := r.updateTotalPrice(ctx, cartKey); err != nil {
			return err
		}
	}

	r.client.Expire(ctx, cartKey, r.config.TTL)

	return nil
}

func parsInt(s string) int64 {
	i, _ := strconv.ParseInt(s, 10, 64)
	return i
}

func (r Repo) GetCart(ctx context.Context, userID types.ID) (entity.Cart, error) {
	const op = "shoppingbasketapp.repository.GetCart"
	cartKey := r.cartKey(userID)

	exists, err := r.client.Exists(ctx, cartKey).Result()
	if err != nil {
		return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if exists == 0 {
		return entity.Cart{}, richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
	}

	allCart, err := r.client.HGetAll(ctx, cartKey).Result()
	if err != nil {
		return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	c := entity.Cart{Items: []entity.Item{}}

	for field, value := range allCart {
		if strings.HasPrefix(field, "item:") {
			var i entity.Item
			if err := json.Unmarshal([]byte(value), &i); err != nil {
				return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
			}

			c.Items = append(c.Items, i)

			continue
		}

		switch field {
		case UserIDField:
			c.UserID = types.ID(parsInt(value))
		case TotalPriceField:
			c.TotalPrice = types.Price(parsInt(value))
		case CreatedAtField:
			c.CreatedAt = parsInt(value)
		case ExpireAtField:
			c.ExpireAt = parsInt(value)
		}
	}

	return c, nil
}

func (r Repo) DeleteItem(ctx context.Context, userID, productID types.ID) error {
	const op = "shoppingbasketapp.repository.DeleteItem"
	cartKey := r.cartKey(userID)
	itemKey := r.itemKey(productID)

	if err := r.existsCart(ctx, cartKey); err != nil {
		return err
	}

	if err := r.existsItem(ctx, cartKey, itemKey); err != nil {
		return err
	}

	if err := r.client.HDel(ctx, cartKey, itemKey).Err(); err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	num, err := r.client.HLen(ctx, cartKey).Result()
	if err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if num < FieldNumber {
		return r.DeleteCart(ctx, userID)
	}

	return r.updateTotalPrice(ctx, cartKey)
}

func (r Repo) UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error {
	const op = "shoppingbasketapp.repository.UpdateQuantity"
	cartKey := r.cartKey(userID)
	itemKey := r.itemKey(productID)

	if err := r.existsCart(ctx, cartKey); err != nil {
		return err
	}

	if err := r.existsItem(ctx, cartKey, itemKey); err != nil {
		return err
	}

	data, err := r.client.HGet(ctx, cartKey, itemKey).Result()
	if err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	var item entity.Item
	if err := json.Unmarshal([]byte(data), &item); err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	item.Quantity = quantity

	j, _ := json.Marshal(item)

	if err := r.client.HSet(ctx, cartKey, itemKey, string(j)).Err(); err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	return r.updateTotalPrice(ctx, cartKey)
}

func (r Repo) DeleteCart(ctx context.Context, userID types.ID) error {
	const op = "shoppingbasketapp.repository.DeleteCart"
	cartKey := r.cartKey(userID)

	if err := r.existsCart(ctx, cartKey); err != nil {
		return err
	}

	if err := r.client.Del(ctx, cartKey).Err(); err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	return nil
}

func (r Repo) updateTotalPrice(ctx context.Context, cartKey string) error {
	const op = "shoppingbasketapp.repository.updateTotalPrice"

	allFields, err := r.client.HGetAll(ctx, cartKey).Result()
	if err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	var total types.Price

	for field, value := range allFields {
		if strings.HasPrefix(field, "item:") {
			var item entity.Item

			if err := json.Unmarshal([]byte(value), &item); err != nil {
				return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
			}

			total += item.Price * types.Price(item.Quantity)
		}
	}

	return r.client.HSet(ctx, cartKey, TotalPriceField, int64(total)).Err()
}

func (r Repo) existsCart(ctx context.Context, cartKey string) error {
	const op = "shoppingbasketapp.repository.existsCart"

	exists, err := r.client.Exists(ctx, cartKey).Result()
	if err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if exists == 0 {
		return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
	}

	return nil
}

func (r Repo) existsItem(ctx context.Context, cartKey, itemKey string) error {
	const op = "shoppingbasketapp.repository.existsCart"

	exists, err := r.client.HExists(ctx, cartKey, itemKey).Result()
	if err != nil {
		return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if !exists {
		return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found product form shopping basket")
	}

	return nil
}
