package service

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

	"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
	marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
	rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"

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

type DeliveryOrdersService interface {
	GetAll(ctx *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
	CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
	BulkApproveToStatus(ctx *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error)
	GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error)
}

type deliveryOrdersService struct {
	Validate                     *validator.Validate
	MarketingRepo                marketingRepo.MarketingRepository
	MarketingProductRepo         marketingRepo.MarketingProductRepository
	MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
	StockLogRepo                 rShared.StockLogRepository
	ProductWarehouseRepo         rProductWarehouse.ProductWarehouseRepository
	ProjectFlockPopulationRepo   rProjectFlock.ProjectFlockPopulationRepository
	ApprovalSvc                  commonSvc.ApprovalService
	FifoStockV2Svc               commonSvc.FifoStockV2Service
}

func NewDeliveryOrdersService(
	marketingRepo marketingRepo.MarketingRepository,
	marketingProductRepo marketingRepo.MarketingProductRepository,
	marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
	stockLogRepo rShared.StockLogRepository,
	productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
	projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
	approvalSvc commonSvc.ApprovalService,
	fifoStockV2Svc commonSvc.FifoStockV2Service,
	validate *validator.Validate,
) DeliveryOrdersService {
	return &deliveryOrdersService{
		Validate:                     validate,
		MarketingRepo:                marketingRepo,
		MarketingProductRepo:         marketingProductRepo,
		MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
		StockLogRepo:                 stockLogRepo,
		ProductWarehouseRepo:         productWarehouseRepo,
		ProjectFlockPopulationRepo:   projectFlockPopulationRepo,
		ApprovalSvc:                  approvalSvc,
		FifoStockV2Svc:               fifoStockV2Svc,
	}
}

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

func (s deliveryOrdersService) marketingOwnerRelationQuery(ctx context.Context) *gorm.DB {
	return s.MarketingRepo.DB().
		WithContext(ctx).
		Table("marketing_products mp").
		Select("1").
		Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
		Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = pw.project_flock_kandang_id").
		Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
		Joins("LEFT JOIN warehouses w ON w.id = pw.warehouse_id").
		Joins("LEFT JOIN kandangs k ON k.id = COALESCE(pfk.kandang_id, w.kandang_id)").
		Where("mp.marketing_id = marketings.id")
}

func (s deliveryOrdersService) marketingAttributionRelationQuery(ctx context.Context) *gorm.DB {
	baseDB := s.MarketingRepo.DB().WithContext(ctx)
	return baseDB.
		Table("marketing_delivery_products mdp").
		Select("1").
		Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
		Joins("JOIN (?) AS mda ON mda.marketing_delivery_product_id = mdp.id", commonRepo.MarketingDeliveryAttributionRowsQuery(baseDB)).
		Joins("JOIN project_flock_kandangs pfk_attr ON pfk_attr.id = mda.project_flock_kandang_id").
		Joins("JOIN project_flocks pf_attr ON pf_attr.id = pfk_attr.project_flock_id").
		Joins("JOIN kandangs k_attr ON k_attr.id = pfk_attr.kandang_id").
		Where("mp.marketing_id = marketings.id")
}

func (s deliveryOrdersService) applyMarketingProjectFlockFilter(ctx context.Context, db *gorm.DB, projectFlockID, projectFlockKandangID uint) *gorm.DB {
	if projectFlockID > 0 {
		db = db.Where(
			"(EXISTS (?) OR EXISTS (?))",
			s.marketingOwnerRelationQuery(ctx).Where("pfk.project_flock_id = ?", projectFlockID),
			s.marketingAttributionRelationQuery(ctx).Where("mda.project_flock_id = ?", projectFlockID),
		)
	}

	if projectFlockKandangID > 0 {
		db = db.Where(
			"(EXISTS (?) OR EXISTS (?))",
			s.marketingOwnerRelationQuery(ctx).Where("pw.project_flock_kandang_id = ?", projectFlockKandangID),
			s.marketingAttributionRelationQuery(ctx).Where("mda.project_flock_kandang_id = ?", projectFlockKandangID),
		)
	}

	return db
}

