package service

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

	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"
	"gitlab.com/mbugroup/lti-api.git/internal/config"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
	ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
	rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"

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

type TransferLayingService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error)
	CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
	Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
	Execute(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
	ExecuteWithBusinessDate(ctx *fiber.Ctx, id uint, businessDate time.Time) (*entity.LayingTransfer, error)
	Unexecute(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
	GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error)
	GetMaxTargetQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (map[uint]float64, error)
}

type transferLayingService struct {
	Log                        *logrus.Logger
	Validate                   *validator.Validate
	Repository                 repository.TransferLayingRepository
	LayingTransferSourceRepo   repository.LayingTransferSourceRepository
	LayingTransferTargetRepo   repository.LayingTransferTargetRepository
	ProjectFlockRepo           ProjectFlockRepository.ProjectflockRepository
	ProjectFlockKandangRepo    ProjectFlockRepository.ProjectFlockKandangRepository
	ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository
	ProductWarehouseRepo       rInventory.ProductWarehouseRepository
	WarehouseRepo              rWarehouse.WarehouseRepository
	StockLogRepo               rStockLogs.StockLogRepository
	ApprovalService            commonSvc.ApprovalService
	FifoStockV2Svc             commonSvc.FifoStockV2Service
}

const (
	transferToLayingFlagGroupCode              = "AYAM"
	transferLayingDeleteDownstreamGuardMessage = "Transfer laying tidak dapat dihapus karena stok target transfer sudah dipakai transaksi turunan. Hapus dependensi terkait secara manual terlebih dahulu."
)

func NewTransferLayingService(
	repo repository.TransferLayingRepository,
	layingTransferSourceRepo repository.LayingTransferSourceRepository,
	layingTransferTargetRepo repository.LayingTransferTargetRepository,
	projectFlockRepo ProjectFlockRepository.ProjectflockRepository,
	projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository,
	projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository,
	productWarehouseRepo rInventory.ProductWarehouseRepository,
	warehouseRepo rWarehouse.WarehouseRepository,
	approvalService commonSvc.ApprovalService,
	fifoStockV2Svc commonSvc.FifoStockV2Service,
	validate *validator.Validate,
) TransferLayingService {
	return &transferLayingService{
		Log:                        utils.Log,
		Validate:                   validate,
		Repository:                 repo,
		LayingTransferSourceRepo:   layingTransferSourceRepo,
		LayingTransferTargetRepo:   layingTransferTargetRepo,
		ProjectFlockRepo:           projectFlockRepo,
		ProjectFlockKandangRepo:    projectFlockKandangRepo,
		ProjectFlockPopulationRepo: projectFlockPopulationRepo,
		ProductWarehouseRepo:       productWarehouseRepo,
		WarehouseRepo:              warehouseRepo,
		StockLogRepo:               rStockLogs.NewStockLogRepository(repo.DB()),
		ApprovalService:            approvalService,
		FifoStockV2Svc:             fifoStockV2Svc,
	}
}

func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB {
	return db.
		Preload("CreatedUser").
		Preload("ExecutedUser").
		Preload("FromProjectFlock").
		Preload("ToProjectFlock").
		Preload("SourceProjectFlockKandang").
		Preload("SourceProjectFlockKandang.Kandang").
		Preload("SourceProductWarehouse").
		Preload("SourceProductWarehouse.Product").
		Preload("SourceProductWarehouse.Warehouse").
		Preload("Sources").
		Preload("Sources.SourceProjectFlockKandang").
		Preload("Sources.SourceProjectFlockKandang.Kandang").
		Preload("Sources.ProductWarehouse").
		Preload("Sources.ProductWarehouse.Product").
		Preload("Sources.ProductWarehouse.Warehouse").
		Preload("Targets").
		Preload("Targets.TargetProjectFlockKandang").
		Preload("Targets.TargetProjectFlockKandang.Kandang").
		Preload("Targets.ProductWarehouse").
		Preload("Targets.ProductWarehouse.Product").
		Preload("Targets.ProductWarehouse.Warehouse")
}

func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

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

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

	filterParams := &repository.GetAllFilterParams{
		Search:           params.Search,
		StartDate:        params.StartDate,
		EndDate:          params.EndDate,
		FlockSource:      params.FlockSource,
		FlockDestination: params.FlockDestination,
		Status:           params.Status,
		LocationIDs:      scope.IDs,
		LocationRestrict: scope.Restrict,
	}

	transferLayings, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, filterParams)
	if err != nil {
		s.Log.Errorf("Failed to get transferLayings: %+v", err)
		return nil, 0, err
	}

	approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
	for i, transfer := range transferLayings {
		latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, func(db *gorm.DB) *gorm.DB {
			return db.Preload("ActionUser")
		})
		if err == nil && latestApproval != nil {
			transferLayings[i].LatestApproval = latestApproval
		}
	}

	return transferLayings, total, nil
}

func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) {
	transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations)

	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get transferLaying by id: %+v", err)
		return nil, nil, err
	}

	approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
	latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	})
	if err == nil && latestApproval != nil {
		transferLaying.LatestApproval = latestApproval
	}

	return transferLaying, transferLaying.LatestApproval, nil
}

