package service

import (
	"errors"
	"fmt"
	"gorm.io/gorm"
	"net/http"

	"github.com/UpmeshLTD/urus-aio/infrastructure/db/entities"
	gin2 "github.com/UpmeshLTD/urus-aio/infrastructure/server/transport/http/gin"
	"github.com/UpmeshLTD/urus-aio/infrastructure/server/transport/http/gin/middleware"
	"github.com/UpmeshLTD/urus-aio/paid-order/service/dto"
	"github.com/UpmeshLTD/urus-aio/paid-order/service/transform"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"time"
)

type iFulfillment interface {
	GetListOfFulfillments(c *gin.Context)
	GetFulfillment(c *gin.Context)
	GetFulfillmentEstimatedFee(c *gin.Context)

	CreateFulfillment(c *gin.Context)
	CreateFulfillmentFromDraft(c *gin.Context)
	UpdateFulfillment(c *gin.Context)
	DeleteFulfillment(c *gin.Context)
	DeleteOrderFromFulfillment(c *gin.Context)
}

// GetListOfFulfillments godoc
// @Summary Get list of fulfillments.
// @Description Get list of fulfillments.
// @Tags Fulfillment
// @Accept */*
// @Produce json
// @Param userworkspace header string true "User workspace")
// @Param GetFulfillmentsRequest query dto.GetFulfillmentsRequest true "GetFulfillmentsRequest"
// @Success 200 {object} dto.GetFulfillmentsResponse
// @Security BearerToken
// @Router /fulfillments [GET]
func (s *serviceImpl) GetListOfFulfillments(c *gin.Context) {
	req := &dto.GetFulfillmentsRequest{}
	if err := gin2.BindData(c, req, gin2.BindType_Query); err != nil {
		gin2.ResponseBadRequest(c, err)
		return
	}

	result, total, err := s.fulfillmentDomain.GetList(c, middleware.GetWorkspaceId(c), req)
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	gin2.ResponseSuccess(c, &dto.GetFulfillmentsResponse{
		Data:  transform.FulfillmentEntitiesToDTOs(result),
		Total: total,
	})
}

// GetFulfillment godoc
// @Summary Get a fulfillment paid order.
// @Description Get a fulfillment paid order.
// @Tags Fulfillment
// @Accept */*
// @Produce json
// @Param userworkspace header string true "User workspace")
// @Param id path string true "Fulfillment ID"
// @Success 200 {object} dto.Fulfillment
// @Security BearerToken
// @Router /fulfillments/{id} [GET]
func (s *serviceImpl) GetFulfillment(c *gin.Context) {
	req := &dto.GetFulfillmentRequest{}
	if err := gin2.BindData(c, req, gin2.BindType_URI); err != nil {
		gin2.ResponseBadRequest(c, err)
		return
	}

	fulfillment, err := s.fulfillmentDomain.GetWithAssociates(c, req.ID)
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}
	if fulfillment.WorkspaceID != middleware.GetWorkspaceId(c) {
		gin2.ResponseNotFound(c, errors.New("fulfillment not found"))
		return
	}

	cartIds := make([]string, len(fulfillment.PaidOrders))
	for i, v := range fulfillment.PaidOrders {
		cartIds[i] = v.CartID
	}

	deliveryAddresses, err := s.deliveryAddressDomain.GetList(c, cartIds)
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	gin2.ResponseSuccess(c, transform.FulfillmentEntityToDTO(fulfillment, deliveryAddresses))
}

