package service

import (
	"context"
	"fmt"
	"slices"

	"route256/loms/internal/domain/entity"
	"route256/loms/internal/domain/model"

	pb "route256/pkg/api/loms/v1"

	"github.com/rs/zerolog/log"
)

//go:generate minimock -i OrderRepository -o ./mock -s _mock.go
type OrderRepository interface {
	OrderCreate(ctx context.Context, order *entity.Order) (entity.ID, error)
	OrderSetStatus(ctx context.Context, orderID entity.ID, newStatus string) error
	OrderGetByID(ctx context.Context, orderID entity.ID) (*entity.Order, error)
}

//go:generate minimock -i StockRepository -o ./mock -s _mock.go
type StockRepository interface {
	StockReserve(ctx context.Context, stock *entity.Stock) error
	StockReserveRemove(ctx context.Context, stock *entity.Stock) error
	StockCancel(ctx context.Context, stock *entity.Stock) error
	StockGetByID(ctx context.Context, sku entity.Sku) (*entity.Stock, error)
}

type txManager interface {
	WriteWithTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error)
	ReadWithTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error)
	WriteWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error)
	ReadWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error)
}

type LomsService struct {
	orders    OrderRepository
	stocks    StockRepository
	txManager txManager
}

func NewLomsService(orderRepo OrderRepository, stockRepo StockRepository, txManager txManager) *LomsService {
	return &LomsService{
		orders:    orderRepo,
		stocks:    stockRepo,
		txManager: txManager,
	}
}

func (s *LomsService) rollbackStocks(ctx context.Context, stocks []*entity.Stock) {
	for _, stock := range stocks {
		if err := s.stocks.StockCancel(ctx, stock); err != nil {
			log.Error().Err(err).Msg("failed to rollback stock")
		}
	}
}

func (s *LomsService) OrderCreate(ctx context.Context, orderReq *pb.OrderCreateRequest) (entity.ID, error) {
	if orderReq == nil || orderReq.UserId <= 0 || len(orderReq.Items) == 0 {
		return 0, model.ErrInvalidInput
	}

	for _, item := range orderReq.Items {
		if item.Sku <= 0 || item.Count == 0 {
			return 0, model.ErrInvalidInput
		}
	}

	order := &entity.Order{
		OrderID: 0,
		Status:  pb.OrderStatus_ORDER_STATUS_NEW.String(),
		UserID:  entity.ID(orderReq.UserId),
		Items:   make([]entity.OrderItem, len(orderReq.Items)),
	}

	for i, item := range orderReq.Items {
		order.Items[i] = entity.OrderItem{
			ID:    entity.Sku(item.Sku),
			Count: item.Count,
		}
	}

	slices.SortStableFunc(order.Items, func(a, b entity.OrderItem) int {
		return int(a.ID - b.ID)
	})

	var (
		orderID entity.ID
		resErr  error
	)

	err := s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
		id, err := s.orders.OrderCreate(txCtx, order)
		if err != nil {
			return err
		}
		order.OrderID = id
		orderID = id

		committed := make([]*entity.Stock, 0, len(order.Items))
		for _, it := range order.Items {
			st := &entity.Stock{Item: it, Reserved: it.Count}
			if err := s.stocks.StockReserve(txCtx, st); err != nil {
				s.rollbackStocks(txCtx, committed)

				_ = s.orders.OrderSetStatus(txCtx, id,
					pb.OrderStatus_ORDER_STATUS_FAILED.String())

				resErr = fmt.Errorf("stocks.StockReserve: %w", err)
				return nil
			}
			committed = append(committed, st)
		}

		return s.orders.OrderSetStatus(txCtx, id,
			pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String())
	})
	if err != nil {
		return 0, err
	}
	if resErr != nil {
		return 0, resErr
	}
	return orderID, nil
}

func (s *LomsService) OrderInfo(ctx context.Context, orderID entity.ID) (*entity.Order, error) {
	if orderID <= 0 {
		return nil, model.ErrInvalidInput
	}

	return s.orders.OrderGetByID(ctx, orderID)
}

func (s *LomsService) OrderPay(ctx context.Context, orderID entity.ID) error {
	if orderID <= 0 {
		return model.ErrInvalidInput
	}

	return s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
		order, err := s.orders.OrderGetByID(txCtx, orderID)
		if err != nil {
			return err
		}

		switch order.Status {
		case pb.OrderStatus_ORDER_STATUS_PAYED.String():
			return nil
		case pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String():
			for _, it := range order.Items {
				if err := s.stocks.StockReserveRemove(txCtx, &entity.Stock{
					Item:     it,
					Reserved: it.Count,
				}); err != nil {
					log.Error().Err(err).Msg("failed to free stock reservation")
				}
			}
			return s.orders.OrderSetStatus(txCtx, orderID,
				pb.OrderStatus_ORDER_STATUS_PAYED.String())
		default:
			return model.ErrOrderInvalidStatus
		}
	})
}

func (s *LomsService) OrderCancel(ctx context.Context, orderID entity.ID) error {
	if orderID <= 0 {
		return model.ErrInvalidInput
	}

	return s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
		order, err := s.orders.OrderGetByID(txCtx, orderID)
		if err != nil {
			return err
		}

		switch order.Status {
		case pb.OrderStatus_ORDER_STATUS_CANCELLED.String():
			return nil
		case pb.OrderStatus_ORDER_STATUS_FAILED.String(),
			pb.OrderStatus_ORDER_STATUS_PAYED.String():
			return model.ErrOrderInvalidStatus
		}

		for _, it := range order.Items {
			if err := s.stocks.StockCancel(txCtx, &entity.Stock{
				Item:     it,
				Reserved: it.Count,
			}); err != nil {
				return err
			}
		}
		return s.orders.OrderSetStatus(txCtx, orderID,
			pb.OrderStatus_ORDER_STATUS_CANCELLED.String())
	})
}

func (s *LomsService) StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error) {
	if sku <= 0 {
		return 0, model.ErrInvalidInput
	}

	stock, err := s.stocks.StockGetByID(ctx, sku)
	if err != nil {
		return 0, err
	}

	return stock.Item.Count - stock.Reserved, nil
}
