package service

import (
	"context"
	"errors"
	"fmt"
	"math"
	"strings"

	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
	customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
	warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
	projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/sirupsen/logrus"
	"gorm.io/gorm"
)

type SalesOrdersService interface {
	CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
	Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error)
}

type salesOrdersService struct {
	Log                     *logrus.Logger
	Validate                *validator.Validate
	MarketingRepo           repository.MarketingRepository
	CustomerRepo            customerRepo.CustomerRepository
	ProductWarehouseRepo    productWarehouseRepo.ProductWarehouseRepository
	UserRepo                userRepo.UserRepository
	ApprovalSvc             commonSvc.ApprovalService
	FifoStockV2Svc          commonSvc.FifoStockV2Service
	WarehouseRepo           warehouseRepo.WarehouseRepository
	ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
}

func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, warehouseRepo warehouseRepo.WarehouseRepository,
	projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
	return &salesOrdersService{
		Log:                     utils.Log,
		Validate:                validate,
		MarketingRepo:           marketingRepo,
		CustomerRepo:            customerRepo,
		ProductWarehouseRepo:    productWarehouseRepo,
		UserRepo:                userRepo,
		ApprovalSvc:             approvalSvc,
		FifoStockV2Svc:          fifoStockV2Svc,
		WarehouseRepo:           warehouseRepo,
		ProjectFlockKandangRepo: projectFlockKandangRepo,
	}
}

func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB {
	return db.
		Preload("CreatedUser").
		Preload("Customer").
		Preload("SalesPerson").
		Preload("Products.ProductWarehouse.Product.Flags").
		Preload("Products.ProductWarehouse.Product.Uom").
		Preload("Products.ProductWarehouse.Warehouse")
}

func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) {
	if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
		return nil, err
	}

	marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
	}
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
	}

	if s.ApprovalSvc != nil {
		approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB {
			return db.Preload("ActionUser")
		})
		if err == nil && len(approvals) > 0 {
			latest := approvals[len(approvals)-1]
			marketing.LatestApproval = &latest
		}
	}

	return marketing, nil
}

func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

	// Validasi semua product harus punya marketing_type yang sama
	if len(req.MarketingProducts) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "marketing_products is required")
	}

	firstMarketingType := req.MarketingProducts[0].MarketingType
	if !utils.IsValidMarketingType(firstMarketingType) {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Tipe penjualan tidak valid")
	}

	for i, item := range req.MarketingProducts {
		if !utils.IsValidMarketingType(item.MarketingType) {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tipe penjualan tidak valid pada produk ke-%d", i+1))
		}
		if item.MarketingType != firstMarketingType {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Semua produk harus memiliki tipe penjualan yang sama")
		}
	}

	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return nil, err
	}

	if err := commonSvc.EnsureRelations(c.Context(),
		commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists},
	); err != nil {
		return nil, err
	}

	for _, item := range req.MarketingProducts {
		if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi")
		}
		if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Unit konversi tidak valid")
		}
		if item.MarketingType == string(utils.MarketingTypeTelur) &&
			item.ConvertionUnit != nil &&
			*item.ConvertionUnit == string(utils.ConvertionUnitPeti) &&
			(item.WeightPerConvertion == nil || *item.WeightPerConvertion <= 0) {
			return nil, fiber.NewError(fiber.StatusBadRequest, "weight_per_convertion wajib diisi dan > 0 untuk TELUR dengan convertion_unit PETI")
		}
		if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
			return nil, err
		}
		if err := commonSvc.EnsureRelations(c.Context(),
			commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
		); err != nil {
			return nil, err
		}
	}

	requestedByWarehouse := make(map[uint]float64)
	for _, item := range req.MarketingProducts {
		if item.ProductWarehouseId == 0 {
			continue
		}
		requestedByWarehouse[item.ProductWarehouseId] += item.Qty
	}

	for pwID, requestedQty := range requestedByWarehouse {
		productWarehouse, err := s.ProductWarehouseRepo.GetDetailByID(c.Context(), pwID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", pwID))
			}
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock availability")
		}
		availableQty := productWarehouse.Quantity
		if availableQty+1e-6 < requestedQty {
			return nil, fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf("Stok tidak mencukupi untuk gudang %d: diminta %.3f, tersedia %.3f", pwID, requestedQty, availableQty),
			)
		}
	}

	soDate, err := utils.ParseDateString(req.Date)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
	}

	soNumber, err := s.MarketingRepo.NextSoNumber(context.Background(), nil)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number")
	}

	var marketing *entity.Marketing
	err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {

		marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
		marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
		invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
		approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))

		marketing = &entity.Marketing{
			CustomerId:    req.CustomerId,
			SoNumber:      soNumber,
			SoDate:        soDate,
			SalesPersonId: req.SalesPersonId,
			Notes:         req.Notes,
			MarketingType: firstMarketingType,
			CreatedBy:     actorID,
		}
		if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders")
		}

		if len(req.MarketingProducts) > 0 {
			pwIDs := make([]uint, 0, len(req.MarketingProducts))
			for _, product := range req.MarketingProducts {
				if product.ProductWarehouseId != 0 {
					pwIDs = append(pwIDs, product.ProductWarehouseId)
				}
				if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product.MarketingType, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
					return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
				}
			}
			if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
				return err
			}
		}

		approvalAction := entity.ApprovalActionCreated
		if _, err := approvalSvcTx.CreateApproval(
			c.Context(),
			utils.ApprovalWorkflowMarketing,
			marketing.Id,
			utils.MarketingStepPengajuan,
			&approvalAction,
			actorID,
			nil); err != nil {
			if !errors.Is(err, gorm.ErrDuplicatedKey) {
				fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
			}
		}

		return nil
	})
	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders")
	}

	marketing, err = s.MarketingRepo.GetByID(c.Context(), marketing.Id, s.withRelations)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created sales order")
	}

	return marketing, nil
}