func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}
	if !m.HasPermission(c, m.P_TransferToLaying_CreateOne) {
		if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); 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: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
		commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
	); err != nil {
		return nil, err
	}

	if len(req.SourceKandangs) != 1 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Transfer to laying baru hanya mendukung 1 kandang sumber per transaksi")
	}
	sourceDetail := req.SourceKandangs[0]

	if err := s.validateKandangOwnership(
		c.Context(),
		req.SourceProjectFlockId,
		[]uint{sourceDetail.ProjectFlockKandangId},
	); err != nil {
		return nil, err
	}

	targetKandangIDs := make([]uint, len(req.TargetKandangs))
	for i, detail := range req.TargetKandangs {
		targetKandangIDs[i] = detail.ProjectFlockKandangId
	}

	if err := s.validateKandangOwnership(
		c.Context(),
		req.TargetProjectFlockId,
		targetKandangIDs,
	); err != nil {
		return nil, err
	}
	if err := s.validateTargetSourceLineage(c.Context(), sourceDetail.ProjectFlockKandangId, targetKandangIDs, 0); err != nil {
		return nil, err
	}

	transferDate, err := utils.ParseDateString(req.TransferDate)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Format tanggal transfer tidak valid")
	}

	var totalTargetQty float64
	for _, targetDetail := range req.TargetKandangs {
		if targetDetail.Quantity < 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan tidak boleh negatif")
		}
		if targetDetail.Quantity <= 0 {
			continue
		}
		totalTargetQty += targetDetail.Quantity
	}

	if totalTargetQty == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0")
	}

	sourceRequestedQty := totalTargetQty

	populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
	if err != nil {
		return nil, err
	}

	var totalPopulation float64
	var sourceProductWarehouseID uint
	for _, pop := range populations {
		totalPopulation += pop.TotalQty
		if sourceProductWarehouseID == 0 {
			sourceProductWarehouseID = pop.ProductWarehouseId
		}
	}

	if totalPopulation == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki populasi untuk ditransfer", sourceDetail.ProjectFlockKandangId))
	}
	if totalPopulation < sourceRequestedQty {
		return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d jumlah tidak mencukupi. Tersedia: %.0f, Diminta: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceRequestedQty))
	}
	if sourceProductWarehouseID == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki warehouse produk ayam", sourceDetail.ProjectFlockKandangId))
	}

	transferNumber, err := s.Repository.GenerateMovementNumber(c.Context())
	if err != nil {
		s.Log.Errorf("Failed to generate movement number: %+v", err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor transfer")
	}

	createBody := &entity.LayingTransfer{
		TransferNumber:              transferNumber,
		Notes:                       req.Reason,
		FromProjectFlockId:          req.SourceProjectFlockId,
		ToProjectFlockId:            req.TargetProjectFlockId,
		SourceProjectFlockKandangId: &sourceDetail.ProjectFlockKandangId,
		SourceProductWarehouseId:    &sourceProductWarehouseID,
		SourceRequestedQty:          sourceRequestedQty,
		SourceUsageQty:              0,
		SourcePendingUsageQty:       0,
		TransferDate:                transferDate,
		CreatedBy:                   actorID,
	}

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

		repoTx := s.Repository.WithTx(dbTransaction)
		targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
		pwRepoTx := rInventory.NewProductWarehouseRepository(dbTransaction)

		if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat record transfer laying")
		}

		sourcePW, err := pwRepoTx.GetByID(c.Context(), sourceProductWarehouseID, nil)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan warehouse produk kandang sumber")
		}

		for _, targetDetail := range req.TargetKandangs {
			if targetDetail.Quantity <= 0 {
				continue
			}

			targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan project flock kandang tujuan")
			}

			targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.KandangId)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse tidak ditemukan untuk kandang tujuan %d", targetDetail.ProjectFlockKandangId))
				}
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan warehouse tujuan")
			}

			// Cari product warehouse di target berdasarkan: warehouse + project_flock_kandang + PRODUCT
			targetPW, err := pwRepoTx.FindByProductWarehouseAndPfk(c.Context(), sourcePW.ProductId, targetWarehouse.Id, &targetDetail.ProjectFlockKandangId)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					newTargetPW := entity.ProductWarehouse{
						ProductId:             sourcePW.ProductId,
						WarehouseId:           targetWarehouse.Id,
						ProjectFlockKandangId: &targetDetail.ProjectFlockKandangId,
						Quantity:              0,
					}
					if err := pwRepoTx.CreateOne(c.Context(), &newTargetPW, nil); err != nil {
						return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk kandang tujuan %d: %v", targetDetail.ProjectFlockKandangId, err))
					}
					targetPW = &newTargetPW
				} else {
					return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mendapatkan product warehouse untuk kandang tujuan %d: %v", targetDetail.ProjectFlockKandangId, err))
				}
			}

			target := entity.LayingTransferTarget{
				LayingTransferId:            createBody.Id,
				TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
				TotalQty:                    targetDetail.Quantity,
				TotalUsed:                   0,
				ProductWarehouseId:          &targetPW.Id,
			}
			if err := targetRepoTx.CreateOne(c.Context(), &target, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat target transfer")
			}
		}

		if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat approval transfer")
		}

		return nil
	})

	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat transfer laying")
	}

	laying_transfer, _, err := s.GetOne(c, createBody.Id)
	if err != nil {
		return nil, err
	}
	s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{req.TargetProjectFlockId}, transferDate)
	return laying_transfer, nil

}