// CreateFulfillment godoc
// @Summary Create fulfillment
// @Description Create fulfillment
// @Tags Fulfillment
// @Accept */*
// @Produce json
// @Param userworkspace header string true "User workspace")
// @Param request body dto.CreateFulfillmentRequest true "CreateFulfillmentRequest"
// @Success 200 {object} dto.CreateFulfillmentResponse
// @Security BearerToken
// @Router /fulfillments [POST]
func (s *serviceImpl) CreateFulfillment(c *gin.Context) {
	req := &dto.CreateFulfillmentRequest{}
	if err := c.BindJSON(req); err != nil {
		gin2.ResponseBadRequest(c, err)
		return
	}

	pos, _, err := s.paidOrderDomain.GetList(c, &entities.GetPaidOrdersParams{
		IDs: req.PaidOrderIDs,
	})
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}
	if len(pos) == 0 {
		gin2.ResponseBadRequest(c, errors.New("cannot mass ship a empty paid orders"))
		return
	}
	if len(pos) > s.config.GetAppConfig().MassShipLimit {
		gin2.ResponseBadRequest(c, errors.New(fmt.Sprintf("The number of orders in one mass-ship cannot exceed %v", s.config.GetAppConfig().MassShipLimit)))
		return
	}

	// Create new fulfillment
	fulfillment, err := s.fulfillmentDomain.Create(c, &entities.Fulfillment{
		WorkspaceID: middleware.GetWorkspaceId(c),
		Step:        1, // Default
		Draft:       true,
	})
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	poIds := make([]string, 0)
	for _, po := range pos {
		poIds = append(poIds, po.ID)
	}

	// Set fulfillment id
	if err := s.paidOrderDomain.ResetAndUpdateNewFulfillmentBatch(c, poIds, fulfillment.ID.String()); err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	gin2.ResponseSuccess(c, dto.CreateFulfillmentResponse{
		FulfillmentID: fulfillment.ID.String(),
	})
}

// CreateFulfillmentFromDraft godoc
// @Summary Create fulfillment from draft
// @Description Create fulfillment from draft
// @Tags Fulfillment
// @Accept */*
// @Produce json
// @Param userworkspace header string true "User workspace")
// @Param id path string true "Fulfillment ID"
// @Param request body dto.CreateFulfillmentFromDraftRequest true "CreateFulfillmentFromDraftRequest"
// @Success 200 {object} dto.CreateFulfillmentFromDraftResponse
// @Security BearerToken
// @Router /fulfillments/{id} [POST]
func (s *serviceImpl) CreateFulfillmentFromDraft(c *gin.Context) {
	hasArrangedShip := false
	workspaceID := middleware.GetWorkspaceId(c)
	req := &dto.CreateFulfillmentFromDraftRequest{}
	if err := gin2.BindData(c, req, gin2.BindType_URI, gin2.BindType_JSON); err != nil {
		gin2.ResponseBadRequest(c, err)
		return
	}

	if len(req.PaidOrderIDs) == 0 {
		gin2.ResponseBadRequest(c, errors.New("can't arrange ship a empty paid orders"))
		return
	}

	if len(req.PaidOrderIDs) > s.config.GetAppConfig().MassShipLimit {
		gin2.ResponseBadRequest(c, fmt.Errorf("the number of paid orders can't exceed %v", s.config.GetAppConfig().MassShipLimit))
		return
	}

	// Get draft fulfillment
	draft, err := s.fulfillmentDomain.Get(c, req.FulfillmentID)
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	// Check owner
	if draft.WorkspaceID != workspaceID {
		gin2.ResponseBadRequest(c, errors.New("fulfillment not found"))
		return
	}

	// Get paid_orders of the draft by fulfillment_id
	draftPaidOrders, _, err := s.paidOrderDomain.GetList(c, &entities.GetPaidOrdersParams{
		FulfillmentIDs:  []string{req.FulfillmentID},
		WorkspaceID:     &workspaceID,
		HasArrangedShip: &hasArrangedShip,
	})
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	if len(draftPaidOrders) == 0 {
		gin2.ResponseBadRequest(c, errors.New("draft paid orders empty"))
		return
	}

	// If the draft has only one cart
	if len(draftPaidOrders) == 1 {
		gin2.ResponseSuccess(c, dto.CreateFulfillmentFromDraftResponse{
			FulfillmentID: draft.ID.String(),
		})
		return
	}

	// Get selected paid_orders by paid_order_ids
	paidOrders, _, err := s.paidOrderDomain.GetList(c, &entities.GetPaidOrdersParams{
		IDs:             req.PaidOrderIDs,
		WorkspaceID:     &workspaceID,
		HasArrangedShip: &hasArrangedShip,
	})
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	if len(paidOrders) == 0 {
		gin2.ResponseBadRequest(c, errors.New("selected paid orders empty"))
		return
	}

	// Create new fulfillment from draft
	draft.ID = uuid.Nil
	draft.CreatedAt = time.Now()
	draft.UpdatedAt = time.Now()
	fulfillment, err := s.fulfillmentDomain.Create(c, draft)
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	fulfillmentID := fulfillment.ID.String()

	// Set new fulfillment_id to paid_orders by paid_order_ids
	if err := s.paidOrderDomain.UpdateFulfillmentIDBatch(c, func() (arr []string) {
		for _, v := range paidOrders {
			arr = append(arr, v.ID)
		}
		return arr
	}(), &fulfillmentID); err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	gin2.ResponseSuccess(c, dto.CreateFulfillmentFromDraftResponse{
		FulfillmentID: fulfillmentID,
	})
}

