package repository

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
	"git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
	richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
	"git.gocasts.ir/ebhomengo/niki/types"
	"time"
)

type DB struct {
	db     *mysql.DB
	config Config
}

func NewDB(db *mysql.DB, cfg Config) DB {
	return DB{db: db, config: cfg}
}

func (d DB) getCart(ctx context.Context, tx *sql.Tx, query string, by types.ID) (entity.Cart, error) {
	t, ok, err := d.getLocalTX(ctx, tx, "mysql-getCart")
	if err != nil {
		return entity.Cart{}, err
	}

	if ok {
		defer func() {
			_ = tx.Rollback()
		}()
	}

	var c entity.Cart

	err = t.QueryRowContext(ctx, query, by).
		Scan(&c.ID, &c.UserID, &c.TotalPrice, &c.Status, &c.ExpireAt, &c.CreatedAt)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
				WithKind(richerror.KindNotFound).
				WithErr(ErrNotExistsCart)
		}

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

	}

	if c.ExpireAt.Before(time.Now().UTC()) {
		if err := d.updateCartStatus(ctx, t, c.ID, entity.CartStatusExpired); err != nil {
			return entity.Cart{}, err
		}
		return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
			WithKind(richerror.KindInvalid).WithErr(ErrExpiredCart)
	}

	itemsQuery := `SELECT id, cart_id, user_id, product_id, quantity, price, name, added_at, updated_at
					FROM items
					WHERE cart_id=? AND deleted_at IS NULL
					ORDER BY added_at`

	rows, err := t.QueryContext(ctx, itemsQuery, c.ID)
	if err != nil {
		return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
			WithKind(richerror.KindUnexpected).
			WithErr(err)
	}

	defer rows.Close()

	items := make([]entity.Item, 0)
	for rows.Next() {
		var i entity.Item
		if err := rows.Scan(&i.ID, &i.CartID, &i.UserID, &i.ProductID,
			&i.Quantity, &i.Price, &i.Name, &i.AddedAt, &i.UpdatedAt); err != nil {

			return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
				WithKind(richerror.KindUnexpected).
				WithErr(fmt.Errorf("error scan rows: %w", err))
		}

		items = append(items, i)
	}

	if err := rows.Err(); err != nil {
		return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
			WithKind(richerror.KindUnexpected).
			WithErr(err)
	}

	c.Items = items

	if ok {
		if err := tx.Commit(); err != nil {
			return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
				WithKind(richerror.
					KindUnexpected).WithErr(err)
		}
	}

	return c, nil
}

func (d DB) addToBasket(ctx context.Context, userID types.ID, item entity.Item) error {
	tx, err := d.db.Conn().BeginTx(ctx, &sql.TxOptions{
		Isolation: sql.LevelReadCommitted,
	})
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-addToBasket"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	defer func() {
		_ = tx.Rollback()
	}()

	c, err := d.getCart(ctx, tx, FindCartByUserIDQuery, userID)
	if err != nil {
		if err.Error() == ErrExpiredCart.Error() || err.Error() == ErrNotExistsCart.Error() {
			cartQuery := `INSERT INTO carts user_id, total_price, status, expire_at, created_at
						  VALUES (?,?,?,?)`

			newCart := entity.Cart{
				UserID:     userID,
				TotalPrice: item.Price * float64(item.Quantity),
				Status:     entity.CartStatusActive,
				CreatedAt:  time.Now().UTC(),
				ExpireAt:   time.Now().Add(d.config.MysqlTTL).UTC(),
			}

			result, err := tx.ExecContext(ctx, cartQuery,
				newCart.UserID, newCart.TotalPrice, newCart.Status, newCart.ExpireAt, newCart.CreatedAt)
			if err != nil {
				return richerror.New(richerror.Op(op("mysql-addToBasket"))).
					WithKind(richerror.KindUnexpected).
					WithErr(err)
			}

			cartID, _ := result.LastInsertId()
			item.CartID = types.ID(cartID)

			if err := d.addItem(ctx, tx, item); err != nil {
				return err
			}

			if err := d.updateTotalPrice(ctx, tx, types.ID(cartID)); err != nil {
				return err
			}
		}

		return err
	}

	isNew := true
	if c.ID > 0 {
		for _, i := range c.Items {
			if i.ProductID == item.ProductID {
				item.Quantity = i.Quantity + item.Quantity
				if err := d.updateQuantity(ctx, tx, item); err != nil {
					return err
				}

				isNew = false
			}
		}
	}

	if isNew {
		err := d.addItem(ctx, tx, item)
		if err != nil {
			return err
		}
	}

	if err := d.updateTotalPrice(ctx, tx, c.ID); err != nil {
		return err
	}

	if err := tx.Commit(); err != nil {
		return richerror.New(richerror.Op(op("mysql-addToBasket"))).
			WithKind(richerror.KindUnexpected).
			WithErr(err)
	}

	return nil
}