func (s deliveryOrdersService) applyMarketingSearchFilter(ctx context.Context, db *gorm.DB, rawSearch string) *gorm.DB {
	searchPattern := "%" + strings.TrimSpace(rawSearch) + "%"
	if searchPattern == "%%" {
		return db
	}

	return db.Where(
		`(
			marketings.so_number ILIKE ? OR
			EXISTS (
				SELECT 1
				FROM customers c
				WHERE c.id = marketings.customer_id
					AND c.name ILIKE ?
			) OR
			EXISTS (
				SELECT 1
				FROM users su
				WHERE su.id = marketings.sales_person_id
					AND su.name ILIKE ?
			) OR
			EXISTS (
				SELECT 1
				FROM marketing_products mp
				JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
				JOIN products p ON p.id = pw.product_id
				WHERE mp.marketing_id = marketings.id
					AND p.name ILIKE ?
			) OR
			EXISTS (
				SELECT 1
				FROM marketing_products mp
				JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
				JOIN warehouses w ON w.id = pw.warehouse_id
				WHERE mp.marketing_id = marketings.id
					AND w.name ILIKE ?
			) OR
			EXISTS (?) OR
			EXISTS (?)
		)`,
		searchPattern,
		searchPattern,
		searchPattern,
		searchPattern,
		searchPattern,
		s.marketingOwnerRelationQuery(ctx).Where("pf.flock_name ILIKE ? OR k.name ILIKE ?", searchPattern, searchPattern),
		s.marketingAttributionRelationQuery(ctx).Where("pf_attr.flock_name ILIKE ? OR k_attr.name ILIKE ?", searchPattern, searchPattern),
	)
}

func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) {
	if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), marketingId); err != nil {
		return nil, err
	}

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

	latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketingId, nil)
	if err != nil {
	}
	marketing.LatestApproval = latestApproval

	allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketingId)
	if err != nil {
		allDeliveryProducts = []entity.MarketingDeliveryProduct{}
	}

	responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts)
	return &responseDTO, nil
}

func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

	scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB())
	if err != nil {
		return nil, 0, err
	}

	offset := (params.Page - 1) * params.Limit

	marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
		db = db.
			Preload("CreatedUser").
			Preload("Customer").
			Preload("SalesPerson").
			Preload("Products.ProductWarehouse.Product").
			Preload("Products.ProductWarehouse.Product.Uom").
			Preload("Products.ProductWarehouse.Warehouse").
			Preload("Products.DeliveryProduct")

		if params.Status != "" {
			status := strings.TrimSpace(params.Status)
			latestApprovalSubQuery := s.MarketingRepo.DB().
				WithContext(c.Context()).
				Table("approvals").
				Select("DISTINCT ON (approvable_id) approvable_id, step_name, action").
				Where("approvable_type = ?", utils.ApprovalWorkflowMarketing.String()).
				Order("approvable_id, id DESC")

			if strings.EqualFold(status, "DITOLAK") {
				db = db.Where(`EXISTS (
					SELECT 1
					FROM (?) AS latest_approval
					WHERE latest_approval.approvable_id = marketings.id
						AND latest_approval.action = ?
				)`, latestApprovalSubQuery, string(entity.ApprovalActionRejected))
			} else {
				db = db.Where(`EXISTS (
					SELECT 1
					FROM (?) AS latest_approval
					WHERE latest_approval.approvable_id = marketings.id
						AND LOWER(latest_approval.step_name) = LOWER(?)
						AND (latest_approval.action IS NULL OR latest_approval.action <> ?)
				)`, latestApprovalSubQuery, status, string(entity.ApprovalActionRejected))
			}
		}

		if len(params.ProductIDs) > 0 {
			db = db.Where(`EXISTS (
				SELECT 1
				FROM marketing_products mp
				JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
				JOIN products p ON p.id = pw.product_id
				WHERE mp.marketing_id = marketings.id
					AND p.id IN ?
			)`, params.ProductIDs)
		}

		if params.CustomerId != 0 {
			db = db.Where("marketings.customer_id = ?", params.CustomerId)
		}

		db = s.applyMarketingProjectFlockFilter(c.Context(), db, params.ProjectFlockID, params.ProjectFlockKandangID)
		db = s.applyMarketingSearchFilter(c.Context(), db, params.Search)

		if scope.Restrict {
			if len(scope.IDs) == 0 {
				return db.Where("1 = 0")
			}
			db = db.Where(
				`EXISTS (
					SELECT 1
					FROM marketing_products mp
					JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
					JOIN warehouses w ON w.id = pw.warehouse_id
					WHERE mp.marketing_id = marketings.id
						AND w.location_id IN ?
				)`,
				scope.IDs,
			)
		}

		if params.MarketingId != 0 {
			return db.Where("id = ?", params.MarketingId)
		}
		return db.Order("created_at DESC").Order("updated_at DESC")
	})
	if err != nil {
		return nil, 0, err
	}
	for i := range marketings {
		latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, func(db *gorm.DB) *gorm.DB {
			return db.Preload("ActionUser")
		})
		if err != nil {
			continue
		}
		marketings[i].LatestApproval = latestApproval
	}

	result := make([]dto.MarketingListDTO, len(marketings))
	for i, marketing := range marketings {
		result[i] = dto.ToMarketingListDTO(&marketing, []entity.MarketingDeliveryProduct{})
	}

	return result, total, nil
}