// UpdateFulfillment godoc
// @Summary Update a fulfillment
// @Description Update a fulfillment
// @Tags Fulfillment
// @Accept json
// @Produce json
// @Param userworkspace header string true "User workspace")
// @Param id path string true "Fulfillment ID"
// @Param request body dto.UpdateFulfillmentRequest true "UpdateFulfillmentRequest"
// @Success 204
// @Security BearerToken
// @Router /fulfillments/{id} [PATCH]
func (s *serviceImpl) UpdateFulfillment(c *gin.Context) {
	req := &dto.UpdateFulfillmentRequest{}
	if err := gin2.BindData(c, &req, gin2.BindType_JSON, gin2.BindType_URI); err != nil {
		gin2.ResponseErrorWithCode(c, http.StatusBadRequest, err)
		return
	}

	if req.Step == entities.FulfillmentStepOrderDetails {
		for _, v := range req.OrderDetails {
			if v.Weight <= 0 {
				gin2.ResponseBadRequest(c, errors.New("weight must be greater than 0"))
				return
			}
			if (v.Width == nil && v.Height == nil && v.Length == nil) ||
				(v.Width != nil && *v.Width > 0 &&
					v.Height != nil && *v.Height > 0 &&
					v.Length != nil && *v.Length > 0) {
				continue
			}

			gin2.ResponseBadRequest(c, errors.New("invalid dimension"))
			return
		}
	}

	f, err := s.fulfillmentDomain.Get(c, req.ID)
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}
	if f.WorkspaceID != middleware.GetWorkspaceId(c) {
		gin2.ResponseNotFound(c, errors.New("fulfillment not found"))
		return
	}

	if err := s.fulfillmentDomain.Update(c, f, transform.UpdateFulfillmentRequestDTOToEntity(req)); err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	if req.Step == entities.FulfillmentStepOrderDetails {
		for _, v := range req.OrderDetails {
			if len(v.ID) == 0 {
				po, err := s.paidOrderDomain.Get(c, v.CartID, v.ShippingGroupID)
				if err != nil {
					if err == gorm.ErrRecordNotFound {
						gin2.ResponseBadRequest(c, errors.New(fmt.Sprintf("cart %s not found", v.CartID)))
						return
					}
					gin2.ResponseInternalServerError(c, err)
					return
				}
				v.ID = po.ID
			}
			if err := s.paidOrderDomain.UpdateDimension(c, v.ID, entities.WeightDimension{
				Weight: v.Weight,
				Width:  v.Width,
				Length: v.Length,
				Height: v.Height,
			}); err != nil {
				gin2.ResponseInternalServerError(c, err)
				return
			}
		}
	}

	gin2.ResponseNoContent(c)
}