func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

	// Validasi semua product harus punya marketing_type yang sama
	if len(req.MarketingProducts) > 0 {
		firstMarketingType := req.MarketingProducts[0].MarketingType
		if !utils.IsValidMarketingType(firstMarketingType) {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Tipe penjualan tidak valid")
		}

		for i, item := range req.MarketingProducts {
			if !utils.IsValidMarketingType(item.MarketingType) {
				return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tipe penjualan tidak valid pada produk ke-%d", i+1))
			}
			if item.MarketingType != firstMarketingType {
				return nil, fiber.NewError(fiber.StatusBadRequest, "Semua produk harus memiliki tipe penjualan yang sama")
			}
		}
	}

	if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
		return nil, err
	}

	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return nil, err
	}

	if err := commonSvc.EnsureRelations(c.Context(),
		commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
		commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists},
		commonSvc.RelationCheck{Name: "SalesPerson", ID: &req.SalesPersonId, Exists: s.UserRepo.IdExists},
	); err != nil {
		return nil, err
	}

	latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
	}
	if latestApproval != nil && latestApproval.StepNumber >= 3 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Cannot update sales order after delivery order approval")
	}

	if len(req.MarketingProducts) > 0 {
		for _, item := range req.MarketingProducts {
			if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
				return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi")
			}
			if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) {
				return nil, fiber.NewError(fiber.StatusBadRequest, "Unit konversi tidak valid")
			}
			if item.MarketingType == string(utils.MarketingTypeTelur) &&
				item.ConvertionUnit != nil &&
				*item.ConvertionUnit == string(utils.ConvertionUnitPeti) &&
				(item.WeightPerConvertion == nil || *item.WeightPerConvertion <= 0) {
				return nil, fiber.NewError(fiber.StatusBadRequest, "weight_per_convertion wajib diisi dan > 0 untuk TELUR dengan convertion_unit PETI")
			}
			if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
				return nil, err
			}
			if err := commonSvc.EnsureRelations(c.Context(),
				commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
			); err != nil {
				return nil, err
			}
		}
	}
	if len(req.MarketingProducts) > 0 {
		pwIDs := make([]uint, 0, len(req.MarketingProducts))
		for _, item := range req.MarketingProducts {
			if item.ProductWarehouseId != 0 {
				pwIDs = append(pwIDs, item.ProductWarehouseId)
			}
		}

		if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
			return nil, err
		}
	}

	err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {

		marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
		marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
		approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
		invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)

		updateBody := make(map[string]any)
		if req.CustomerId != 0 {
			updateBody["customer_id"] = req.CustomerId
		}
		if req.SalesPersonId != 0 {
			updateBody["sales_person_id"] = req.SalesPersonId
		}
		if req.Date != "" {
			soDate, err := utils.ParseDateString(req.Date)
			if err != nil {
				return fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
			}
			updateBody["so_date"] = soDate
		}
		if req.Notes != "" {
			updateBody["notes"] = req.Notes
		}
		if len(req.MarketingProducts) > 0 {
			updateBody["marketing_type"] = req.MarketingProducts[0].MarketingType
		}

		if len(updateBody) > 0 {
			if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order")
			}
		}

		if len(req.MarketingProducts) > 0 {

			oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id)
			if err != nil && err != gorm.ErrRecordNotFound {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch existing products")
			}

			oldByPW := make(map[uint]*entity.MarketingProduct)
			for i := range oldProducts {
				p := oldProducts[i]
				oldByPW[p.ProductWarehouseId] = &p
			}

			reqByPW := make(map[uint]validation.CreateMarketingProduct)
			for _, rp := range req.MarketingProducts {
				reqByPW[rp.ProductWarehouseId] = rp
			}

			for _, rp := range req.MarketingProducts {
				if old, ok := oldByPW[rp.ProductWarehouseId]; ok {

					totalWeight, totalPrice := s.calculatePriceByMarketingType(
						rp.MarketingType,
						rp.Qty,
						rp.AvgWeight,
						rp.UnitPrice,
						rp.Week,
						rp.ConvertionUnit,
						rp.WeightPerConvertion,
					)

					deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
					if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
						return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
					}
					if err == nil && deliveryProduct.Id != 0 {
						oldQty := old.Qty
						newQty := rp.Qty
						qtyDiff := newQty - oldQty

						if qtyDiff < 0 {
							return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
						} else if qtyDiff > 0 {
							nextRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty + qtyDiff
							if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, nextRequestedQty, 0); err != nil {
								return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing delivery fifo fields")
							}
							if err := reflowMarketingScope(
								c.Context(),
								s.FifoStockV2Svc,
								dbTransaction,
								rp.ProductWarehouseId,
								resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
							); err != nil {
								return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
							}
						}
					}

					updateBody := map[string]any{
						"product_warehouse_id":  rp.ProductWarehouseId,
						"qty":                   rp.Qty,
						"unit_price":            rp.UnitPrice,
						"avg_weight":            rp.AvgWeight,
						"total_weight":          totalWeight,
						"total_price":           totalPrice,
						"convertion_unit":       rp.ConvertionUnit,
						"weight_per_convertion": rp.WeightPerConvertion,
						"week":                  rp.Week,
					}
					if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil {
						return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
					}

					if deliveryProduct.Id == 0 {
						mdp := &entity.MarketingDeliveryProduct{
							MarketingProductId: old.Id,
							UnitPrice:          0,
							TotalWeight:        0,
							AvgWeight:          0,
							TotalPrice:         0,
							DeliveryDate:       nil,
							VehicleNumber:      rp.VehicleNumber,
							UsageQty:           0,
							PendingQty:         0,
						}
						if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
							return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
						}
					}
				} else {
					if err := s.createMarketingProductWithDelivery(c.Context(), id, rp.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
						return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
					}
				}
			}

			for _, old := range oldProducts {
				if _, ok := reqByPW[old.ProductWarehouseId]; !ok {
					deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
					if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
						return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product")
					}
					if err == nil && deliveryProduct.Id != 0 {

						if deliveryProduct.DeliveryDate != nil {
							return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id))
						}

						if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, 0, 0); err != nil {
							return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset marketing delivery fifo fields")
						}
						if err := reflowMarketingScope(
							c.Context(),
							s.FifoStockV2Svc,
							dbTransaction,
							deliveryProduct.ProductWarehouseId,
							resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
						); err != nil {
							return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
						}

						if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
							return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product")
						}
					}

					if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil {
						return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product")
					}
				}
			}
		}
		if latestApproval != nil {
			action := entity.ApprovalActionUpdated
			_, err := approvalSvcTx.CreateApproval(
				c.Context(),
				utils.ApprovalWorkflowMarketing,
				id,
				approvalutils.ApprovalStep(latestApproval.StepNumber),
				&action,
				actorID,
				nil)
			if err != nil {
				if !errors.Is(err, gorm.ErrDuplicatedKey) {
					return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval")
				}
			}
		}

		return nil
	})

	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order")
	}

	return s.getOne(c, id)
}