func (s deliveryOrdersService) GetProgressRows(c *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error) {
	scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB())
	if err != nil {
		return nil, err
	}
	return s.MarketingRepo.GetProgressRows(c.Context(), query.StartDate, query.EndDate, scope.IDs, scope.Restrict)
}

func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, 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, "Marketing not found")
	}
	if err != nil {
		return nil, err
	}

	allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id)
	if err != nil {
		allDeliveryProducts = []entity.MarketingDeliveryProduct{}
	}

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

	responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts)
	return &responseDTO, nil
}

func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

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

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

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

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

	latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
	}
	if latestApproval == nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval")
	}
	if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order")
	}
	if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing")
	}

	err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
		marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
		approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
		marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction)

		marketing, err := marketingRepoTx.GetByID(c.Context(), req.MarketingId, nil)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
		}

		allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId))
			}
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products")
		}

		for _, requestedProduct := range req.DeliveryProducts {
			var foundMarketingProduct *entity.MarketingProduct
			for i := range allMarketingProducts {
				if allMarketingProducts[i].Id == requestedProduct.MarketingProductId {
					foundMarketingProduct = &allMarketingProducts[i]
					break
				}
			}
			if foundMarketingProduct == nil {
				return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
			}

			if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
				c.Context(),
				dbTransaction,
				[]uint{foundMarketingProduct.ProductWarehouseId},
			); err != nil {
				return err
			}

			deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId))
				}
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
			}

			var itemDeliveryDate *time.Time
			if requestedProduct.DeliveryDate != "" {
				parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate)
				if err != nil {
					return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err))
				}
				itemDeliveryDate = &parsedDate
			}

			totalWeight, totalPrice := s.resolveDeliveryTotals(marketing.MarketingType, requestedProduct, foundMarketingProduct)

			deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
			deliveryProduct.UnitPrice = requestedProduct.UnitPrice
			deliveryProduct.AvgWeight = requestedProduct.AvgWeight
			deliveryProduct.WeightPerConvertion = requestedProduct.WeightPerConvertion
			deliveryProduct.TotalWeight = totalWeight
			deliveryProduct.TotalPrice = totalPrice
			deliveryProduct.DeliveryDate = itemDeliveryDate
			deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber

			if requestedProduct.Qty > 0 {
				if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty, actorID); err != nil {
					return err
				}
			}
			if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
			}

		}

		approvalAction := entity.ApprovalActionApproved
		if _, err := approvalSvcTx.CreateApproval(
			c.Context(),
			utils.ApprovalWorkflowMarketing,
			req.MarketingId,
			utils.MarketingDeliveryOrder,
			&approvalAction,
			actorID,
			nil); err != nil {
			if !errors.Is(err, gorm.ErrDuplicatedKey) {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order 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 delivery order")
	}

	return s.getMarketingWithDeliveries(c, req.MarketingId)
}

func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

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

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

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

	err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
		marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
		marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction)

		marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
		}

		allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products")
		}

		if len(req.DeliveryProducts) > 0 {
			for _, requestedProduct := range req.DeliveryProducts {

				var foundMarketingProduct *entity.MarketingProduct
				for i := range allMarketingProducts {
					if allMarketingProducts[i].Id == requestedProduct.MarketingProductId {
						foundMarketingProduct = &allMarketingProducts[i]
						break
					}
				}
				if foundMarketingProduct == nil {
					return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
				}
				if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
					c.Context(),
					dbTransaction,
					[]uint{foundMarketingProduct.ProductWarehouseId},
				); err != nil {
					return err
				}

				deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
				if err != nil {
					if errors.Is(err, gorm.ErrRecordNotFound) {
						return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId))
					}
					return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
				}

				oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
				var itemDeliveryDate *time.Time
				if requestedProduct.DeliveryDate != "" {
					parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate)
					if err != nil {
						return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err))
					}
					itemDeliveryDate = &parsedDate
				} else if deliveryProduct.DeliveryDate != nil {
					itemDeliveryDate = deliveryProduct.DeliveryDate
				}

				totalWeight, totalPrice := s.resolveDeliveryTotals(marketing.MarketingType, requestedProduct, foundMarketingProduct)

				deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
				deliveryProduct.UnitPrice = requestedProduct.UnitPrice
				deliveryProduct.AvgWeight = requestedProduct.AvgWeight
				deliveryProduct.WeightPerConvertion = requestedProduct.WeightPerConvertion
				deliveryProduct.TotalWeight = totalWeight
				deliveryProduct.TotalPrice = totalPrice
				deliveryProduct.DeliveryDate = itemDeliveryDate
				deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber

				if requestedProduct.Qty != oldRequestedQty {

					if oldRequestedQty > 0 {
						if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, actorID); err != nil {
							return err
						}
					}

					if requestedProduct.Qty > 0 {
						if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty, actorID); err != nil {
							return err
						}
					}
				}

				if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil {
					return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
				}
			}
		}

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

	return s.getMarketingWithDeliveries(c, id)
}

