package service

import (
	"context"
	"fmt"
	"slices"

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

	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 StatusProducer interface {
	Send(ctx context.Context, id entity.ID, status string) error
}

type LomsService struct {
	orders         OrderRepository
	stocks         StockRepository
	txManager      txManager
	statusProducer StatusProducer
}

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

func (s *LomsService) rollbackStocks(ctx context.Context, stocks []*entity.Stock) {
	ctx, span := tracing.Tracer().Start(ctx, "rollbackStocks")
	defer span.End()

	for _, stock := range stocks {
		if err := s.stocks.StockCancel(ctx, stock); err != nil {
			log.Error().Err(err).Msg("failed to rollback stock")
		}
	}
}

// Wraps writing status to DB and status topic.
// Should use this function for status updates.
// Guarantees that status upd event will be sent only if DB write is successful.
func (s *LomsService) setStatus(ctx context.Context, id entity.ID, status string) error {
	log.Trace().Msgf("running status update for %d with status %s", id, status)

	if err := s.orders.OrderSetStatus(ctx, id, status); err != nil {
		return fmt.Errorf("orders.OrderSetStatus: %w", err)
	}

	if err := s.statusProducer.Send(ctx, id, status); err != nil {
		log.Error().Err(err).Msg("statusProducer.Send")
	}

	return nil
}

func (s *LomsService) createInitial(ctx context.Context, orderReq *pb.OrderCreateRequest) (*entity.Order, error) {
	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)
	})

	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

		return nil
	})
	if err != nil {
		return nil, err
	}

	return order, nil
}

func (s *LomsService) OrderCreate(ctx context.Context, orderReq *pb.OrderCreateRequest) (entity.ID, error) {
	ctx, span := tracing.Tracer().Start(ctx, "OrderCreate")
	defer span.End()

	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, err := s.createInitial(ctx, orderReq)
	if err != nil {
		return 0, err
	}

	if statErr := s.setStatus(ctx, order.OrderID, order.Status); statErr != nil {
		return 0, statErr
	}

	var resErr error

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

				resErr = fmt.Errorf("stocks.StockReserve: %w", resErr)

				return nil
			}
			committed = append(committed, st)
		}

		return nil
	})

	finalStatus := pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String()

	defer func() {
		if statErr := s.setStatus(ctx, order.OrderID, finalStatus); statErr != nil {
			log.Error().Err(statErr).Msgf("failed to setStatus to %s", finalStatus)
		}
	}()

	if err != nil {
		finalStatus = pb.OrderStatus_ORDER_STATUS_FAILED.String()

		return 0, err
	}
	if resErr != nil {
		finalStatus = pb.OrderStatus_ORDER_STATUS_FAILED.String()

		return 0, resErr
	}

	return order.OrderID, nil
}

func (s *LomsService) OrderInfo(ctx context.Context, orderID entity.ID) (*entity.Order, error) {
	ctx, span := tracing.Tracer().Start(ctx, "OrderInfo")
	defer span.End()

	if orderID <= 0 {
		return nil, model.ErrInvalidInput
	}

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

func (s *LomsService) OrderPay(ctx context.Context, orderID entity.ID) error {
	ctx, span := tracing.Tracer().Start(ctx, "OrderPay")
	defer span.End()

	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.setStatus(txCtx, orderID,
				pb.OrderStatus_ORDER_STATUS_PAYED.String())
		default:
			return model.ErrOrderInvalidStatus
		}
	})
}

func (s *LomsService) OrderCancel(ctx context.Context, orderID entity.ID) error {
	ctx, span := tracing.Tracer().Start(ctx, "OrderCancel")
	defer span.End()

	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.setStatus(txCtx, orderID,
			pb.OrderStatus_ORDER_STATUS_CANCELLED.String())
	})
}

func (s *LomsService) StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error) {
	ctx, span := tracing.Tracer().Start(ctx, "StocksInfo")
	defer span.End()

	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
}