func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
	if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
		return err
	}

	marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)

	if errors.Is(err, gorm.ErrRecordNotFound) {
		return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
	}
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
	}
	if len(marketing.Products) > 0 {
		pwIDs := make([]uint, 0, len(marketing.Products))
		for _, p := range marketing.Products {
			if p.ProductWarehouseId != 0 {
				pwIDs = append(pwIDs, p.ProductWarehouseId)
			}
		}

		if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
			return err
		}
	}
	err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {

		marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
		marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
		marketingRepoTx := repository.NewMarketingRepository(dbTransaction)

		if len(marketing.Products) > 0 {
			deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
			if err == nil && len(deliveryProducts) > 0 {
				for _, dp := range deliveryProducts {
					if err := marketingDeliveryProductRepoTx.UpdateFifoFields(c.Context(), dp.Id, 0, 0); err != nil {
						return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset fifo fields for delivery product %d", dp.Id))
					}
					if err := reflowMarketingScope(
						c.Context(),
						s.FifoStockV2Svc,
						dbTransaction,
						dp.ProductWarehouseId,
						resolveMarketingAsOf(dp.DeliveryDate, dp.CreatedAt),
					); err != nil {
						return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2 for delivery product %d: %v", dp.Id, err))
					}
				}
			}

			for _, product := range marketing.Products {
				if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
					return db.Where("marketing_product_id = ?", product.Id).Unscoped()
				}); err != nil && err != gorm.ErrRecordNotFound {
					return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products")
				}
			}
		}

		if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
			return db.Where("marketing_id = ?", id).Unscoped()
		}); err != nil && err != gorm.ErrRecordNotFound {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products")
		}

		if err := marketingRepoTx.DeleteOne(c.Context(), id); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order")
		}

		return nil
	})

	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return fiberErr
		}
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order")
	}

	return nil
}