func (s deliveryOrdersService) BulkApproveToStatus(c *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

	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 := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
			return nil, err
		}
	}

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

	var deliveryDate time.Time
	if req.RequiresDate(target) {
		deliveryDate, err = utils.ParseDateString(req.Date)
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
		}
	}

	err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
		approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
		marketingRepoTx := marketingRepo.NewMarketingRepository(tx)

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

			marketing, err := marketingRepoTx.GetByID(c.Context(), id, s.withRelations)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing %d not found", id))
				}
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
			}

			latestApproval, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
			}
			if latestApproval == nil {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", id))
			}
			if latestApproval.Action != nil && *latestApproval.Action == entity.ApprovalActionRejected {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is rejected and cannot be bulk approved", id))
			}

			currentStep := approvalutils.ApprovalStep(latestApproval.StepNumber)
			if currentStep >= target {
				currentStepName := utils.MarketingApprovalSteps[currentStep]
				targetStepName := utils.MarketingApprovalSteps[target]
				if currentStep == target {
					return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is already at %s step", id, targetStepName))
				}
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is already beyond %s step (current step: %s)", id, targetStepName, currentStepName))
			}

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

			for step := currentStep + 1; step <= target; step++ {
				if step == utils.MarketingDeliveryOrder {
					if err := s.createDeliveryFromMarketingProducts(c.Context(), tx, marketing, deliveryDate, actorID); err != nil {
						return err
					}
				}

				approvalAction := entity.ApprovalActionApproved
				if _, err := approvalSvcTx.CreateApproval(
					c.Context(),
					utils.ApprovalWorkflowMarketing,
					id,
					step,
					&approvalAction,
					actorID,
					req.Notes,
				); err != nil {
					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 bulk approve marketings")
	}

	results := make([]dto.MarketingDetailDTO, 0, len(approvableIDs))
	for _, id := range approvableIDs {
		result, err := s.getMarketingWithDeliveries(c, id)
		if err != nil {
			return nil, err
		}
		results = append(results, *result)
	}

	return results, nil
}

func (s deliveryOrdersService) createDeliveryFromMarketingProducts(
	ctx context.Context,
	tx *gorm.DB,
	marketing *entity.Marketing,
	deliveryDate time.Time,
	actorID uint,
) error {
	if marketing == nil {
		return fiber.NewError(fiber.StatusBadRequest, "Marketing not found")
	}
	if tx == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Transaction is required")
	}

	marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(tx)

	for _, marketingProduct := range marketing.Products {
		deliveryProduct := marketingProduct.DeliveryProduct
		if deliveryProduct == nil {
			record, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(ctx, marketingProduct.Id)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", marketingProduct.Id))
				}
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
			}
			deliveryProduct = record
		}

		oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
		deliveryDateCopy := deliveryDate

		deliveryProduct.ProductWarehouseId = marketingProduct.ProductWarehouseId
		deliveryProduct.UnitPrice = marketingProduct.UnitPrice
		deliveryProduct.AvgWeight = marketingProduct.AvgWeight
		deliveryProduct.WeightPerConvertion = marketingProduct.WeightPerConvertion
		deliveryProduct.TotalWeight = marketingProduct.TotalWeight
		deliveryProduct.TotalPrice = marketingProduct.TotalPrice
		deliveryProduct.DeliveryDate = &deliveryDateCopy

		requestedQty := marketingProduct.Qty
		if requestedQty != oldRequestedQty {
			if oldRequestedQty > 0 {
				if err := s.releaseDeliveryStock(ctx, tx, deliveryProduct, &marketingProduct, actorID); err != nil {
					return err
				}
			}
			if requestedQty > 0 {
				if err := s.consumeDeliveryStock(ctx, tx, deliveryProduct, &marketingProduct, requestedQty, actorID); err != nil {
					return err
				}
			}
		}

		if err := marketingDeliveryProductRepositoryTx.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
		}
	}

	return nil
}