func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}
	if !m.HasPermission(c, m.P_TransferToLaying_UpdateOne) {
		if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil {
			return nil, err
		}
	}

	existingTransfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
		return db.Preload("Sources.ProductWarehouse").Preload("Targets")
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
	}
	if isLegacyTransfer(existingTransfer) {
		return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat diubah. Buat transfer baru dengan 1 kandang sumber", existingTransfer.TransferNumber))
	}

	approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
	latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
	}

	if latestApproval != nil && latestApproval.Action != nil {
		action := string(*latestApproval.Action)
		if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot update transfer laying with status %s", action))
		}
	}

	if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Source project flock not found")
	}

	if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Target project flock not found")
	}

	if len(req.SourceKandangs) != 1 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Transfer to laying baru hanya mendukung 1 kandang sumber per transaksi")
	}
	sourceDetail := req.SourceKandangs[0]

	if err := s.validateKandangOwnership(
		c.Context(),
		req.SourceProjectFlockId,
		[]uint{sourceDetail.ProjectFlockKandangId},
	); err != nil {
		return nil, err
	}

	targetKandangIDs := make([]uint, len(req.TargetKandangs))
	for i, detail := range req.TargetKandangs {
		targetKandangIDs[i] = detail.ProjectFlockKandangId
	}

	if err := s.validateKandangOwnership(
		c.Context(),
		req.TargetProjectFlockId,
		targetKandangIDs,
	); err != nil {
		return nil, err
	}
	if err := s.validateTargetSourceLineage(c.Context(), sourceDetail.ProjectFlockKandangId, targetKandangIDs, id); err != nil {
		return nil, err
	}

	transferDate, err := time.Parse("2006-01-02", req.TransferDate)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
	}

	var totalTargetQty float64
	for _, targetDetail := range req.TargetKandangs {
		if targetDetail.Quantity < 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan tidak boleh negatif")
		}
		if targetDetail.Quantity <= 0 {
			continue
		}
		totalTargetQty += targetDetail.Quantity
	}

	if totalTargetQty == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0")
	}

	sourceRequestedQty := totalTargetQty

	populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
	if err != nil {
		return nil, err
	}

	var totalPopulation float64
	var sourceProductWarehouseID uint
	for _, pop := range populations {
		totalPopulation += pop.TotalQty
		if sourceProductWarehouseID == 0 {
			sourceProductWarehouseID = pop.ProductWarehouseId
		}
	}
	if totalPopulation == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki populasi untuk ditransfer", sourceDetail.ProjectFlockKandangId))
	}
	if totalPopulation < sourceRequestedQty {
		return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d jumlah tidak mencukupi. Tersedia: %.0f, Diminta: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceRequestedQty))
	}
	if sourceProductWarehouseID == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki warehouse produk ayam", sourceDetail.ProjectFlockKandangId))
	}

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		repoTx := s.Repository.WithTx(dbTransaction)
		sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction)
		targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction)
		pwRepo := rInventory.NewProductWarehouseRepository(dbTransaction)

		// Hapus old sources dan targets
		for _, oldSource := range existingTransfer.Sources {
			if err := sourceRepo.DeleteOne(c.Context(), oldSource.Id); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old source")
			}
		}

		for _, oldTarget := range existingTransfer.Targets {
			if err := targetRepo.DeleteOne(c.Context(), oldTarget.Id); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old target")
			}
		}

		if err := repoTx.PatchOne(c.Context(), id, map[string]any{
			"transfer_date":                   transferDate,
			"notes":                           req.Reason,
			"source_project_flock_kandang_id": sourceDetail.ProjectFlockKandangId,
			"source_product_warehouse_id":     sourceProductWarehouseID,
			"source_requested_qty":            sourceRequestedQty,
			"source_usage_qty":                0,
			"source_pending_usage_qty":        0,
		}, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer header")
		}

		sourcePW, err := pwRepo.GetByID(c.Context(), sourceProductWarehouseID, nil)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
		}

		for _, targetDetail := range req.TargetKandangs {
			if targetDetail.Quantity <= 0 {
				continue
			}
			targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
			}

			targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.KandangId)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId))
				}
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
			}

			targetPW, err := pwRepo.FindByProductWarehouseAndPfk(c.Context(), sourcePW.ProductId, targetWarehouse.Id, &targetDetail.ProjectFlockKandangId)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {

					newTargetPW := entity.ProductWarehouse{
						ProductId:             sourcePW.ProductId,
						WarehouseId:           targetWarehouse.Id,
						ProjectFlockKandangId: &targetDetail.ProjectFlockKandangId,
						Quantity:              0,
					}
					if err := pwRepo.CreateOne(c.Context(), &newTargetPW, nil); err != nil {
						return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
					}
					targetPW = &newTargetPW
				} else {
					return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
				}
			}

			target := entity.LayingTransferTarget{
				LayingTransferId:            id,
				TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
				TotalQty:                    targetDetail.Quantity,
				TotalUsed:                   0,
				ProductWarehouseId:          &targetPW.Id,
			}
			if err := targetRepo.CreateOne(c.Context(), &target, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
			}
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	layingTransfer, _, err := s.GetOne(c, id)
	invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(existingTransfer.TransferDate, transferDate)
	s.invalidateDepreciationSnapshots(
		c.Context(),
		nil,
		[]uint{existingTransfer.ToProjectFlockId, req.TargetProjectFlockId},
		invalidateFromDate,
	)

	return layingTransfer, err
}

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

	transfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
		return db.Preload("Sources.ProductWarehouse").Preload("Targets")
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
		}
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
	}
	if isLegacyTransfer(transfer) {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat dihapus", transfer.TransferNumber))
	}
	if err := s.ensureNoDownstreamConsumptionForDelete(c.Context(), nil, transfer.TransferNumber, transfer.Targets); err != nil {
		return err
	}

	approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())

	latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
	}

	if latestApproval != nil && latestApproval.Action != nil {
		action := string(*latestApproval.Action)
		if action == string(entity.ApprovalActionRejected) {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action))
		}
		if action == string(entity.ApprovalActionApproved) && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero() {
			if _, err := s.Unexecute(c, id); err != nil {
				if fiberErr, ok := err.(*fiber.Error); ok {
					return fiberErr
				}
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal unexecute transfer laying sebelum delete")
			}
		}
	}
	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		repoTx := s.Repository.WithTx(dbTransaction)

		// Lock header row to keep delete deterministic after single downstream guard check.
		if _, err := repoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
			return db.Clauses(clause.Locking{Strength: "UPDATE"})
		}); err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
			}
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
		}

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

		return nil
	})

	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return fiberErr
		}
		s.Log.Errorf("Failed to delete transferLaying: %+v", err)
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
	}
	s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{transfer.ToProjectFlockId}, transfer.TransferDate)

	return nil
}

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

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

	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 _, approvableID := range approvableIDs {
		if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), approvableID); err != nil {
			return nil, err
		}
	}

	step := utils.TransferToLayingStepPengajuan
	if action == entity.ApprovalActionApproved {
		step = utils.TransferToLayingStepDisetujui
	}

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		repoTx := s.Repository.WithTx(dbTransaction)
		approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
		targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)

		for _, approvableID := range approvableIDs {
			transfer, err := repoTx.GetByID(c.Context(), approvableID, func(db *gorm.DB) *gorm.DB {
				return db.Preload("Sources")
			})
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
				}
				return err
			}
			if isLegacyTransfer(transfer) {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat diproses approval", transfer.TransferNumber))
			}

			if _, err := approvalSvcTx.CreateApproval(
				c.Context(),
				utils.ApprovalWorkflowTransferToLaying,
				approvableID,
				step,
				&action,
				actorID,
				req.Notes,
			); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
			}

			if action == entity.ApprovalActionApproved {
				if transfer.SourceProjectFlockKandangId == nil || *transfer.SourceProjectFlockKandangId == 0 {
					return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying %s tidak memiliki kandang sumber valid", transfer.TransferNumber))
				}
				targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID)
				if err != nil {
					return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil target transfer")
				}
				sourceRequestedQty := 0.0
				for _, target := range targets {
					if target.TotalQty > 0 {
						sourceRequestedQty += target.TotalQty
					}
				}
				if sourceRequestedQty <= 0 {
					return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying %s tidak memiliki total target valid", transfer.TransferNumber))
				}
				economicCutoffDate, err := s.calculateEconomicCutoffDate(c.Context(), *transfer.SourceProjectFlockKandangId)
				if err != nil {
					return err
				}

				if err := repoTx.PatchOne(c.Context(), approvableID, map[string]any{
					"economic_cutoff_date": economicCutoffDate,
					"effective_move_date":  economicCutoffDate, // Backward-compatible alias for existing clients.
					"source_requested_qty": sourceRequestedQty,
					"executed_at":          nil,
					"executed_by":          nil,
				}, nil); err != nil {
					return fiber.NewError(fiber.StatusInternalServerError, "Gagal menyimpan tanggal efektif transfer laying")
				}

				today := normalizeDateOnlyUTC(time.Now().UTC())
				physicalMoveDate := normalizeDateOnlyUTC(transfer.TransferDate)
				if !today.Before(physicalMoveDate) {
					if err := s.executeTransferInTx(
						c.Context(),
						dbTransaction,
						transfer.Id,
						actorID,
						today,
						physicalMoveDate,
						true,
					); err != nil {
						return err
					}
				}
			}
		}

		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.LayingTransfer, 0, len(approvableIDs))
	for _, approvableID := range approvableIDs {
		transfer, _, err := s.GetOne(c, approvableID)
		if err != nil {
			return nil, err
		}
		if transfer != nil {
			s.invalidateDepreciationSnapshots(
				c.Context(),
				nil,
				[]uint{transfer.ToProjectFlockId},
				resolveDepreciationEffectiveDateForTransfer(transfer),
			)
		}
		updated = append(updated, *transfer)
	}

	return updated, nil
}