func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

	for _, id := range req.ApprovableIds {
		if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
			return nil, err
		}
	}

	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return nil, err
	}

	approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB()))

	var action entity.ApprovalAction
	switch strings.ToUpper(strings.TrimSpace(req.Action)) {
	case string(entity.ApprovalActionRejected):
		action = entity.ApprovalActionRejected
	case string(entity.ApprovalActionApproved):
		action = entity.ApprovalActionApproved
	default:
		return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
	}

	approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
	if len(approvableIDs) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
	}

	for _, id := range approvableIDs {
		if err := commonSvc.EnsureRelations(c.Context(),
			commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
		); err != nil {
			return nil, err
		}

		latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
		if err != nil {
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
		}
		if latestApproval == nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id))
		}

		if action == entity.ApprovalActionApproved {
			switch latestApproval.StepNumber {
			case uint16(utils.MarketingStepPengajuan):
			case uint16(utils.MarketingStepSalesOrder):
			default:
				return nil, fiber.NewError(fiber.StatusBadRequest,
					fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
			}
		}
		marketing, mErr := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
			return db.Preload("Products")
		})
		if mErr != nil {
			if errors.Is(mErr, gorm.ErrRecordNotFound) {
				return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("SalesOrders %d not found", id))
			}
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order for project validation")
		}

		if len(marketing.Products) > 0 {
			pwIDs := make([]uint, 0, len(marketing.Products))
			for _, p := range marketing.Products {
				if p.ProductWarehouseId != 0 {
					pwIDs = append(pwIDs, p.ProductWarehouseId)
				}
			}
			if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
				return nil, err
			}
		}
	}

	err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {

		approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))

		for _, approvableID := range approvableIDs {
			latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step")
			}

			if latestApproval == nil {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", approvableID))
			}

			var nextStep approvalutils.ApprovalStep
			currentStep := latestApproval.StepNumber

			if action == entity.ApprovalActionApproved {

				if currentStep == uint16(utils.MarketingStepPengajuan) {
					nextStep = utils.MarketingStepSalesOrder
				} else if currentStep == uint16(utils.MarketingStepSalesOrder) {
					nextStep = utils.MarketingDeliveryOrder
				} else {
					return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d already completed all approval steps", approvableID))
				}
			} else {

				nextStep = approvalutils.ApprovalStep(currentStep)
			}

			if _, err := approvalSvc.CreateApproval(
				c.Context(),
				utils.ApprovalWorkflowMarketing,
				approvableID,
				nextStep,
				&action,
				actorID,
				req.Notes,
			); err != nil {
				s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err)
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
			}
		}
		return nil
	})

	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
	}

	updated := make([]entity.Marketing, 0, len(approvableIDs))
	for _, id := range approvableIDs {
		marketing, err := s.getOne(c, id)
		if err != nil {
			return nil, err
		}
		updated = append(updated, *marketing)
	}

	return updated, nil
}