func (s *deliveryOrdersService) 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
}

func (s *deliveryOrdersService) resolveDeliveryTotals(marketingType string, requestedProduct validation.DeliveryProduct, marketingProduct *entity.MarketingProduct) (totalWeight, totalPrice float64) {
	totalWeight, totalPrice = s.calculatePriceByMarketingType(
		marketingType,
		requestedProduct.Qty,
		requestedProduct.AvgWeight,
		requestedProduct.UnitPrice,
		marketingProduct.Week,
		marketingProduct.ConvertionUnit,
		marketingProduct.WeightPerConvertion,
	)

	if requestedProduct.TotalWeight != nil {
		totalWeight = *requestedProduct.TotalWeight
	}
	if requestedProduct.TotalPrice != nil {
		totalPrice = *requestedProduct.TotalPrice
	}

	return totalWeight, totalPrice
}

func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64, actorID uint) error {
	if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
		return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
	}

	if deliveryProduct == nil || deliveryProduct.Id == 0 {
		return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
	}
	if deliveryProduct.ProductWarehouseId == 0 {
		return fiber.NewError(fiber.StatusInternalServerError, "Delivery product warehouse not found")
	}
	if deliveryProduct.ProductWarehouseId != marketingProduct.ProductWarehouseId {
		return fiber.NewError(fiber.StatusBadRequest, "Delivery product warehouse mismatch with marketing product")
	}

	deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
	previousUsage := deliveryProduct.UsageQty
	deliveryProduct.UsageQty = requestedQty
	deliveryProduct.PendingQty = 0

	if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
	}
	if err := reflowMarketingScope(
		ctx,
		s.FifoStockV2Svc,
		tx,
		marketingProduct.ProductWarehouseId,
		resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
	); err != nil {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
	}

	refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
	}
	deliveryProduct.UsageQty = refreshed.UsageQty
	deliveryProduct.PendingQty = refreshed.PendingQty
	deliveryProduct.CreatedAt = refreshed.CreatedAt

	if err := s.allocatePopulationForMarketingDelivery(ctx, tx, deliveryProduct, marketingProduct.ProductWarehouseId); err != nil {
		return err
	}

	allocatedDelta := deliveryProduct.UsageQty - previousUsage
	if actorID > 0 && allocatedDelta > 0 {
		decreaseLog := &entity.StockLog{
			Decrease:           allocatedDelta,
			LoggableType:       string(utils.StockLogTypeMarketing),
			LoggableId:         deliveryProduct.Id,
			ProductWarehouseId: deliveryProduct.ProductWarehouseId,
			CreatedBy:          actorID,
			Notes:              fmt.Sprintf("FIFO v2 reflow consume (%.2f)", allocatedDelta),
		}

		stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, deliveryProduct.ProductWarehouseId, 1)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
		}
		if len(stockLogs) > 0 {
			latestStockLog := stockLogs[0]
			decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease
		} else {
			decreaseLog.Stock -= decreaseLog.Decrease
		}
		s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil)
	}

	return nil
}

func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, actorID uint) error {
	if deliveryProduct == nil || deliveryProduct.Id == 0 {
		return nil
	}

	if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
		return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
	}

	deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
	currentUsage := deliveryProduct.UsageQty
	currentPending := deliveryProduct.PendingQty
	if currentUsage <= 0 && currentPending <= 0 {
		return nil
	}

	affectedKandangIDs, err := s.marketingPopulationKandangIDsFromActiveAllocations(ctx, tx, deliveryProduct.Id)
	if err != nil {
		return err
	}

	deliveryProduct.UsageQty = 0
	deliveryProduct.PendingQty = 0
	if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset delivery product")
	}

	if err := reflowMarketingScope(
		ctx,
		s.FifoStockV2Svc,
		tx,
		marketingProduct.ProductWarehouseId,
		resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
	); err != nil {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
	}

	refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
	}
	deliveryProduct.UsageQty = refreshed.UsageQty
	deliveryProduct.PendingQty = refreshed.PendingQty
	deliveryProduct.CreatedAt = refreshed.CreatedAt

	if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
		return err
	}
	if err := s.resyncPopulationUsageByKandangIDs(ctx, tx, affectedKandangIDs); err != nil {
		return err
	}

	releasedUsage := currentUsage - deliveryProduct.UsageQty
	if actorID > 0 && releasedUsage > 0 {
		increaseLog := &entity.StockLog{
			Increase:           releasedUsage,
			LoggableType:       string(utils.StockLogTypeMarketing),
			LoggableId:         deliveryProduct.Id,
			ProductWarehouseId: marketingProduct.ProductWarehouseId,
			CreatedBy:          actorID,
			Notes:              fmt.Sprintf("FIFO v2 reflow release (%.2f)", releasedUsage),
		}
		stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
		}
		if len(stockLogs) > 0 {
			latestStockLog := stockLogs[0]
			increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase
		} else {
			increaseLog.Stock += increaseLog.Increase
		}

		s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil)
	}

	return nil
}