func (s transferLayingService) Execute(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
	if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil {
		return nil, err
	}

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

	today := normalizeDateOnlyUTC(time.Now().UTC())
	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		return s.executeTransferInTx(
			c.Context(),
			dbTransaction,
			id,
			actorID,
			today,
			time.Now().UTC(),
			false,
		)
	})
	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengeksekusi transfer laying")
	}

	transfer, _, err := s.GetOne(c, id)
	if err != nil {
		return nil, err
	}
	if transfer != nil {
		s.invalidateDepreciationSnapshots(
			c.Context(),
			nil,
			[]uint{transfer.ToProjectFlockId},
			resolveDepreciationEffectiveDateForTransfer(transfer),
		)
	}
	return transfer, nil
}

func (s transferLayingService) ExecuteWithBusinessDate(c *fiber.Ctx, id uint, businessDate time.Time) (*entity.LayingTransfer, error) {
	if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil {
		return nil, err
	}

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

	businessDate = normalizeDateOnlyUTC(businessDate)
	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		return s.executeTransferInTx(
			c.Context(),
			dbTransaction,
			id,
			actorID,
			businessDate,
			businessDate,
			true,
		)
	})
	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal auto execute transfer laying")
	}

	transfer, _, err := s.GetOne(c, id)
	if err != nil {
		return nil, err
	}
	if transfer != nil {
		s.invalidateDepreciationSnapshots(
			c.Context(),
			nil,
			[]uint{transfer.ToProjectFlockId},
			resolveDepreciationEffectiveDateForTransfer(transfer),
		)
	}
	return transfer, nil
}

func (s transferLayingService) executeTransferInTx(
	ctx context.Context,
	tx *gorm.DB,
	id uint,
	actorID uint,
	eligibilityDate time.Time,
	executedAt time.Time,
	forceDateOnlyExecutedAt bool,
) error {
	repoTx := s.Repository.WithTx(tx)
	targetRepoTx := repository.NewLayingTransferTargetRepository(tx)
	approvalRepoTx := commonRepo.NewApprovalRepository(tx)

	transfer, err := repoTx.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
		return db.
			Clauses(clause.Locking{Strength: "UPDATE"}).
			Preload("Sources")
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return fiber.NewError(fiber.StatusNotFound, "Transfer laying tidak ditemukan")
		}
		return err
	}
	if isLegacyTransfer(transfer) {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat dieksekusi", transfer.TransferNumber))
	}
	if transfer.SourceProjectFlockKandangId == nil || *transfer.SourceProjectFlockKandangId == 0 || transfer.SourceProductWarehouseId == nil || *transfer.SourceProductWarehouseId == 0 {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying %s belum memiliki data source yang valid", transfer.TransferNumber))
	}

	if transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero() {
		return nil
	}

	latestApproval, err := approvalRepoTx.LatestByTarget(ctx, string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	if latestApproval == nil || latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved {
		return fiber.NewError(fiber.StatusBadRequest, "Transfer laying harus disetujui sebelum dieksekusi")
	}

	targets, err := targetRepoTx.GetByLayingTransferId(ctx, transfer.Id)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil target transfer laying")
	}

	if transfer.EconomicCutoffDate == nil || transfer.EconomicCutoffDate.IsZero() {
		economicCutoffDate, calcErr := s.calculateEconomicCutoffDate(ctx, *transfer.SourceProjectFlockKandangId)
		if calcErr != nil {
			return calcErr
		}
		if patchErr := repoTx.PatchOne(ctx, transfer.Id, map[string]any{
			"economic_cutoff_date": economicCutoffDate,
			"effective_move_date":  economicCutoffDate, // Keep legacy field in sync.
		}, nil); patchErr != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal menyimpan tanggal efektif transfer laying")
		}
		transfer.EconomicCutoffDate = &economicCutoffDate
		transfer.EffectiveMoveDate = &economicCutoffDate
	}

	physicalMoveDate := normalizeDateOnlyUTC(transfer.TransferDate)
	eligibilityDate = normalizeDateOnlyUTC(eligibilityDate)
	if eligibilityDate.Before(physicalMoveDate) {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying baru bisa dieksekusi mulai tanggal pindah fisik %s", physicalMoveDate.Format("2006-01-02")))
	}

	if err := s.executeApprovedTransferMovement(ctx, tx, transfer, actorID, targets); err != nil {
		return err
	}

	executedAt = executedAt.UTC()
	if forceDateOnlyExecutedAt {
		executedAt = normalizeDateOnlyUTC(executedAt)
	}
	if executedAt.IsZero() {
		executedAt = eligibilityDate
	}

	if err := repoTx.PatchOne(ctx, transfer.Id, map[string]any{
		"executed_at": executedAt,
		"executed_by": actorID,
	}, nil); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal menyimpan status eksekusi transfer laying")
	}

	return nil
}