// DeleteFulfillment godoc
// @Summary Delete a fulfillment
// @Description Delete a fulfillment
// @Tags Fulfillment
// @Accept json
// @Produce json
// @Param userworkspace header string true "User workspace")
// @Param id path string true "Fulfillment ID"
// @Success 204
// @Security BearerToken
// @Router /fulfillments/{id} [DELETE]
func (s *serviceImpl) DeleteFulfillment(c *gin.Context) {
	req := &dto.DeleteFulfillmentRequest{}
	if err := gin2.BindData(c, req, gin2.BindType_URI); err != nil {
		gin2.ResponseBadRequest(c, err)
		return
	}

	workspaceID := middleware.GetWorkspaceId(c)
	fulfillment, err := s.fulfillmentDomain.Get(getCtx(c), req.ID)
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}
	if fulfillment.WorkspaceID != workspaceID {
		gin2.ResponseNotFound(c, errors.New("fulfillment not found"))
		return
	}

	pos, _, err := s.paidOrderDomain.GetList(c, &entities.GetPaidOrdersParams{
		FulfillmentIDs: []string{fulfillment.ID.String()},
	})

	if err := s.paidOrderDomain.UpdateFulfillmentIDBatch(
		getCtx(c),
		func() []string {
			ids := make([]string, 0)
			for _, po := range pos {
				ids = append(ids, po.ID)
			}
			return ids
		}(),
		nil,
	); err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	if err = s.fulfillmentDomain.Delete(getCtx(c), req.ID); err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	gin2.ResponseNoContent(c)
}

// DeleteOrderFromFulfillment godoc
// @Summary Delete order from fulfillment
// @Description Delete order from fulfillment
// @Tags Fulfillment
// @Accept json
// @Produce json
// @Param userworkspace header string true "User workspace")
// @Param id path string true "Fulfillment ID"
// @Param paidOrderId path string true "Paid order ID"
// @Success 204
// @Security BearerToken
// @Router /fulfillments/{id}/orders/{paidOrderId} [DELETE]
func (s *serviceImpl) DeleteOrderFromFulfillment(c *gin.Context) {
	req := &dto.DeleteOrderFromFulfillmentRequest{}
	if err := c.BindUri(req); err != nil {
		gin2.ResponseBadRequest(c, err)
		return
	}

	f, err := s.fulfillmentDomain.Get(getCtx(c), req.FulfillmentID)
	if err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}
	if f.WorkspaceID != middleware.GetWorkspaceId(c) {
		gin2.ResponseNotFound(c, errors.New("fulfillment not found"))
		return
	}

	if err := s.paidOrderDomain.UpdateFulfillmentID(c, req.PaidOrderID, nil); err != nil {
		gin2.ResponseInternalServerError(c, err)
		return
	}

	gin2.ResponseNoContent(c)
}

// GetFulfillmentEstimatedFee godoc
// @Summary Get estimated fee
// @Description Get estimated fee
// @Tags Fulfillment
// @Accept */*
// @Produce json
// @Param userworkspace header string true "User workspace")
// @Param request body dto.GetFulfillmentEstimatedFeeRequest true "GetFulfillmentEstimatedFeeRequest"
// @Success 200 {object} dto.GetFulfillmentEstimatedFeeResponse
// @Security BearerToken
// @Router /fulfillments/estimated-fee [GET]
func (s *serviceImpl) GetFulfillmentEstimatedFee(c *gin.Context) {
	req := &dto.GetFulfillmentEstimatedFeeRequest{}
	if err := c.BindJSON(req); err != nil {
		gin2.ResponseBadRequest(c, err)
		return
	}

	fees := s.fulfillmentDomain.GetFulfillmentEstimatedFees(c, req.CourierDeliveryPlanID)
	dimensions := make([]dto.GetFulfillmentEstimatedFeeDimension, 0)
	totalFee := float64(0)

	for _, v := range req.Dimensions {
		fee := s.fulfillmentDomain.CalculateEstimatedFee(fees, &entities.WeightDimension{
			Width:  &v.Width,
			Length: &v.Length,
			Height: &v.Height,
			Weight: v.Weight,
		})
		dimensions = append(dimensions, dto.GetFulfillmentEstimatedFeeDimension{
			ID:           v.ID,
			Width:        v.Width,
			Length:       v.Length,
			Height:       v.Height,
			Weight:       v.Weight,
			EstimatedFee: fee,
		})
		totalFee += fee
	}
	gin2.ResponseSuccess(c, dto.GetFulfillmentEstimatedFeeResponse{
		CourierDeliveryPlanID: req.CourierDeliveryPlanID,
		Dimensions:            dimensions,
		TotalFee:              totalFee,
	})
}