func (s deliveryOrdersService) allocatePopulationForMarketingDelivery(
	ctx context.Context,
	tx *gorm.DB,
	deliveryProduct *entity.MarketingDeliveryProduct,
	productWarehouseID uint,
) error {
	if deliveryProduct == nil || deliveryProduct.Id == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Delivery product tidak valid")
	}
	if tx == nil {
		return errors.New("transaction is required")
	}
	if deliveryProduct.UsageQty <= 0 {
		return nil
	}
	if productWarehouseID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Product warehouse tidak ditemukan")
	}

	flagGroupCode, err := resolveMarketingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
	if err != nil {
		return err
	}
	if !strings.EqualFold(flagGroupCode, "AYAM") {
		return nil
	}

	exactAllocations, err := s.findDirectPopulationAllocationsForMarketing(ctx, tx, deliveryProduct.Id)
	if err != nil {
		return err
	}
	if len(exactAllocations) > 0 {
		if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
			return err
		}
		if err := s.applyDirectPopulationAllocationsForMarketing(ctx, tx, productWarehouseID, deliveryProduct.Id, exactAllocations); err != nil {
			return err
		}
		return s.resyncPopulationUsageByKandangIDs(ctx, tx, marketingAllocationKandangIDs(exactAllocations))
	}

	sourceGroups, err := s.findPopulationSourceGroupsForMarketing(ctx, tx, deliveryProduct.Id, productWarehouseID)
	if err != nil {
		return err
	}
	if len(sourceGroups) == 0 {
		return nil
	}
	if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
		return err
	}
	for _, group := range sourceGroups {
		populations, err := s.ProjectFlockPopulationRepo.WithTx(tx).GetByProjectFlockKandangIDAndProductWarehouseID(
			ctx,
			group.ProjectFlockKandangID,
			group.ProductWarehouseID,
		)
		if err != nil {
			return err
		}
		if len(populations) == 0 {
			return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk delivery")
		}
		if err := s.allocatePopulationConsumptionWithoutRelease(
			ctx,
			tx,
			populations,
			productWarehouseID,
			deliveryProduct.Id,
			group.Qty,
		); err != nil {
			return err
		}
	}
	return s.resyncPopulationUsageByKandangIDs(ctx, tx, marketingSourceGroupKandangIDs(sourceGroups))
}

type marketingPopulationAllocation struct {
	ProjectFlockPopulationID uint    `gorm:"column:project_flock_population_id"`
	ProjectFlockKandangID    uint    `gorm:"column:project_flock_kandang_id"`
	Qty                      float64 `gorm:"column:qty"`
}

type marketingPopulationSourceGroup struct {
	ProjectFlockKandangID uint    `gorm:"column:project_flock_kandang_id"`
	ProductWarehouseID    uint    `gorm:"column:product_warehouse_id"`
	Qty                   float64 `gorm:"column:qty"`
}

func (s deliveryOrdersService) findDirectPopulationAllocationsForMarketing(
	ctx context.Context,
	tx *gorm.DB,
	deliveryProductID uint,
) ([]marketingPopulationAllocation, error) {
	var rows []marketingPopulationAllocation
	err := tx.WithContext(ctx).
		Table("stock_allocations sa").
		Select(`
			pfp.id AS project_flock_population_id,
			pc.project_flock_kandang_id AS project_flock_kandang_id,
			SUM(sa.qty) AS qty
		`).
		Joins("JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
		Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
		Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
			fifo.UsableKeyMarketingDelivery.String(),
			deliveryProductID,
			entity.StockAllocationStatusActive,
			entity.StockAllocationPurposeConsume,
		).
		Group("pfp.id, pc.project_flock_kandang_id").
		Order("pfp.id ASC").
		Scan(&rows).Error
	return rows, err
}