func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
	if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil {
		return nil, err
	}

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

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		repoTx := s.Repository.WithTx(dbTransaction)
		targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
		approvalRepoTx := commonRepo.NewApprovalRepository(dbTransaction)
		stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction)

		transfer, err := repoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
			return db.Preload("Sources")
		})
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusNotFound, "Transfer laying tidak ditemukan")
			}
			return err
		}
		if isLegacyTransfer(transfer) {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat di-unexecute", transfer.TransferNumber))
		}
		if transfer.SourceProductWarehouseId == nil || *transfer.SourceProductWarehouseId == 0 {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying %s belum memiliki source warehouse valid", transfer.TransferNumber))
		}

		if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
			return nil
		}

		latestApproval, err := approvalRepoTx.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil)
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		if latestApproval == nil || latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved {
			return fiber.NewError(fiber.StatusBadRequest, "Transfer laying hanya bisa di-unexecute saat status approval APPROVED")
		}

		targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), transfer.Id)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil target transfer laying")
		}
		if len(targets) == 0 {
			return fiber.NewError(fiber.StatusBadRequest, "Transfer laying belum memiliki target")
		}
		physicalMoveDate := normalizeDateOnlyUTC(transfer.TransferDate)
		for _, target := range targets {
			if target.TotalUsed > 1e-6 {
				return fiber.NewError(
					fiber.StatusBadRequest,
					fmt.Sprintf("Transfer laying %s tidak dapat di-unexecute karena target kandang %d sudah dipakai downstream", transfer.TransferNumber, target.TargetProjectFlockKandangId),
				)
			}
			hasDownstreamRecording, firstRecordingDate, err := s.hasDownstreamRecordingOnTarget(c.Context(), dbTransaction, target.TargetProjectFlockKandangId, physicalMoveDate)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi recording downstream pada target transfer laying")
			}
			if hasDownstreamRecording {
				return fiber.NewError(
					fiber.StatusBadRequest,
					fmt.Sprintf("Transfer laying %s tidak dapat di-unexecute karena target kandang %d sudah memiliki recording laying downstream sejak %s", transfer.TransferNumber, target.TargetProjectFlockKandangId, firstRecordingDate.Format("2006-01-02")),
				)
			}
		}

		flagGroupCode, err := resolveTransferLayingUsableFlagGroupByProductWarehouse(
			c.Context(),
			dbTransaction,
			*transfer.SourceProductWarehouseId,
		)
		if err != nil {
			return fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf("Konfigurasi FIFO v2 transfer laying tidak valid: %v", err),
			)
		}
		activeConsumeAllocCount, err := s.countActiveTransferSourceConsumeAllocations(
			c.Context(),
			dbTransaction,
			transfer.Id,
			*transfer.SourceProductWarehouseId,
		)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi alokasi FIFO source transfer laying")
		}
		if transfer.SourceUsageQty > 1e-6 && activeConsumeAllocCount == 0 {
			return fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf("Unexecute transfer laying %s gagal: alokasi FIFO source tidak ditemukan", transfer.TransferNumber),
			)
		}

		type targetReflowKey struct {
			productWarehouseID uint
		}
		targetReflow := make(map[targetReflowKey]struct{})

		for _, target := range targets {
			if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
				return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
			}
			if target.TotalQty <= 0 {
				continue
			}

			stockLogDecrease := &entity.StockLog{
				ProductWarehouseId: *target.ProductWarehouseId,
				CreatedBy:          actorID,
				Increase:           0,
				Decrease:           target.TotalQty,
				LoggableType:       string(utils.StockLogTypeTransferLaying),
				LoggableId:         transfer.Id,
				Notes:              fmt.Sprintf("TL UNEXEC #%s", transfer.TransferNumber),
			}
			stockLogs, err := stockLogRepoTx.GetByProductWarehouse(c.Context(), *target.ProductWarehouseId, 1)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
			}
			if len(stockLogs) > 0 {
				latestStockLog := stockLogs[0]
				stockLogDecrease.Stock = latestStockLog.Stock - stockLogDecrease.Decrease
			} else {
				stockLogDecrease.Stock -= stockLogDecrease.Decrease
			}
			if err := stockLogRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar target saat unexecute")
			}

			if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]any{
				"total_qty":  0,
				"total_used": 0,
			}, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal rollback kuantitas target transfer laying")
			}
			targetReflow[targetReflowKey{productWarehouseID: *target.ProductWarehouseId}] = struct{}{}
		}
		asOf := normalizeDateOnlyUTC(transfer.TransferDate)
		for key := range targetReflow {
			if err := reflowTransferLayingScope(c.Context(), s.FifoStockV2Svc, dbTransaction, key.productWarehouseID, &asOf); err != nil {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 target transfer laying: %v", err))
			}
		}

		rollbackResult, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
			ProductWarehouseID: *transfer.SourceProductWarehouseId,
			Usable: commonSvc.FifoStockV2Ref{
				ID:            transfer.Id,
				LegacyTypeKey: fifo.UsableKeyTransferToLayingOut.String(),
				FunctionCode:  transferLayingOutFunctionCode,
			},
			Reason: fmt.Sprintf("transfer laying unexecute #%s [%s]", transfer.TransferNumber, flagGroupCode),
			Tx:     dbTransaction,
		})
		if err != nil {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 source transfer laying: %v", err))
		}
		releasedQty := 0.0
		if rollbackResult != nil {
			releasedQty = rollbackResult.ReleasedQty
		}
		if transfer.SourceUsageQty > 1e-6 && releasedQty < transfer.SourceUsageQty-1e-6 {
			return fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf(
					"Rollback FIFO v2 source transfer laying tidak lengkap. Dibutuhkan %.3f, terlepas %.3f",
					transfer.SourceUsageQty,
					releasedQty,
				),
			)
		}

		if err := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
			"source_usage_qty":         0,
			"source_pending_usage_qty": 0,
		}, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal reset kuantitas source transfer laying")
		}
		if err := fifoV2.ReleasePopulationConsumptionByUsable(
			c.Context(),
			dbTransaction,
			fifo.UsableKeyTransferToLayingOut.String(),
			transfer.Id,
		); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal rollback pemakaian population sumber transfer laying")
		}
		if transfer.SourceProjectFlockKandangId != nil && *transfer.SourceProjectFlockKandangId != 0 {
			if err := s.resyncPopulationUsageByProjectFlockKandang(c.Context(), dbTransaction, *transfer.SourceProjectFlockKandangId); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal sinkronisasi population source setelah unexecute transfer laying")
			}
		}

		now := time.Now().UTC()
		if err := dbTransaction.WithContext(c.Context()).
			Model(&entity.StockAllocation{}).
			Where("stockable_type = ? AND stockable_id = ? AND status = ? AND allocation_purpose = ?",
				fifo.UsableKeyTransferToLayingOut.String(),
				transfer.Id,
				entity.StockAllocationStatusActive,
				entity.StockAllocationPurposeConsume,
			).
			Updates(map[string]any{
				"status":      entity.StockAllocationStatusReleased,
				"released_at": now,
				"note":        fmt.Sprintf("TL UNEXEC #%s", transfer.TransferNumber),
			}).Error; err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal release mapping source-target transfer laying")
		}

		if transfer.SourceUsageQty > 0 {
			stockLogIncrease := &entity.StockLog{
				ProductWarehouseId: *transfer.SourceProductWarehouseId,
				CreatedBy:          actorID,
				Increase:           transfer.SourceUsageQty,
				Decrease:           0,
				LoggableType:       string(utils.StockLogTypeTransferLaying),
				LoggableId:         transfer.Id,
				Notes:              fmt.Sprintf("TL UNEXEC #%s", transfer.TransferNumber),
			}
			stockLogs, err := stockLogRepoTx.GetByProductWarehouse(c.Context(), *transfer.SourceProductWarehouseId, 1)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
			}
			if len(stockLogs) > 0 {
				latestStockLog := stockLogs[0]
				stockLogIncrease.Stock = latestStockLog.Stock + stockLogIncrease.Increase
			} else {
				stockLogIncrease.Stock += stockLogIncrease.Increase
			}
			if err := stockLogRepoTx.CreateOne(c.Context(), stockLogIncrease, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk source saat unexecute")
			}
		}

		if err := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
			"executed_at": nil,
			"executed_by": nil,
		}, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal reset status eksekusi transfer laying")
		}

		return nil
	})
	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal melakukan unexecute transfer laying")
	}

	transfer, _, err := s.GetOne(c, id)
	if err != nil {
		return nil, err
	}
	if transfer != nil {
		s.invalidateDepreciationSnapshots(
			c.Context(),
			nil,
			[]uint{transfer.ToProjectFlockId},
			resolveDepreciationEffectiveDateForTransfer(transfer),
		)
	}
	return transfer, nil
}