func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, marketingType string, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {

	totalWeight, totalPrice := s.calculatePriceByMarketingType(
		marketingType,
		rp.Qty,
		rp.AvgWeight,
		rp.UnitPrice,
		rp.Week,
		rp.ConvertionUnit,
		rp.WeightPerConvertion,
	)

	marketingProduct := &entity.MarketingProduct{
		MarketingId:         marketingId,
		ProductWarehouseId:  rp.ProductWarehouseId,
		Qty:                 rp.Qty,
		UnitPrice:           rp.UnitPrice,
		AvgWeight:           rp.AvgWeight,
		TotalWeight:         totalWeight,
		TotalPrice:          totalPrice,
		ConvertionUnit:      rp.ConvertionUnit,
		WeightPerConvertion: rp.WeightPerConvertion,
		Week:                rp.Week,
	}
	if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil {
		return err
	}

	marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
		MarketingProductId: marketingProduct.Id,
		ProductWarehouseId: marketingProduct.ProductWarehouseId,
		UnitPrice:          0,
		TotalWeight:        0,
		AvgWeight:          0,
		TotalPrice:         0,
		DeliveryDate:       nil,
		VehicleNumber:      rp.VehicleNumber,
		UsageQty:           0,
		PendingQty:         0,
	}
	if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
		return err
	}

	return nil
}

func (s *salesOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int, convertionUnit *string, _ *float64) (totalWeight, totalPrice float64) {
	if marketingType == string(utils.MarketingTypeTrading) {
		totalWeight = 0
		totalPrice = math.Round(qty*unitPrice*100) / 100
	} else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 {
		totalWeight = math.Round(qty*avgWeight*100) / 100
		totalPrice = math.Round(unitPrice*float64(*week)*qty*100) / 100
	} else {
		totalWeight = math.Round(qty*avgWeight*100) / 100

		if marketingType == string(utils.MarketingTypeTelur) && convertionUnit != nil {
			switch *convertionUnit {
			case string(utils.ConvertionUnitQty):
				totalPrice = math.Round(qty*unitPrice*100) / 100
				return totalWeight, totalPrice
			case string(utils.ConvertionUnitPeti):
				totalPrice = math.Round(totalWeight*unitPrice*100) / 100
				return totalWeight, totalPrice
			}
		}

		totalPrice = math.Round(totalWeight*unitPrice*100) / 100
	}
	return totalWeight, totalPrice
}