func (s deliveryOrdersService) findPopulationSourceGroupsForMarketing(
	ctx context.Context,
	tx *gorm.DB,
	deliveryProductID uint,
	productWarehouseID uint,
) ([]marketingPopulationSourceGroup, error) {
	groups := make(map[string]marketingPopulationSourceGroup)

	appendGroup := func(projectFlockKandangID uint, sourceProductWarehouseID uint, qty float64) {
		if projectFlockKandangID == 0 || sourceProductWarehouseID == 0 || qty <= 0 {
			return
		}
		key := fmt.Sprintf("%d:%d", projectFlockKandangID, sourceProductWarehouseID)
		current := groups[key]
		current.ProjectFlockKandangID = projectFlockKandangID
		current.ProductWarehouseID = sourceProductWarehouseID
		current.Qty += qty
		groups[key] = current
	}

	var transferRows []marketingPopulationSourceGroup
	if err := tx.WithContext(ctx).
		Table("stock_allocations sa").
		Select(`
			source_pw.project_flock_kandang_id AS project_flock_kandang_id,
			std.source_product_warehouse_id AS product_warehouse_id,
			SUM(sa.qty) AS qty
		`).
		Joins("JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
		Joins("JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id").
		Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
			fifo.UsableKeyMarketingDelivery.String(),
			deliveryProductID,
			entity.StockAllocationStatusActive,
			entity.StockAllocationPurposeConsume,
		).
		Where("source_pw.project_flock_kandang_id IS NOT NULL").
		Group("source_pw.project_flock_kandang_id, std.source_product_warehouse_id").
		Scan(&transferRows).Error; err != nil {
		return nil, err
	}
	for _, row := range transferRows {
		appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
	}

	var purchaseRows []marketingPopulationSourceGroup
	if err := tx.WithContext(ctx).
		Table("stock_allocations sa").
		Select(`
			pi.project_flock_kandang_id AS project_flock_kandang_id,
			pi.product_warehouse_id AS product_warehouse_id,
			SUM(sa.qty) AS qty
		`).
		Joins("JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
		Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
			fifo.UsableKeyMarketingDelivery.String(),
			deliveryProductID,
			entity.StockAllocationStatusActive,
			entity.StockAllocationPurposeConsume,
		).
		Where("pi.project_flock_kandang_id IS NOT NULL").
		Where("pi.product_warehouse_id IS NOT NULL").
		Group("pi.project_flock_kandang_id, pi.product_warehouse_id").
		Scan(&purchaseRows).Error; err != nil {
		return nil, err
	}
	for _, row := range purchaseRows {
		appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
	}

	var layingRows []marketingPopulationSourceGroup
	if err := tx.WithContext(ctx).
		Table("stock_allocations sa").
		Select(`
			ltt.target_project_flock_kandang_id AS project_flock_kandang_id,
			ltt.product_warehouse_id AS product_warehouse_id,
			SUM(sa.qty) AS qty
		`).
		Joins("JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
		Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
			fifo.UsableKeyMarketingDelivery.String(),
			deliveryProductID,
			entity.StockAllocationStatusActive,
			entity.StockAllocationPurposeConsume,
		).
		Where("ltt.product_warehouse_id IS NOT NULL").
		Group("ltt.target_project_flock_kandang_id, ltt.product_warehouse_id").
		Scan(&layingRows).Error; err != nil {
		return nil, err
	}
	for _, row := range layingRows {
		appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
	}

	if len(groups) == 0 {
		pw, err := s.ProductWarehouseRepo.WithTx(tx).GetByID(ctx, productWarehouseID, nil)
		if err != nil {
			return nil, err
		}
		if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId != 0 {
			appendGroup(*pw.ProjectFlockKandangId, productWarehouseID, 0)
		}
	}

	result := make([]marketingPopulationSourceGroup, 0, len(groups))
	for _, group := range groups {
		if group.Qty == 0 {
			group.Qty = s.resolveMarketingRequestedUsageQty(ctx, tx, deliveryProductID)
		}
		if group.Qty > 0 {
			result = append(result, group)
		}
	}

	return result, nil
}

func (s deliveryOrdersService) applyDirectPopulationAllocationsForMarketing(
	ctx context.Context,
	tx *gorm.DB,
	productWarehouseID uint,
	deliveryProductID uint,
	allocations []marketingPopulationAllocation,
) error {
	stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
	for _, allocation := range allocations {
		if allocation.ProjectFlockPopulationID == 0 || allocation.Qty <= 0 {
			continue
		}
		record := &entity.StockAllocation{
			ProductWarehouseId: productWarehouseID,
			StockableType:      fifo.StockableKeyProjectFlockPopulation.String(),
			StockableId:        allocation.ProjectFlockPopulationID,
			UsableType:         fifo.UsableKeyMarketingDelivery.String(),
			UsableId:           deliveryProductID,
			Qty:                allocation.Qty,
			Status:             entity.StockAllocationStatusActive,
			AllocationPurpose:  entity.StockAllocationPurposeConsume,
		}
		if err := stockAllocationRepo.CreateOne(ctx, record, nil); err != nil {
			return err
		}
		if err := tx.WithContext(ctx).
			Model(&entity.ProjectFlockPopulation{}).
			Where("id = ?", allocation.ProjectFlockPopulationID).
			Update("total_used_qty", gorm.Expr("total_used_qty + ?", allocation.Qty)).Error; err != nil {
			return err
		}
	}
	return nil
}