func (s *transferLayingService) executeApprovedTransferMovement(
	ctx context.Context,
	tx *gorm.DB,
	transfer *entity.LayingTransfer,
	actorID uint,
	targets []entity.LayingTransferTarget,
) error {
	if transfer == nil || transfer.Id == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Transfer laying tidak valid")
	}
	if transfer.SourceProjectFlockKandangId == nil || *transfer.SourceProjectFlockKandangId == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Transfer laying belum memiliki kandang sumber")
	}
	if transfer.SourceProductWarehouseId == nil || *transfer.SourceProductWarehouseId == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Transfer laying belum memiliki source product warehouse")
	}
	if len(targets) == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Transfer laying belum memiliki target")
	}
	if s.FifoStockV2Svc == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
	}

	stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
	targetRepoTx := repository.NewLayingTransferTargetRepository(tx)
	stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
	repoTx := s.Repository.WithTx(tx)

	totalTargetQty := 0.0
	for _, target := range targets {
		totalTargetQty += target.TotalQty
	}
	if totalTargetQty <= 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Total kuantitas target transfer laying harus lebih dari 0")
	}

	if transfer.SourceRequestedQty <= 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Total kuantitas sumber transfer laying harus lebih dari 0")
	}

	if err := repoTx.PatchOne(ctx, transfer.Id, map[string]any{
		"source_usage_qty":         transfer.SourceUsageQty + totalTargetQty,
		"source_pending_usage_qty": 0,
	}, nil); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty transfer laying")
	}

	asOf := normalizeDateOnlyUTC(transfer.TransferDate)
	if _, err := s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
		FlagGroupCode:      transferToLayingFlagGroupCode,
		ProductWarehouseID: *transfer.SourceProductWarehouseId,
		AsOf:               &asOf,
		Tx:                 tx,
	}); err != nil {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal consume FIFO v2 stock: %v", err))
	}

	refreshedTransfer, err := repoTx.GetByID(ctx, transfer.Id, nil)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal refresh transfer laying setelah reflow")
	}
	usageDelta := refreshedTransfer.SourceUsageQty - transfer.SourceUsageQty
	pendingQty := refreshedTransfer.SourcePendingUsageQty
	if pendingQty > 1e-6 || usageDelta < totalTargetQty-1e-6 {
		return fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf("Stok sumber tidak mencukupi untuk mengeksekusi transfer laying %s", transfer.TransferNumber),
		)
	}

	if err := s.allocatePopulationForTransfer(ctx, tx, transfer.Id, *transfer.SourceProjectFlockKandangId, *transfer.SourceProductWarehouseId, usageDelta); err != nil {
		return err
	}

	for _, target := range targets {
		if target.TotalQty <= 0 {
			continue
		}
		mappingAllocation := &entity.StockAllocation{
			StockableType:      fifo.UsableKeyTransferToLayingOut.String(),
			StockableId:        transfer.Id,
			UsableType:         fifo.StockableKeyTransferToLayingIn.String(),
			UsableId:           target.Id,
			ProductWarehouseId: *transfer.SourceProductWarehouseId,
			Qty:                target.TotalQty,
			Status:             entity.StockAllocationStatusActive,
		}
		if err := stockAllocationRepo.CreateOne(ctx, mappingAllocation, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target")
		}
	}

	stockLogDecrease := &entity.StockLog{
		ProductWarehouseId: *transfer.SourceProductWarehouseId,
		CreatedBy:          actorID,
		Increase:           0,
		Decrease:           usageDelta,
		LoggableType:       string(utils.StockLogTypeTransferLaying),
		LoggableId:         transfer.Id,
		Notes:              fmt.Sprintf("TL #%s", transfer.TransferNumber),
	}
	stockLogs, err := stockLogRepoTx.GetByProductWarehouse(ctx, *transfer.SourceProductWarehouseId, 1)
	if err != nil {
		s.Log.Errorf("Failed to get stock logs: %+v", err)
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
	}
	if len(stockLogs) > 0 {
		latestStockLog := stockLogs[0]
		stockLogDecrease.Stock = latestStockLog.Stock - stockLogDecrease.Decrease
	} else {
		stockLogDecrease.Stock -= stockLogDecrease.Decrease
	}

	if err := stockLogRepoTx.CreateOne(ctx, stockLogDecrease, nil); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
	}

	type targetReflowKey struct {
		productWarehouseID uint
	}
	targetReflow := make(map[targetReflowKey]struct{})

	for _, target := range targets {
		if target.ProductWarehouseId == nil {
			return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
		}

		if err := targetRepoTx.PatchOne(ctx, target.Id, map[string]any{
			"total_qty": target.TotalQty,
		}, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
		}
		targetReflow[targetReflowKey{productWarehouseID: *target.ProductWarehouseId}] = struct{}{}

		stockLogIncrease := &entity.StockLog{
			ProductWarehouseId: *target.ProductWarehouseId,
			CreatedBy:          actorID,
			Increase:           target.TotalQty,
			Decrease:           0,
			LoggableType:       string(utils.StockLogTypeTransferLaying),
			LoggableId:         transfer.Id,
			Notes:              fmt.Sprintf("TL #%s", transfer.TransferNumber),
		}
		stockLogs, err := stockLogRepoTx.GetByProductWarehouse(ctx, *target.ProductWarehouseId, 1)
		if err != nil {
			s.Log.Errorf("Failed to get stock logs: %+v", err)
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
		}
		if len(stockLogs) > 0 {
			latestStockLog := stockLogs[0]
			stockLogIncrease.Stock = latestStockLog.Stock + stockLogIncrease.Increase
		} else {
			stockLogIncrease.Stock += stockLogIncrease.Increase
		}

		if err := stockLogRepoTx.CreateOne(ctx, stockLogIncrease, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
		}
	}
	for key := range targetReflow {
		if err := reflowTransferLayingScope(ctx, s.FifoStockV2Svc, tx, key.productWarehouseID, &asOf); err != nil {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow FIFO v2 target transfer laying: %v", err))
		}
	}

	return nil
}

func (s *transferLayingService) allocatePopulationForTransfer(
	ctx context.Context,
	tx *gorm.DB,
	transferID uint,
	sourceProjectFlockKandangID uint,
	sourceProductWarehouseID uint,
	consumeQty float64,
) error {
	if consumeQty <= 0 {
		return nil
	}
	if tx == nil {
		return errors.New("transaction is required")
	}
	if sourceProjectFlockKandangID == 0 || sourceProductWarehouseID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang sumber atau product warehouse tidak valid")
	}

	populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(
		ctx,
		sourceProjectFlockKandangID,
		sourceProductWarehouseID,
	)
	if err != nil {
		return err
	}
	if len(populations) == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk transfer laying")
	}

	return fifoV2.AllocatePopulationConsumption(
		ctx,
		tx,
		populations,
		sourceProductWarehouseID,
		fifo.UsableKeyTransferToLayingOut.String(),
		transferID,
		consumeQty,
	)
}