func (d DB) addItem(ctx context.Context, tx *sql.Tx, item entity.Item) error {
	t, ok, err := d.getLocalTX(ctx, tx, "mysql-addItem")
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-addItem"))).
			WithKind(richerror.KindUnexpected).
			WithErr(err)
	}

	if ok {
		defer func() {
			_ = tx.Rollback()
		}()
	}

	query := `INSERT INTO items cart_id, user_id, product_id, quantity, price, name, added_at
	          VALUES (?,?,?,?,?,?,?)`

	_, err = t.ExecContext(ctx, query,
		item.CartID, item.UserID, item.ProductID, item.Quantity, item.Price, item.Name, item.AddedAt)
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-addItem"))).
			WithKind(richerror.KindUnexpected).
			WithErr(err)
	}

	if ok {
		if err := tx.Commit(); err != nil {
			return richerror.New(richerror.Op(op("mysql-addItem"))).
				WithKind(richerror.KindUnexpected).
				WithErr(err)
		}
	}

	return nil
}

func (d DB) updateQuantity(ctx context.Context, tx *sql.Tx, item entity.Item) error {
	t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateQuantity")
	if err != nil {
		return err
	}

	if ok {
		defer func() {
			_ = tx.Rollback()
		}()
	}

	query := `UPDATE items
            SET
                quantity = ?
            WHERE  id = ?`

	_, err = t.ExecContext(ctx, query, item.Quantity, item.ID)
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-updateQuantity"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if ok {
		if err := t.Commit(); err != nil {
			return richerror.New(richerror.Op(op("mysql-updateQuantity"))).
				WithKind(richerror.KindUnexpected).WithErr(err)
		}
	}

	return nil
}

func (d DB) updateTotalPrice(ctx context.Context, tx *sql.Tx, cartID types.ID) error {
	t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateTotalPrice")
	if err != nil {
		return err
	}

	if ok {
		defer func() {
			_ = tx.Rollback()
		}()
	}

	c, err := d.getCart(ctx, t, FindCartByIDQuery, cartID)
	if err != nil {
		return err
	}

	var totalPrice float64

	for _, i := range c.Items {
		totalPrice += i.Price * float64(i.Quantity)
	}

	query := `UPDATE carts
             SET
                 total_price = ?
             WHERE cart_id=?`

	_, err = tx.ExecContext(ctx, query, totalPrice, c.ID)
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-updateTotalPrice"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if ok {
		if err := tx.Commit(); err != nil {
			return richerror.New(richerror.Op(op("mysql-updateTotalPrice"))).
				WithKind(richerror.KindUnexpected).WithErr(err)
		}
	}

	return nil
}

func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, itemID types.ID) error {
	t, ok, err := d.getLocalTX(ctx, tx, "mysql-deleteItem")
	if err != nil {
		return err
	}

	if ok {
		defer func() {
			_ = tx.Rollback()
		}()
	}

	query := `UPDATE items
              SET 
                  deleted_at = ?
              WHERE cart_id = ? AND id = ? AND deleted_at IS NULL`
	deleteTime := time.Now().UTC()
	_, err = t.ExecContext(ctx, query, deleteTime, cartID, itemID)
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-deleteItem"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if err := d.updateTotalPrice(ctx, t, cartID); err != nil {
		return err
	}

	if ok {
		if err := t.Commit(); err != nil {
			return richerror.New(richerror.Op(op("mysql-deleteItem"))).
				WithKind(richerror.KindUnexpected).WithErr(err)
		}
	}

	return nil
}

func (d DB) deleteCart(ctx context.Context, cartID, userID types.ID) error {
	itemQuery := `UPDATE items
              SET 
                  deleted_at = ?
              WHERE cart_id = ? AND user_id = ? AND deleted_at IS NULL`

	deleteItemTime := time.Now().UTC()

	_, err := d.db.Conn().ExecContext(ctx, itemQuery, deleteItemTime, cartID, userID)
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-deleteCart"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	cartQuery := `UPDATE carts
              SET 
                  deleted_at = ?
              WHERE id = ? AND user_id = ? AND deleted_at IS NULL`

	deleteCartTime := time.Now().UTC()

	_, err = d.db.Conn().ExecContext(ctx, cartQuery, deleteCartTime, cartID, userID)
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-deleteCart"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	return nil
}

func (d DB) updateCartStatus(ctx context.Context, tx *sql.Tx, cartID types.ID, status entity.CartStatus) error {
	t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateCartStatus")
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if ok {
		defer func() {
			_ = t.Rollback()
		}()
	}

	query := `UPDATE carts
             SET 
                 status = ?
              WHERE id = ? AND status = active AND deleted_at IS NULL`

	_, err = t.ExecContext(ctx, query, status, cartID)
	if err != nil {
		return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
			WithKind(richerror.KindUnexpected).WithErr(err)
	}

	if ok {
		if err := t.Commit(); err != nil {
			return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
				WithKind(richerror.KindUnexpected).WithErr(err)
		}
	}

	return nil
}

func (d DB) getLocalTX(ctx context.Context, tx *sql.Tx, p string) (*sql.Tx, bool, error) {
	var err error
	var ok bool
	if tx == nil {
		tx, err = d.db.Conn().BeginTx(ctx, &sql.TxOptions{
			Isolation: sql.LevelReadCommitted,
		})
		if err != nil {
			return nil, false, richerror.New(richerror.Op(op(fmt.Sprintf("%s", p)))).
				WithKind(richerror.KindUnexpected).
				WithErr(err)
		}

		ok = true
	}

	return tx, ok, nil
}