func (s deliveryOrdersService) allocatePopulationConsumptionWithoutRelease(
	ctx context.Context,
	tx *gorm.DB,
	populations []entity.ProjectFlockPopulation,
	productWarehouseID uint,
	deliveryProductID uint,
	consumeQty float64,
) error {
	if consumeQty <= 0 {
		return nil
	}
	remaining := consumeQty
	stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
	for _, population := range populations {
		available := population.TotalQty - population.TotalUsedQty
		if available <= 0 {
			continue
		}
		portion := available
		if remaining < portion {
			portion = remaining
		}
		if portion <= 0 {
			continue
		}

		record := &entity.StockAllocation{
			ProductWarehouseId: productWarehouseID,
			StockableType:      fifo.StockableKeyProjectFlockPopulation.String(),
			StockableId:        population.Id,
			UsableType:         fifo.UsableKeyMarketingDelivery.String(),
			UsableId:           deliveryProductID,
			Qty:                portion,
			Status:             entity.StockAllocationStatusActive,
			AllocationPurpose:  entity.StockAllocationPurposeConsume,
		}
		if err := stockAllocationRepo.CreateOne(ctx, record, nil); err != nil {
			return err
		}
		if err := tx.WithContext(ctx).
			Model(&entity.ProjectFlockPopulation{}).
			Where("id = ?", population.Id).
			Update("total_used_qty", gorm.Expr("total_used_qty + ?", portion)).Error; err != nil {
			return err
		}

		remaining -= portion
		if remaining <= 0.000001 {
			break
		}
	}

	if remaining > 0.000001 {
		return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak mencukupi")
	}
	return nil
}

func (s deliveryOrdersService) marketingPopulationKandangIDsFromActiveAllocations(
	ctx context.Context,
	tx *gorm.DB,
	deliveryProductID uint,
) ([]uint, error) {
	var ids []uint
	err := tx.WithContext(ctx).
		Table("stock_allocations sa").
		Distinct("pc.project_flock_kandang_id").
		Joins("JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
		Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
		Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
			fifo.UsableKeyMarketingDelivery.String(),
			deliveryProductID,
			entity.StockAllocationStatusActive,
			entity.StockAllocationPurposeConsume,
		).
		Pluck("pc.project_flock_kandang_id", &ids).Error
	return ids, err
}

func (s deliveryOrdersService) resyncPopulationUsageByKandangIDs(ctx context.Context, tx *gorm.DB, kandangIDs []uint) error {
	for _, kandangID := range uniqueUintIDs(kandangIDs) {
		if kandangID == 0 {
			continue
		}
		if err := s.ProjectFlockPopulationRepo.WithTx(tx).ResyncUsageByProjectFlockKandangID(ctx, tx, kandangID); err != nil {
			return err
		}
	}
	return nil
}

func (s deliveryOrdersService) resolveMarketingRequestedUsageQty(ctx context.Context, tx *gorm.DB, deliveryProductID uint) float64 {
	var usageQty float64
	if err := tx.WithContext(ctx).
		Table("marketing_delivery_products").
		Select("usage_qty + pending_qty").
		Where("id = ?", deliveryProductID).
		Scan(&usageQty).Error; err != nil {
		return 0
	}
	return usageQty
}

func marketingAllocationKandangIDs(rows []marketingPopulationAllocation) []uint {
	ids := make([]uint, 0, len(rows))
	for _, row := range rows {
		ids = append(ids, row.ProjectFlockKandangID)
	}
	return ids
}

func marketingSourceGroupKandangIDs(rows []marketingPopulationSourceGroup) []uint {
	ids := make([]uint, 0, len(rows))
	for _, row := range rows {
		ids = append(ids, row.ProjectFlockKandangID)
	}
	return ids
}

func uniqueUintIDs(ids []uint) []uint {
	seen := make(map[uint]struct{}, len(ids))
	result := make([]uint, 0, len(ids))
	for _, id := range ids {
		if id == 0 {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		result = append(result, id)
	}
	return result
}