func (s *transferLayingService) calculateEconomicCutoffDate(ctx context.Context, sourceProjectFlockKandangID uint) (time.Time, error) {
	if sourceProjectFlockKandangID == 0 {
		return time.Time{}, fiber.NewError(fiber.StatusBadRequest, "Kandang sumber transfer laying tidak valid")
	}

	maxGrowingWeek := config.TransferToLayingGrowingMaxWeek
	if maxGrowingWeek <= 0 {
		maxGrowingWeek = 19
	}

	baselineChickInDate, err := s.resolveSourceChickInDate(ctx, sourceProjectFlockKandangID)
	if err != nil {
		return time.Time{}, err
	}

	if baselineChickInDate.IsZero() {
		return time.Time{}, fiber.NewError(fiber.StatusBadRequest, "Tanggal chick in sumber transfer laying tidak ditemukan")
	}

	economicCutoffDate := baselineChickInDate.AddDate(0, 0, maxGrowingWeek*7)
	return normalizeDateOnlyUTC(economicCutoffDate), nil
}

func (s *transferLayingService) resolveSourceChickInDate(ctx context.Context, sourceProjectFlockKandangID uint) (time.Time, error) {
	if sourceProjectFlockKandangID == 0 {
		return time.Time{}, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang sumber tidak valid")
	}

	populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, sourceProjectFlockKandangID)
	if err != nil {
		return time.Time{}, err
	}

	var earliestChickInDate time.Time
	for _, population := range populations {
		if population.ProjectChickin == nil || population.ProjectChickin.ChickInDate.IsZero() {
			continue
		}
		chickInDate := normalizeDateOnlyUTC(population.ProjectChickin.ChickInDate)
		if earliestChickInDate.IsZero() || chickInDate.Before(earliestChickInDate) {
			earliestChickInDate = chickInDate
		}
	}

	if earliestChickInDate.IsZero() {
		return time.Time{}, fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf("Tanggal chick in untuk kandang sumber %d tidak ditemukan", sourceProjectFlockKandangID),
		)
	}

	return earliestChickInDate, nil
}

func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayingID uint, actorID uint) error {
	if transferLayingID == 0 || actorID == 0 {
		return nil
	}

	approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
	action := entity.ApprovalActionCreated

	_, err := approvalSvc.CreateApproval(
		ctx,
		utils.ApprovalWorkflowTransferToLaying,
		transferLayingID,
		utils.TransferToLayingStepPengajuan,
		&action,
		actorID,
		nil,
	)
	return err
}

func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {

	pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
		return db
	})
	if err != nil {
		s.Log.Errorf("Failed to get project flock %d: %+v", projectFlockID, err)
		return nil, nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
	}

	kandangs, _, err := s.ProjectFlockKandangRepo.GetAll(ctx.Context(), 0, 1000, func(db *gorm.DB) *gorm.DB {
		return db.Where("project_flock_id = ?", projectFlockID).Order("kandang_id ASC")
	})
	if err != nil {
		s.Log.Errorf("Failed to get kandangs for project flock %d: %+v", projectFlockID, err)
		return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
	}

	kandangAvailableQty := make(map[uint]float64)
	for _, kandang := range kandangs {
		// Gunakan fungsi repository yang sama dengan recording service
		totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
		if err != nil {
			s.Log.Warnf("Failed to get available qty for kandang %d: %+v", kandang.Id, err)
			kandangAvailableQty[kandang.Id] = 0
			continue
		}

		kandangAvailableQty[kandang.Id] = totalAvailable
	}

	return pf, kandangAvailableQty, nil
}

func (s *transferLayingService) validateKandangOwnership(
	ctx context.Context,
	projectFlockID uint,
	kandangIDs []uint,
) error {

	for _, kandangID := range kandangIDs {

		projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(ctx, kandangID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang %d tidak ditemukan", kandangID))
			}
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to get project flock kandang")
		}

		if projectFlockKandang.ProjectFlockId != projectFlockID {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak terhubung ke project flock %d", kandangID, projectFlockID))
		}
	}

	return nil
}

func (s *transferLayingService) validateTargetSourceLineage(
	ctx context.Context,
	sourceProjectFlockKandangID uint,
	targetKandangIDs []uint,
	excludeTransferID uint,
) error {
	if sourceProjectFlockKandangID == 0 || len(targetKandangIDs) == 0 {
		return nil
	}

	seen := make(map[uint]struct{}, len(targetKandangIDs))
	for _, targetKandangID := range targetKandangIDs {
		if targetKandangID == 0 {
			continue
		}
		if _, exists := seen[targetKandangID]; exists {
			continue
		}
		seen[targetKandangID] = struct{}{}

		existingTransfer, err := s.Repository.GetLatestApprovedByTargetKandang(ctx, targetKandangID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				continue
			}
			s.Log.Errorf("Failed to validate transfer lineage for target kandang %d: %+v", targetKandangID, err)
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi sumber transfer ke laying")
		}
		if existingTransfer == nil {
			continue
		}
		if excludeTransferID != 0 && existingTransfer.Id == excludeTransferID {
			continue
		}

		existingSourceID := uint(0)
		if existingTransfer.SourceProjectFlockKandangId != nil && *existingTransfer.SourceProjectFlockKandangId != 0 {
			existingSourceID = *existingTransfer.SourceProjectFlockKandangId
		}
		if existingSourceID == 0 && s.LayingTransferSourceRepo != nil {
			sources, sourceErr := s.LayingTransferSourceRepo.GetByLayingTransferId(ctx, existingTransfer.Id)
			if sourceErr != nil {
				s.Log.Errorf("Failed to resolve transfer sources for lineage validation transfer=%d: %+v", existingTransfer.Id, sourceErr)
				return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi sumber transfer ke laying")
			}
			for _, source := range sources {
				if source.SourceProjectFlockKandangId != 0 {
					existingSourceID = source.SourceProjectFlockKandangId
					break
				}
			}
		}
		if existingSourceID == 0 {
			continue
		}
		if existingSourceID == sourceProjectFlockKandangID {
			continue
		}

		return fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf(
				"Kandang tujuan %d sudah memiliki lineage sumber kandang %d dari transfer %s. Tidak boleh ganti ke sumber kandang %d.",
				targetKandangID,
				existingSourceID,
				existingTransfer.TransferNumber,
				sourceProjectFlockKandangID,
			),
		)
	}

	return nil
}

func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) {

	if err := commonSvc.EnsureRelations(c.Context(),
		commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
	); err != nil {
		return nil, err
	}

	projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
	if err != nil {
		return nil, err
	}

	kandangMaxTargetQty := make(map[uint]float64)
	for _, projectFlockKandang := range projectFlockKandangs {

		capacity := projectFlockKandang.Kandang.Capacity

		availableQty, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(
			c.Context(),
			projectFlockKandang.Id,
		)
		if err != nil {
			return nil, err
		}

		kandangMaxTargetQty[projectFlockKandang.Id] = capacity - availableQty

		if kandangMaxTargetQty[projectFlockKandang.Id] < 0 {
			kandangMaxTargetQty[projectFlockKandang.Id] = 0
		}
	}

	return kandangMaxTargetQty, nil
}

func (s *transferLayingService) hasDownstreamRecordingOnTarget(
	ctx context.Context,
	tx *gorm.DB,
	targetProjectFlockKandangID uint,
	sinceDate time.Time,
) (bool, time.Time, error) {
	targetRepo := s.LayingTransferTargetRepo
	if tx != nil {
		targetRepo = repository.NewLayingTransferTargetRepository(tx)
	}

	recordDate, err := targetRepo.GetEarliestRecordingDateByTarget(ctx, targetProjectFlockKandangID, sinceDate)
	if err != nil {
		return false, time.Time{}, err
	}
	if recordDate == nil {
		return false, time.Time{}, nil
	}
	return true, normalizeDateOnlyUTC(*recordDate), nil
}

func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
	ctx context.Context,
	tx *gorm.DB,
	transferID uint,
	productWarehouseID uint,
) (int64, error) {
	targetRepo := s.LayingTransferTargetRepo
	if tx != nil {
		targetRepo = repository.NewLayingTransferTargetRepository(tx)
	}
	return targetRepo.CountActiveTransferSourceConsumeAllocations(ctx, transferID, productWarehouseID)
}

func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
	targetRepo := s.LayingTransferTargetRepo
	if tx != nil {
		targetRepo = repository.NewLayingTransferTargetRepository(tx)
	}
	return targetRepo.SyncPopulationUsageByProjectFlockKandang(ctx, projectFlockKandangID)
}

func sortedIDs(input map[uint]struct{}) []uint {
	if len(input) == 0 {
		return nil
	}
	out := make([]uint, 0, len(input))
	for id := range input {
		if id == 0 {
			continue
		}
		out = append(out, id)
	}
	sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
	return out
}

func joinUint(values []uint) string {
	if len(values) == 0 {
		return "-"
	}
	parts := make([]string, 0, len(values))
	for _, value := range values {
		parts = append(parts, fmt.Sprintf("%d", value))
	}
	return strings.Join(parts, "|")
}

func normalizeDateOnlyUTC(value time.Time) time.Time {
	return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
}

func resolveDepreciationEffectiveDateForTransfer(transfer *entity.LayingTransfer) time.Time {
	if transfer == nil {
		return time.Time{}
	}
	if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() {
		return *transfer.EffectiveMoveDate
	}
	if transfer.EconomicCutoffDate != nil && !transfer.EconomicCutoffDate.IsZero() {
		return *transfer.EconomicCutoffDate
	}
	return transfer.TransferDate
}

func (s transferLayingService) invalidateDepreciationSnapshots(
	ctx context.Context,
	tx *gorm.DB,
	farmIDs []uint,
	fromDate time.Time,
) {
	if fromDate.IsZero() {
		return
	}
	targetDB := s.Repository.DB()
	if tx != nil {
		targetDB = tx
	}
	uniqueFarmIDs := utils.UniqueUintSlice(farmIDs)
	if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, uniqueFarmIDs, fromDate); err != nil {
		s.Log.Warnf(
			"Failed to invalidate farm depreciation snapshots (farms=%v, from=%s): %+v",
			uniqueFarmIDs,
			fromDate.Format("2006-01-02"),
			err,
		)
	}
}

func isLegacyTransfer(transfer *entity.LayingTransfer) bool {
	if transfer == nil {
		return false
	}
	if transfer.SourceProjectFlockKandangId == nil || *transfer.SourceProjectFlockKandangId == 0 {
		return true
	}
	if len(transfer.Sources) > 0 {
		return true
	}
	return false
}

func (s *transferLayingService) ensureNoDownstreamConsumptionForDelete(
	ctx context.Context,
	tx *gorm.DB,
	transferNumber string,
	targets []entity.LayingTransferTarget,
) error {
	targetIDs := make([]uint, 0, len(targets))
	for _, target := range targets {
		if target.Id == 0 {
			continue
		}
		targetIDs = append(targetIDs, target.Id)
	}
	if len(targetIDs) == 0 {
		return nil
	}

	targetRepo := s.LayingTransferTargetRepo
	if tx != nil {
		targetRepo = repository.NewLayingTransferTargetRepository(tx)
	}

	rows, err := targetRepo.GetActiveDownstreamConsumptions(ctx, targetIDs)
	if err != nil {
		s.Log.Errorf("Failed to validate downstream consumption for transfer laying %s: %+v", transferNumber, err)
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transaksi turunan transfer laying")
	}
	if len(rows) == 0 {
		return nil
	}

	dependencyMap := make(map[string]map[uint]struct{})
	for _, row := range rows {
		label := mapTransferLayingDownstreamUsableLabel(row.UsableType)
		if _, ok := dependencyMap[label]; !ok {
			dependencyMap[label] = make(map[uint]struct{})
		}
		dependencyMap[label][row.UsableID] = struct{}{}
	}

	labels := make([]string, 0, len(dependencyMap))
	for label := range dependencyMap {
		labels = append(labels, label)
	}
	sort.Strings(labels)

	details := make([]string, 0, len(labels))
	for _, label := range labels {
		details = append(details, fmt.Sprintf("%s=%s", label, joinUint(sortedIDs(dependencyMap[label]))))
	}

	return fiber.NewError(
		fiber.StatusBadRequest,
		fmt.Sprintf(
			"%s Transfer %s. Dependensi aktif: %s.",
			transferLayingDeleteDownstreamGuardMessage,
			transferNumber,
			strings.Join(details, ", "),
		),
	)
}

func mapTransferLayingDownstreamUsableLabel(usableType string) string {
	switch strings.ToUpper(strings.TrimSpace(usableType)) {
	case fifo.UsableKeyRecordingStock.String(), fifo.UsableKeyRecordingDepletion.String():
		return "Recording"
	case fifo.UsableKeyProjectChickin.String():
		return "Chickin"
	case fifo.UsableKeyMarketingDelivery.String():
		return "Marketing"
	case fifo.UsableKeyTransferToLayingOut.String():
		return "TransferToLaying"
	case fifo.UsableKeyStockTransferOut.String():
		return "TransferStock"
	case fifo.UsableKeyAdjustmentOut.String():
		return "Adjustment"
	default:
		return strings.ToUpper(strings.TrimSpace(usableType))
	}
}
