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"
	sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
	sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
	rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	sProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
	rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
	sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
	rStockLogs "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"
	fifo "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
	recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording"

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

type RecordingService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.Recording, error)
	GetNextDay(ctx *fiber.Ctx, req *validation.GetRecordingNextDay) (int, error)
	CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
	Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
	GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error)
}

type recordingService struct {
	Log                        *logrus.Logger
	Validate                   *validator.Validate
	Repository                 repository.RecordingRepository
	ProjectFlockKandangRepo    rProjectFlock.ProjectFlockKandangRepository
	ProductWarehouseRepo       rProductWarehouse.ProductWarehouseRepository
	ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
	ApprovalRepo               commonRepo.ApprovalRepository
	ApprovalSvc                commonSvc.ApprovalService
	ProductionStandardSvc      sProductionStandard.ProductionStandardService
	ProjectFlockSvc            sProjectFlock.ProjectflockService
	ChickinSvc                 sChickin.ChickinService
	TransferLayingRepo         rTransferLaying.TransferLayingRepository
	TransferLayingSvc          sTransferLaying.TransferLayingService
	FifoStockV2Svc             commonSvc.FifoStockV2Service
	StockLogRepo               rStockLogs.StockLogRepository
}

func NewRecordingService(
	repo repository.RecordingRepository,
	projectFlockKandangRepo rProjectFlock.ProjectFlockKandangRepository,
	productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
	projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
	approvalRepo commonRepo.ApprovalRepository,
	approvalSvc commonSvc.ApprovalService,
	fifoStockV2Svc commonSvc.FifoStockV2Service,
	stockLogRepo rStockLogs.StockLogRepository,
	productionStandardSvc sProductionStandard.ProductionStandardService,
	projectFlockSvc sProjectFlock.ProjectflockService,
	chickinSvc sChickin.ChickinService,
	transferLayingRepo rTransferLaying.TransferLayingRepository,
	transferLayingSvc sTransferLaying.TransferLayingService,
	validate *validator.Validate,
) RecordingService {
	return &recordingService{
		Log:                        utils.Log,
		Validate:                   validate,
		Repository:                 repo,
		ProjectFlockKandangRepo:    projectFlockKandangRepo,
		ProductWarehouseRepo:       productWarehouseRepo,
		ProjectFlockPopulationRepo: projectFlockPopulationRepo,
		ApprovalRepo:               approvalRepo,
		ApprovalSvc:                approvalSvc,
		ProductionStandardSvc:      productionStandardSvc,
		ProjectFlockSvc:            projectFlockSvc,
		ChickinSvc:                 chickinSvc,
		TransferLayingRepo:         transferLayingRepo,
		TransferLayingSvc:          transferLayingSvc,
		FifoStockV2Svc:             fifoStockV2Svc,
		StockLogRepo:               stockLogRepo,
	}
}

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

	var scopeErr error
	if params.ProjectFlockKandangId != 0 {
		if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, params.ProjectFlockKandangId); err != nil {
			return nil, 0, err
		}
	}

	recordings, total, err := s.Repository.GetAllWithFilters(
		c.Context(),
		params.Offset,
		params.Limit,
		params,
		func(db *gorm.DB) *gorm.DB {
			db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id")
			return db
		},
	)

	if scopeErr != nil {
		return nil, 0, scopeErr
	}
	if err != nil {
		s.Log.Errorf("Failed to get recordings: %+v", err)
		return nil, 0, err
	}
	if err := recordingutil.AttachLatestApprovals(c.Context(), recordings, s.ApprovalSvc, s.Log); err != nil {
		return nil, 0, err
	}
	recordingPtrs := make([]*entity.Recording, 0, len(recordings))
	for i := range recordings {
		recordingPtrs = append(recordingPtrs, &recordings[i])
	}
	if err := recordingutil.AttachProductionStandards(c.Context(), s.Repository.DB(), true, s.Log, recordingPtrs...); err != nil {
		return nil, 0, err
	}
	dbCtx := s.Repository.DB().WithContext(c.Context())
	recordingIDs := make([]uint, 0, len(recordings))
	projectFlockIDs := make(map[uint]struct{})
	for i := range recordings {
		if recordings[i].Id != 0 {
			recordingIDs = append(recordingIDs, recordings[i].Id)
		}
		if recordings[i].ProjectFlockKandangId != 0 {
			projectFlockIDs[recordings[i].ProjectFlockKandangId] = struct{}{}
		}
	}

	cumulativeMap, err := s.Repository.GetCumulativeDepletionByRecordingIDs(dbCtx, recordingIDs)
	if err != nil {
		return nil, 0, err
	}
	prevChickMap, err := s.Repository.GetPreviousTotalChickByRecordingIDs(dbCtx, recordingIDs)
	if err != nil {
		return nil, 0, err
	}
	pfkIDs := make([]uint, 0, len(projectFlockIDs))
	for id := range projectFlockIDs {
		pfkIDs = append(pfkIDs, id)
	}
	totalChickMap, err := s.Repository.GetTotalChickByProjectFlockKandangIDs(dbCtx, pfkIDs)
	if err != nil {
		return nil, 0, err
	}

	cutOverChickinAvailability := make(map[uint]bool)
	for i := range recordings {
		if recordings[i].ProjectFlockKandangId != 0 && !recordings[i].RecordDatetime.IsZero() {
			total := cumulativeMap[recordings[i].Id]
			recordings[i].TotalDepletionCumQty = &total
		}

		current := 0.0
		if recordings[i].TotalDepletionQty != nil {
			current = *recordings[i].TotalDepletionQty
		}
		var prev *entity.Recording
		if prevTotal, ok := prevChickMap[recordings[i].Id]; ok && prevTotal != nil {
			prev = &entity.Recording{TotalChickQty: prevTotal}
		}
		totalChick := totalChickMap[recordings[i].ProjectFlockKandangId]
		rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
		recordings[i].DepletionRate = &rate

		populationCanChange, transferExecuted, isTransition, isLaying, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), &recordings[i])
		if stateErr != nil {
			return nil, 0, stateErr
		}
		isTransition, isLaying, stateErr = s.applyCutOverLayingRecordingLookupOverride(c.Context(), &recordings[i], isTransition, isLaying, cutOverChickinAvailability)
		if stateErr != nil {
			return nil, 0, stateErr
		}
		recordings[i].PopulationCanChange = boolPtr(populationCanChange)
		recordings[i].TransferExecuted = boolPtr(transferExecuted)
		recordings[i].IsTransition = boolPtr(isTransition)
		recordings[i].IsLaying = boolPtr(isLaying)
	}
	return recordings, total, nil
}

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

func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) {
	if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
		return nil, err
	}

	recording, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
		return s.Repository.WithRelations(db)
	})
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Recording not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get recording by id: %+v", err)
		return nil, err
	}
	if err := recordingutil.AttachLatestApproval(c.Context(), recording, s.ApprovalSvc, s.Log); err != nil {
		return nil, err
	}
	if err := recordingutil.AttachProductionStandards(c.Context(), s.Repository.DB(), false, s.Log, recording); err != nil {
		return nil, err
	}
	if recording.ProjectFlockKandangId != 0 {
		dbCtx := s.Repository.DB().WithContext(c.Context())
		ids := []uint{recording.Id}

		cumulativeMap, err := s.Repository.GetCumulativeDepletionByRecordingIDs(dbCtx, ids)
		if err != nil {
			return nil, err
		}
		prevChickMap, err := s.Repository.GetPreviousTotalChickByRecordingIDs(dbCtx, ids)
		if err != nil {
			return nil, err
		}
		totalChickMap, err := s.Repository.GetTotalChickByProjectFlockKandangIDs(dbCtx, []uint{recording.ProjectFlockKandangId})
		if err != nil {
			return nil, err
		}

		if !recording.RecordDatetime.IsZero() {
			total := cumulativeMap[recording.Id]
			recording.TotalDepletionCumQty = &total
		}

		current := 0.0
		if recording.TotalDepletionQty != nil {
			current = *recording.TotalDepletionQty
		}
		var prev *entity.Recording
		if prevTotal, ok := prevChickMap[recording.Id]; ok && prevTotal != nil {
			prev = &entity.Recording{TotalChickQty: prevTotal}
		}
		totalChick := totalChickMap[recording.ProjectFlockKandangId]
		rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
		recording.DepletionRate = &rate
	}

	populationCanChange, transferExecuted, isTransition, isLaying, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), recording)
	if stateErr != nil {
		return nil, stateErr
	}
	isTransition, isLaying, stateErr = s.applyCutOverLayingRecordingLookupOverride(c.Context(), recording, isTransition, isLaying, nil)
	if stateErr != nil {
		return nil, stateErr
	}
	recording.PopulationCanChange = boolPtr(populationCanChange)
	recording.TransferExecuted = boolPtr(transferExecuted)
	recording.IsTransition = boolPtr(isTransition)
	recording.IsLaying = boolPtr(isLaying)

	return recording, nil
}

func (s recordingService) GetNextDay(c *fiber.Ctx, req *validation.GetRecordingNextDay) (int, error) {
	if err := s.Validate.Struct(req); err != nil {
		return 0, err
	}
	projectFlockKandangId := req.ProjectFlockKandangId
	recordTime := req.RecordTimeValue.UTC()
	if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, projectFlockKandangId); err != nil {
		return 0, err
	}
	day, err := s.computeRecordingDay(c.Context(), projectFlockKandangId, recordTime)
	if err != nil {
		s.Log.Errorf("Failed to compute recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err)
		return 0, err
	}

	return day, nil
}

func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Recording, error) {
	if err := s.requireFIFO(); err != nil {
		return nil, err
	}
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}
	if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, req.ProjectFlockKandangId); err != nil {
		return nil, err
	}

	ctx := c.Context()
	recordTime := time.Now().UTC()
	if req.RecordDate != nil && strings.TrimSpace(*req.RecordDate) != "" {
		parsed, err := time.Parse("2006-01-02", strings.TrimSpace(*req.RecordDate))
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format")
		}
		recordTime = parsed.UTC()
	}

	pfk, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, req.ProjectFlockKandangId)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang not found")
		}
		s.Log.Errorf("Failed to get project flock kandang: %+v", err)
		return nil, err
	}

	category := strings.ToUpper(pfk.ProjectFlock.Category)
	isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))

	if err := s.tryAutoExecuteTransferForRecordingCreate(c, pfk, recordTime); err != nil {
		return nil, err
	}

	routePayload := buildRecordingRoutePayloadFromCreate(req)
	if err := s.enforceTransferRecordingRoute(ctx, pfk, recordTime, routePayload); err != nil {
		return nil, err
	}
	if routePayload.DepletionCount > 0 {
		if err := s.ensureDepletionMutationAllowed(ctx, &entity.Recording{
			ProjectFlockKandangId: req.ProjectFlockKandangId,
			RecordDatetime:        recordTime,
			ProjectFlockKandang:   pfk,
		}, "buat"); err != nil {
			return nil, err
		}
	}

	if err := s.ProjectFlockSvc.EnsureProjectFlockApproved(ctx, pfk.ProjectFlockId); err != nil {
		return nil, err
	}
	if err := s.ChickinSvc.EnsureChickInExists(ctx, pfk.Id); err != nil {
		return nil, err
	}
	if s.ProductionStandardSvc != nil {
		if err := s.ProductionStandardSvc.EnsureWeekStart(ctx, pfk.ProjectFlock.ProductionStandardId, category); err != nil {
			return nil, err
		}
	}

	var stockOwnerProjectFlockKandangID *uint
	if len(req.Stocks) > 0 {
		stockOwnerProjectFlockKandangID, err = s.resolveRecordingStockOwnerProjectFlockKandangID(ctx, pfk, recordTime)
		if err != nil {
			return nil, err
		}
	}

	day, err := s.computeRecordingDay(ctx, pfk.Id, recordTime)
	if err != nil {
		return nil, err
	}

	if !isLaying && len(req.Eggs) > 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
	}

	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return nil, err
	}
	if req.Eggs, err = s.resolveEggRequestsToFarmWarehouses(ctx, pfk, actorID, req.Eggs); err != nil {
		return nil, err
	}

	if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
		return nil, err
	}
	feedIDs := recordingutil.CollectWarehouseIDs(req.Stocks, func(st validation.Stock) uint { return st.ProductWarehouseId })
	if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "OVK"}, "feed"); err != nil {
		return nil, err
	}
	depletionIDs := recordingutil.CollectWarehouseIDs(req.Depletions, func(d validation.Depletion) uint { return d.ProductWarehouseId })
	if err := s.ensureProductWarehousesByFlags(ctx, depletionIDs, []string{"AYAM-AFKIR", "AYAM-CULLING", "AYAM-MATI"}, "depletion"); err != nil {
		return nil, err
	}
	depletionSourceIDs := recordingutil.CollectWarehouseIDs(req.Depletions, func(d validation.Depletion) uint {
		if d.SourceProductWarehouseId == nil {
			return 0
		}
		return *d.SourceProductWarehouseId
	})
	if err := s.ensureProductWarehousesByFlags(ctx, depletionSourceIDs, []string{"DOC", "PULLET", "LAYER"}, "depletion source"); err != nil {
		return nil, err
	}
	eggIDs := recordingutil.CollectWarehouseIDs(req.Eggs, func(e validation.Egg) uint { return e.ProductWarehouseId })
	if err := s.ensureProductWarehousesByFlags(ctx, eggIDs, []string{"TELUR-UTUH", "TELUR-PECAH", "TELUR-PUTIH", "TELUR-RETAK", "TELUR"}, "egg"); err != nil {
		return nil, err
	}
	var createdRecording entity.Recording
	transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if s.ProductionStandardSvc != nil {
			if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, day); err != nil {
				return err
			}
		}

		existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime)
		if err != nil {
			s.Log.Errorf("Failed to verify existing recording on date: %+v", err)
			return err
		}
		if existsToday {
			return fiber.NewError(fiber.StatusBadRequest, "Recording for this project flock today already exists")
		}

		createdRecording = entity.Recording{
			ProjectFlockKandangId: req.ProjectFlockKandangId,
			RecordDatetime:        recordTime,
			Day:                   &day,
			CreatedBy:             actorID,
		}

		if err := s.Repository.CreateRecording(tx, &createdRecording); err != nil {
			if errors.Is(err, gorm.ErrDuplicatedKey) {
				return fiber.NewError(
					fiber.StatusBadRequest,
					fmt.Sprintf("Recording for project flock kandang %d already exists", req.ProjectFlockKandangId),
				)
			}
			s.Log.Errorf("Failed to create recording: %+v", err)
			return err
		}

		mappedStocks := recordingutil.MapStocks(createdRecording.Id, stockOwnerProjectFlockKandangID, req.Stocks)
		stockDesired := resetStockQuantitiesForFIFO(mappedStocks)
		if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
			s.Log.Errorf("Failed to persist stocks: %+v", err)
			return err
		}

		for i := range mappedStocks {
			if i >= len(stockDesired) {
				break
			}
			usage := stockDesired[i].Usage
			pending := stockDesired[i].Pending
			mappedStocks[i].UsageQty = &usage
			mappedStocks[i].PendingQty = &pending
		}
		note := recordingutil.RecordingNote("Create", createdRecording.Id)
		if err := s.reflowApplyRecordingStocks(ctx, tx, mappedStocks, note, actorID); err != nil {
			return err
		}

		mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
		if len(mappedDepletions) > 0 {
			if err := s.ensureDepletionWithinPopulation(ctx, tx, req.ProjectFlockKandangId, sumDepletionQty(mappedDepletions), 0); err != nil {
				return err
			}
			sourceProjectFlockKandangID := req.ProjectFlockKandangId
			for i := range mappedDepletions {
				mappedDepletions[i].SourceProjectFlockKandangId = &sourceProjectFlockKandangID
				if mappedDepletions[i].SourceProductWarehouseId != nil && *mappedDepletions[i].SourceProductWarehouseId != 0 {
					continue
				}
				sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId)
				if err != nil {
					return err
				}
				mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
			}
		}
		depletionDesired := resetDepletionQuantitiesForFIFO(mappedDepletions)
		if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
			s.Log.Errorf("Failed to persist depletions: %+v", err)
			return err
		}
		applyDepletionDesiredQuantities(mappedDepletions, depletionDesired)
		if err := s.reflowApplyRecordingDepletionsOut(ctx, tx, mappedDepletions, note, actorID); err != nil {
			return err
		}
		if err := s.reflowApplyRecordingDepletionsIn(ctx, tx, mappedDepletions); err != nil {
			return err
		}
		if err := s.resyncPopulationUsageForDepletions(ctx, tx, createdRecording.ProjectFlockKandangId, mappedDepletions); err != nil {
			s.Log.Errorf("Failed to resync depletion source population usage: %+v", err)
			return err
		}

		mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.ProjectFlockKandangId, createdRecording.CreatedBy, req.Eggs)
		if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
			s.Log.Errorf("Failed to persist eggs: %+v", err)
			return err
		}
		if err := s.reflowApplyRecordingEggsIn(ctx, tx, mappedEggs, note, actorID); err != nil {
			return err
		}

		if err := s.computeAndUpdateMetrics(ctx, tx, &createdRecording); err != nil {
			s.Log.Errorf("Failed to compute recording metrics: %+v", err)
			return err
		}
		if err := s.recalculateFrom(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
			s.Log.Errorf("Failed to recalculate recordings after create: %+v", err)
			return err
		}
		if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
			s.Log.Errorf("Failed to sync farm depreciation manual input after create: %+v", err)
			return err
		}

		action := entity.ApprovalActionCreated
		if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil {
			s.Log.Errorf("Failed to create recording approval for %d: %+v", createdRecording.Id, err)
			return err
		}

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

	created, err := s.GetOne(c, createdRecording.Id)
	if err != nil {
		return nil, err
	}
	if created != nil {
		s.invalidateDepreciationSnapshots(c.Context(), nil, created.ProjectFlockKandangId, created.RecordDatetime)
	}
	return created, nil
}

func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) {
	if err := s.requireFIFO(); err != nil {
		return nil, err
	}
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}
	if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
		return nil, err
	}

	if req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
		return s.GetOne(c, id)
	}

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

	var recordingEntity *entity.Recording
	var updatedRecording *entity.Recording
	transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		repoTx := s.Repository.WithTx(tx)
		recording, err := repoTx.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
			return s.Repository.WithRelations(db)
		})

		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusNotFound, "Recording not found")
			}
			s.Log.Errorf("Failed to find recording: %+v", err)
			return err
		}

		recordingEntity = recording
		pfkForRoute := recordingEntity.ProjectFlockKandang
		if pfkForRoute == nil || pfkForRoute.Id == 0 {
			fetchedPfk, fetchErr := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recordingEntity.ProjectFlockKandangId)
			if fetchErr != nil {
				if errors.Is(fetchErr, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang not found")
				}
				s.Log.Errorf("Failed to fetch project flock kandang for route validation: %+v", fetchErr)
				return fetchErr
			}
			pfkForRoute = fetchedPfk
		}
		if err := s.tryAutoExecuteTransferForRecordingCreate(c, pfkForRoute, recordingEntity.RecordDatetime); err != nil {
			return err
		}
		routePayload := buildRecordingRoutePayloadFromUpdate(req)
		if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil {
			return err
		}

		hasStockChanges := req.Stocks != nil
		hasDepletionChanges := req.Depletions != nil
		hasEggChanges := req.Eggs != nil

		var existingStocks []entity.RecordingStock
		var existingDepletions []entity.RecordingDepletion
		var existingEggs []entity.RecordingEgg
		var mappedDepletions []entity.RecordingDepletion
		var stockOwnerProjectFlockKandangID *uint

		note := recordingutil.RecordingNote("Edit", recordingEntity.Id)

		if hasStockChanges {
			stockOwnerProjectFlockKandangID, err = s.resolveRecordingStockOwnerProjectFlockKandangID(ctx, pfkForRoute, recordingEntity.RecordDatetime)
			if err != nil {
				return err
			}
			existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
			if err != nil {
				s.Log.Errorf("Failed to list existing stocks: %+v", err)
				return err
			}
			existingUsage := recordingutil.StockUsageByWarehouse(existingStocks)
			incomingUsage := recordingutil.StockUsageByWarehouseReq(req.Stocks)
			match := recordingutil.FloatMapsEqual(existingUsage, incomingUsage) && recordingStocksAllOwnedBy(existingStocks, stockOwnerProjectFlockKandangID)
			if match {
				hasStockChanges = false
			} else {
				if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
					return err
				}
				feedIDs := recordingutil.CollectWarehouseIDs(req.Stocks, func(st validation.Stock) uint { return st.ProductWarehouseId })
				if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "OVK"}, "feed"); err != nil {
					return err
				}
				if err := s.reflowSyncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, stockOwnerProjectFlockKandangID, note, actorID); err != nil {
					return err
				}
			}
		}

		if hasDepletionChanges {
			existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id)
			if err != nil {
				s.Log.Errorf("Failed to list existing depletions: %+v", err)
				return err
			}
			existingTotals := recordingutil.DepletionTotalsByRoute(existingDepletions, func(dep entity.RecordingDepletion) (uint, *uint, float64) {
				return dep.ProductWarehouseId, dep.SourceProductWarehouseId, dep.Qty
			})
			incomingTotals := recordingutil.DepletionTotalsByRoute(req.Depletions, func(dep validation.Depletion) (uint, *uint, float64) {
				return dep.ProductWarehouseId, dep.SourceProductWarehouseId, dep.Qty
			})
			match := recordingutil.DepletionRouteMapsEqual(existingTotals, incomingTotals)
			if match {
				hasDepletionChanges = false
			} else {
				if err := s.ensurePopulationMutationAllowed(ctx, recordingEntity, "ubah"); err != nil {
					return err
				}
				if err := s.ensureDepletionMutationAllowed(ctx, recordingEntity, "ubah"); err != nil {
					return err
				}
				if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, nil); err != nil {
					return err
				}
				depletionIDs := recordingutil.CollectWarehouseIDs(req.Depletions, func(d validation.Depletion) uint { return d.ProductWarehouseId })
				if err := s.ensureProductWarehousesByFlags(ctx, depletionIDs, []string{"AYAM-AFKIR", "AYAM-CULLING", "AYAM-MATI"}, "depletion"); err != nil {
					return err
				}
				depletionSourceIDs := recordingutil.CollectWarehouseIDs(req.Depletions, func(d validation.Depletion) uint {
					if d.SourceProductWarehouseId == nil {
						return 0
					}
					return *d.SourceProductWarehouseId
				})
				if err := s.ensureProductWarehousesByFlags(ctx, depletionSourceIDs, []string{"DOC", "PULLET", "LAYER"}, "depletion source"); err != nil {
					return err
				}
				if err := s.reflowResetRecordingDepletionsOut(ctx, tx, existingDepletions, note, actorID); err != nil {
					return err
				}
				if err := s.reflowResetRecordingDepletionsIn(ctx, tx, existingDepletions); err != nil {
					return err
				}

				if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
					s.Log.Errorf("Failed to clear depletions: %+v", err)
					return err
				}

				mappedDepletions = recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
				if len(mappedDepletions) > 0 {
					if err := s.ensureDepletionWithinPopulation(ctx, tx, recordingEntity.ProjectFlockKandangId, sumDepletionQty(mappedDepletions), sumDepletionQty(existingDepletions)); err != nil {
						return err
					}
					sourceProjectFlockKandangID := recordingEntity.ProjectFlockKandangId
					for i := range mappedDepletions {
						mappedDepletions[i].SourceProjectFlockKandangId = &sourceProjectFlockKandangID
						if mappedDepletions[i].SourceProductWarehouseId != nil && *mappedDepletions[i].SourceProductWarehouseId != 0 {
							continue
						}
						sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId)
						if err != nil {
							return err
						}
						mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
					}
				}
				depletionDesired := resetDepletionQuantitiesForFIFO(mappedDepletions)
				if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
					s.Log.Errorf("Failed to update depletions: %+v", err)
					return err
				}
				applyDepletionDesiredQuantities(mappedDepletions, depletionDesired)
				if err := s.reflowApplyRecordingDepletionsOut(ctx, tx, mappedDepletions, note, actorID); err != nil {
					return err
				}
				if err := s.reflowApplyRecordingDepletionsIn(ctx, tx, mappedDepletions); err != nil {
					return err
				}
			}
		}

		if hasEggChanges {
			existingEggs, err = s.Repository.ListEggs(tx, recordingEntity.Id)
			if err != nil {
				s.Log.Errorf("Failed to list existing eggs: %+v", err)
				return err
			}
			normalizeEggWarehouses, err := s.shouldNormalizeEggRequestsOnUpdate(ctx, existingEggs)
			if err != nil {
				return err
			}
			if normalizeEggWarehouses {
				if req.Eggs, err = s.resolveEggRequestsToFarmWarehouses(ctx, pfkForRoute, actorID, req.Eggs); err != nil {
					return err
				}
			}
			existingTotals := recordingutil.EggTotalsByWarehouse(existingEggs, func(egg entity.RecordingEgg) (uint, int, *float64) {
				return egg.ProductWarehouseId, egg.Qty, egg.Weight
			})
			incomingTotals := recordingutil.EggTotalsByWarehouse(req.Eggs, func(egg validation.Egg) (uint, int, *float64) {
				return egg.ProductWarehouseId, egg.Qty, egg.Weight
			})
			match := recordingutil.EggTotalsEqual(existingTotals, incomingTotals)
			if match {
				hasEggChanges = false
			} else {
				category := ""
				if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 {
					category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category)
				}
				isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
				if !isLaying && len(req.Eggs) > 0 {
					return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
				}

				if err := s.ensureProductWarehousesExist(c, nil, nil, req.Eggs); err != nil {
					return err
				}
				eggIDs := recordingutil.CollectWarehouseIDs(req.Eggs, func(e validation.Egg) uint { return e.ProductWarehouseId })
				if err := s.ensureProductWarehousesByFlags(ctx, eggIDs, []string{"TELUR-UTUH", "TELUR-PECAH", "TELUR-PUTIH", "TELUR-RETAK", "TELUR"}, "egg"); err != nil {
					return err
				}
				if err := ensureRecordingEggsUnused(existingEggs); err != nil {
					return err
				}
				if err := s.logRecordingEggUsage(ctx, tx, existingEggs, note, actorID); err != nil {
					return err
				}
				if err := s.reflowResetRecordingEggsIn(ctx, tx, existingEggs); err != nil {
					return err
				}

				if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil {
					s.Log.Errorf("Failed to clear eggs: %+v", err)
					return err
				}

				mappedEggs := recordingutil.MapEggs(recordingEntity.Id, recordingEntity.ProjectFlockKandangId, recordingEntity.CreatedBy, req.Eggs)
				if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
					s.Log.Errorf("Failed to update eggs: %+v", err)
					return err
				}

				if err := s.reflowApplyRecordingEggsIn(ctx, tx, mappedEggs, note, actorID); err != nil {
					return err
				}
			}
		}

		if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
			return nil
		}

		if err := s.resyncPopulationUsageForDepletions(ctx, tx, recordingEntity.ProjectFlockKandangId, append(existingDepletions, mappedDepletions...)); err != nil {
			s.Log.Errorf("Failed to resync depletion source population usage: %+v", err)
			return err
		}

		if hasStockChanges || hasDepletionChanges || hasEggChanges {
			if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
				s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
				return err
			}
			if err := s.recalculateFrom(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
				s.Log.Errorf("Failed to recalculate recordings after update: %+v", err)
				return err
			}
		}
		if hasStockChanges {
			if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
				s.Log.Errorf("Failed to sync farm depreciation manual input after update: %+v", err)
				return err
			}
		}

		action := entity.ApprovalActionUpdated
		actorID := recordingEntity.CreatedBy
		if actorID == 0 {
			return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
		}

		step := utils.RecordingStepPengajuan

		latestApproval := recordingEntity.LatestApproval
		if latestApproval == nil {
			if s.ApprovalSvc != nil {
				if fetched, fetchErr := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, recordingEntity.Id, nil); fetchErr != nil {
					s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
					return fetchErr
				} else {
					latestApproval = fetched
				}
			} else if s.ApprovalRepo != nil {
				if fetched, fetchErr := s.ApprovalRepo.LatestByTarget(ctx, utils.ApprovalWorkflowRecording.String(), recordingEntity.Id, nil); fetchErr != nil {
					s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
					return fetchErr
				} else {
					latestApproval = fetched
				}
			}
		}

		shouldCreateApproval := true
		if latestApproval != nil &&
			latestApproval.StepNumber == uint16(step) &&
			latestApproval.Action != nil &&
			*latestApproval.Action == action {
			shouldCreateApproval = false
		}

		if shouldCreateApproval {
			if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, step, action, actorID, nil); err != nil {
				s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err)
				return err
			}
		}

		updated, err := repoTx.GetByID(ctx, recordingEntity.Id, func(db *gorm.DB) *gorm.DB {
			return s.Repository.WithRelations(db)
		})
		if err != nil {
			s.Log.Errorf("Failed to reload recording %d after update: %+v", recordingEntity.Id, err)
			return err
		}
		updatedRecording = updated

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

	if updatedRecording == nil {
		return s.GetOne(c, id)
	}
	if err := recordingutil.AttachLatestApproval(ctx, updatedRecording, s.ApprovalSvc, s.Log); err != nil {
		return nil, err
	}
	if err := recordingutil.AttachProductionStandards(ctx, s.Repository.DB(), false, s.Log, updatedRecording); err != nil {
		return nil, err
	}
	invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(recordingEntity.RecordDatetime, updatedRecording.RecordDatetime)
	s.invalidateDepreciationSnapshots(
		c.Context(),
		nil,
		updatedRecording.ProjectFlockKandangId,
		invalidateFromDate,
	)
	return updatedRecording, nil
}

func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}
	for _, id := range req.ApprovableIds {
		if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
			return nil, err
		}
	}

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

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

	step := utils.RecordingStepPengajuan
	if action == entity.ApprovalActionApproved {
		step = utils.RecordingStepDisetujui
	}

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

	transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		repoTx := s.Repository.WithTx(tx)
		approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))

		for _, id := range ids {
			if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Recording %d not found", id))
				}
				return err
			}

			if _, err := approvalSvc.CreateApproval(
				ctx,
				utils.ApprovalWorkflowRecording,
				id,
				step,
				&action,
				actorID,
				req.Notes,
			); err != nil {
				return err
			}

			if action == entity.ApprovalActionRejected {
				note := recordingutil.RecordingNote("Reject", id)
				existingDepletions, err := s.Repository.ListDepletions(tx, id)
				if err != nil {
					s.Log.Errorf("Failed to list depletions before reject rollback %d: %+v", id, err)
					return err
				}
				if err := s.reflowRollbackRecordingInventory(ctx, tx, id, note, actorID); err != nil {
					return err
				}
				recording, err := repoTx.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
					return s.Repository.WithRelations(db)
				})
				if err != nil {
					return err
				}
				if err := s.computeAndUpdateMetrics(ctx, tx, recording); err != nil {
					s.Log.Errorf("Failed to recompute recording metrics after reject %d: %+v", id, err)
					return err
				}
				if err := s.resyncPopulationUsageForDepletions(ctx, tx, recording.ProjectFlockKandangId, existingDepletions); err != nil {
					s.Log.Errorf("Failed to resync depletion source population usage after reject %d: %+v", id, err)
					return err
				}
				if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
					s.Log.Errorf("Failed to recalculate recordings after reject %d: %+v", id, err)
					return err
				}
			}
		}

		return nil
	})

	if transactionErr != nil {
		if fiberErr, ok := transactionErr.(*fiber.Error); ok {
			return nil, fiberErr
		}
		s.Log.Errorf("Failed to record approvals for recordings %+v: %+v", ids, transactionErr)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to submit recording approval")
	}

	updated := make([]entity.Recording, 0, len(ids))
	for _, id := range ids {
		recording, err := s.GetOne(c, id)
		if err != nil {
			return nil, err
		}
		s.invalidateDepreciationSnapshots(
			c.Context(),
			nil,
			recording.ProjectFlockKandangId,
			recording.RecordDatetime,
		)
		updated = append(updated, *recording)
	}

	return updated, nil
}

func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
	if err := s.requireFIFO(); err != nil {
		return err
	}
	if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
		return err
	}
	ctx := c.Context()
	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return err
	}
	note := recordingutil.RecordingNote("Delete", id)

	err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		recording, err := s.Repository.WithTx(tx).GetByID(ctx, id, nil)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusNotFound, "Recording not found")
			}
			s.Log.Errorf("Failed to find recording: %+v", err)
			return err
		}
		existingDepletions, err := s.Repository.ListDepletions(tx, recording.Id)
		if err != nil {
			s.Log.Errorf("Failed to list existing depletions: %+v", err)
			return err
		}
		if len(existingDepletions) > 0 {
			if err := s.ensurePopulationMutationAllowed(ctx, recording, "hapus"); err != nil {
				return err
			}
			if err := s.ensureDepletionMutationAllowed(ctx, recording, "hapus"); err != nil {
				return err
			}
		}

		if err := s.reflowRollbackRecordingInventory(ctx, tx, id, note, actorID); err != nil {
			return err
		}

		if err := s.Repository.WithTx(tx).DeleteOne(ctx, id); err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusNotFound, "Recording not found")
			}
			s.Log.Errorf("Failed to delete recording: %+v", err)
			return err
		}

		if err := s.resyncPopulationUsageForDepletions(ctx, tx, recording.ProjectFlockKandangId, existingDepletions); err != nil {
			s.Log.Errorf("Failed to resync depletion source population usage after delete: %+v", err)
			return err
		}

		if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
			s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err)
			return err
		}
		if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
			s.Log.Errorf("Failed to sync farm depreciation manual input after delete: %+v", err)
			return err
		}
		s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)

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

func (s recordingService) invalidateDepreciationSnapshots(
	ctx context.Context,
	tx *gorm.DB,
	projectFlockKandangID uint,
	fromDate time.Time,
) {
	if projectFlockKandangID == 0 || fromDate.IsZero() {
		return
	}

	targetDB := s.Repository.DB()
	if tx != nil {
		targetDB = tx
	}

	farmIDs, err := commonSvc.ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx, targetDB, []uint{projectFlockKandangID})
	if err != nil {
		s.Log.Warnf(
			"Failed to resolve farm for recording depreciation invalidation (pfk=%d): %+v",
			projectFlockKandangID,
			err,
		)
		farmIDs = nil
	}

	if len(farmIDs) == 0 {
		if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil {
			s.Log.Warnf(
				"Failed to invalidate depreciation snapshots globally (from=%s): %+v",
				fromDate.Format("2006-01-02"),
				err,
			)
		}
		return
	}

	if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil {
		s.Log.Warnf(
			"Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v",
			farmIDs,
			fromDate.Format("2006-01-02"),
			err,
		)
	}
}

func (s *recordingService) resolveRecordingCategory(ctx context.Context, recording *entity.Recording) (string, error) {
	if recording == nil || recording.ProjectFlockKandangId == 0 {
		return "", nil
	}
	if recording.ProjectFlockKandang != nil && recording.ProjectFlockKandang.ProjectFlock.Id != 0 {
		return strings.ToUpper(strings.TrimSpace(recording.ProjectFlockKandang.ProjectFlock.Category)), nil
	}

	pfk, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recording.ProjectFlockKandangId)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return "", nil
		}
		return "", err
	}

	return strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category)), nil
}

func (s *recordingService) applyCutOverLayingRecordingLookupOverride(
	ctx context.Context,
	recording *entity.Recording,
	isTransition bool,
	isLaying bool,
	chickinAvailabilityCache map[uint]bool,
) (bool, bool, error) {
	if recording == nil || recording.ProjectFlockKandangId == 0 || isLaying {
		return isTransition, isLaying, nil
	}

	category, err := s.resolveRecordingCategory(ctx, recording)
	if err != nil {
		s.Log.Errorf("Failed to resolve recording category for cut-over override (recording=%d): %+v", recording.Id, err)
		return isTransition, isLaying, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi status laying recording")
	}
	if category != strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
		return isTransition, isLaying, nil
	}

	hasChickinDate, err := s.hasProjectFlockKandangChickinDate(ctx, recording.ProjectFlockKandangId, chickinAvailabilityCache)
	if err != nil {
		s.Log.Errorf("Failed to resolve chickin date for cut-over override (project_flock_kandang=%d): %+v", recording.ProjectFlockKandangId, err)
		return isTransition, isLaying, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi status laying recording")
	}
	if !hasChickinDate {
		return isTransition, isLaying, nil
	}

	return false, true, nil
}

func (s *recordingService) hasProjectFlockKandangChickinDate(ctx context.Context, projectFlockKandangID uint, cache map[uint]bool) (bool, error) {
	if projectFlockKandangID == 0 || s.ProjectFlockPopulationRepo == nil {
		return false, nil
	}

	if cache != nil {
		if value, ok := cache[projectFlockKandangID]; ok {
			return value, nil
		}
	}

	populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
	if err != nil {
		return false, err
	}

	hasChickinDate := false
	for _, pop := range populations {
		if pop.ProjectChickin != nil && !pop.ProjectChickin.ChickInDate.IsZero() {
			hasChickinDate = true
			break
		}
	}

	if cache != nil {
		cache[projectFlockKandangID] = hasChickinDate
	}

	return hasChickinDate, nil
}

func (s *recordingService) evaluatePopulationMutationState(ctx context.Context, recording *entity.Recording) (bool, bool, bool, bool, *entity.LayingTransfer, time.Time, error) {
	if recording == nil || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
		return true, false, false, false, nil, time.Time{}, nil
	}

	category, err := s.resolveRecordingCategory(ctx, recording)
	if err != nil {
		s.Log.Errorf("Failed to resolve recording category for population mutation check (recording=%d): %+v", recording.Id, err)
		return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
	}

	var transfer *entity.LayingTransfer
	switch category {
	case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
		transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
	case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
		transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, recording.ProjectFlockKandangId)
	default:
		return true, false, false, false, nil, time.Time{}, nil
	}
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return true, false, false, false, nil, time.Time{}, nil
		}
		s.Log.Errorf("Failed to resolve approved transfer for recording %d: %+v", recording.Id, err)
		return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
	}
	if transfer == nil {
		return true, false, false, false, nil, time.Time{}, nil
	}

	transferDate := transferPhysicalMoveDate(transfer)
	if transferDate.IsZero() {
		return true, false, false, false, transfer, transferDate, nil
	}

	transferExecuted := transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
	recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
	_, economicCutoffDate := transferRecordingWindow(transfer)
	isTransition := !recordDate.Before(transferDate) && recordDate.Before(economicCutoffDate)
	isLaying := !recordDate.Before(economicCutoffDate)

	populationCanChange := true
	if category == strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
		populationCanChange = !(transferExecuted && !recordDate.Before(transferDate))

		if transferExecuted && !recordDate.Before(transferDate) {
			hasTargetLayingRecording, checkErr := s.hasAnyRecordingOnTransferTargets(ctx, transfer)
			if checkErr != nil {
				s.Log.Errorf("Failed to resolve target laying recording state for transfer %d: %+v", transfer.Id, checkErr)
				return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi status transisi recording")
			}
			if hasTargetLayingRecording {
				isTransition = false
				isLaying = true
			} else {
				today := normalizeDateOnlyUTC(time.Now().UTC())
				if !today.Before(economicCutoffDate) {
					isTransition = true
					isLaying = false
				}
			}
		}
	}

	return populationCanChange, transferExecuted, isTransition, isLaying, transfer, transferDate, nil
}

func (s *recordingService) hasAnyRecordingOnTransferTargets(ctx context.Context, transfer *entity.LayingTransfer) (bool, error) {
	if transfer == nil || transfer.Id == 0 {
		return false, nil
	}

	targetIDs, err := s.transferTargetProjectFlockKandangIDs(ctx, transfer.Id)
	if err != nil {
		return false, err
	}
	if len(targetIDs) == 0 {
		// Keep existing behavior for legacy or incomplete target mapping.
		return true, nil
	}

	var count int64
	err = s.Repository.DB().
		WithContext(ctx).
		Table("recordings").
		Where("deleted_at IS NULL").
		Where("project_flock_kandangs_id IN ?", targetIDs).
		Count(&count).Error
	if err != nil {
		return false, err
	}

	return count > 0, nil
}

func (s *recordingService) transferTargetProjectFlockKandangIDs(ctx context.Context, transferID uint) ([]uint, error) {
	if transferID == 0 {
		return nil, nil
	}

	var targetIDs []uint
	err := s.Repository.DB().
		WithContext(ctx).
		Table("laying_transfer_targets").
		Where("laying_transfer_id = ?", transferID).
		Where("deleted_at IS NULL").
		Pluck("target_project_flock_kandang_id", &targetIDs).Error
	if err != nil {
		return nil, err
	}
	return targetIDs, nil
}

func (s *recordingService) ensurePopulationMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
	populationCanChange, _, _, _, transfer, transferDate, err := s.evaluatePopulationMutationState(ctx, recording)
	if err != nil {
		return err
	}
	if populationCanChange {
		return nil
	}

	transferNumber := "-"
	if transfer != nil && strings.TrimSpace(transfer.TransferNumber) != "" {
		transferNumber = transfer.TransferNumber
	}
	recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)

	return fiber.NewError(
		fiber.StatusBadRequest,
		fmt.Sprintf(
			"Recording growing tanggal %s tidak dapat di%s karena transfer laying %s sudah dieksekusi sejak %s. Perubahan populasi tidak diizinkan.",
			recordDate.Format("2006-01-02"),
			operation,
			transferNumber,
			transferDate.Format("2006-01-02"),
		),
	)
}

func (s *recordingService) ensureDepletionMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
	if recording == nil || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
		return nil
	}

	category := ""
	if recording.ProjectFlockKandang != nil && recording.ProjectFlockKandang.ProjectFlock.Id != 0 {
		category = strings.ToUpper(strings.TrimSpace(recording.ProjectFlockKandang.ProjectFlock.Category))
	} else {
		pfk, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recording.ProjectFlockKandangId)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil
			}
			s.Log.Errorf("Failed to load project flock kandang %d for depletion guard: %+v", recording.ProjectFlockKandangId, err)
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan deplesi")
		}
		category = strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
	}

	if !shouldGuardDepletionMutation(category) {
		return nil
	}

	var (
		transfer *entity.LayingTransfer
		err      error
	)

	transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}
		s.Log.Errorf("Failed to resolve transfer laying for depletion guard recording %d: %+v", recording.Id, err)
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan deplesi")
	}
	if transfer == nil || transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
		return nil
	}

	recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
	transferNumber := strings.TrimSpace(transfer.TransferNumber)
	if transferNumber == "" {
		transferNumber = "-"
	}
	executedDate := normalizeDateOnlyUTC(*transfer.ExecutedAt)

	return fiber.NewError(
		fiber.StatusBadRequest,
		fmt.Sprintf(
			"Deplesi recording tanggal %s tidak dapat di%s karena transfer laying %s sudah dieksekusi pada %s. Setelah transfer dieksekusi, mutasi deplesi di kandang growing tidak diizinkan (termasuk backdate).",
			recordDate.Format("2006-01-02"),
			operation,
			transferNumber,
			executedDate.Format("2006-01-02"),
		),
	)
}

func shouldGuardDepletionMutation(category string) bool {
	return strings.EqualFold(strings.TrimSpace(category), string(utils.ProjectFlockCategoryGrowing))
}

func (s *recordingService) tryAutoExecuteTransferForRecordingCreate(c *fiber.Ctx, pfk *entity.ProjectFlockKandang, recordTime time.Time) error {
	if pfk == nil || pfk.Id == 0 || s.TransferLayingRepo == nil || s.TransferLayingSvc == nil {
		return nil
	}

	ctx := c.Context()
	recordDate := normalizeDateOnlyUTC(recordTime)
	category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))

	var (
		transfer *entity.LayingTransfer
		err      error
	)

	switch category {
	case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
		transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, pfk.Id)
	case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
		transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, pfk.Id)
	default:
		return nil
	}
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}
		s.Log.Errorf("Failed to resolve approved transfer for recording create (pfk=%d): %+v", pfk.Id, err)
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
	}
	if transfer == nil || (transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()) {
		return nil
	}

	businessDate := recordDate
	physicalMoveDate := transferPhysicalMoveDate(transfer)
	if !physicalMoveDate.IsZero() && businessDate.Before(physicalMoveDate) {
		businessDate = physicalMoveDate
	}

	if _, err := s.TransferLayingSvc.ExecuteWithBusinessDate(c, transfer.Id, businessDate); err != nil {
		return err
	}

	return nil
}

func (s *recordingService) enforceTransferRecordingRoute(
	ctx context.Context,
	pfk *entity.ProjectFlockKandang,
	recordTime time.Time,
	payload recordingRoutePayload,
) error {
	_ = ctx
	_ = pfk
	_ = recordTime
	_ = payload
	return nil
}

type recordingRoutePayload struct {
	StockCount     int
	DepletionCount int
	EggCount       int
}

func buildRecordingRoutePayloadFromCreate(req *validation.Create) recordingRoutePayload {
	payload := recordingRoutePayload{}
	if req == nil {
		return payload
	}
	for _, stock := range req.Stocks {
		if stock.Qty > 0 {
			payload.StockCount++
		}
	}
	for _, depletion := range req.Depletions {
		if depletion.Qty > 0 {
			payload.DepletionCount++
		}
	}
	for _, egg := range req.Eggs {
		if egg.Qty > 0 {
			payload.EggCount++
		}
	}
	return payload
}

func buildRecordingRoutePayloadFromUpdate(req *validation.Update) recordingRoutePayload {
	payload := recordingRoutePayload{}
	if req == nil {
		return payload
	}

	if req.Stocks != nil {
		for _, stock := range req.Stocks {
			if stock.Qty > 0 {
				payload.StockCount++
			}
		}
	}

	if req.Depletions != nil {
		for _, depletion := range req.Depletions {
			if depletion.Qty > 0 {
				payload.DepletionCount++
			}
		}
	}

	if req.Eggs != nil {
		for _, egg := range req.Eggs {
			if egg.Qty > 0 {
				payload.EggCount++
			}
		}
	}

	return payload
}

func transferPhysicalMoveDate(transfer *entity.LayingTransfer) time.Time {
	if transfer == nil {
		return time.Time{}
	}
	if !transfer.TransferDate.IsZero() {
		return normalizeDateOnlyUTC(transfer.TransferDate)
	}
	return time.Time{}
}

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

func transferRecordingWindow(transfer *entity.LayingTransfer) (time.Time, time.Time) {
	physicalMoveDate := transferPhysicalMoveDate(transfer)
	economicCutoffDate := transferEconomicCutoffDate(transfer)
	if economicCutoffDate.IsZero() {
		economicCutoffDate = physicalMoveDate
	}
	if !physicalMoveDate.IsZero() && economicCutoffDate.Before(physicalMoveDate) {
		economicCutoffDate = physicalMoveDate
	}
	return physicalMoveDate, economicCutoffDate
}

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 boolPtr(value bool) *bool {
	v := value
	return &v
}

func recordingStocksAllOwnedBy(stocks []entity.RecordingStock, owner *uint) bool {
	for _, stock := range stocks {
		if !uintPtrEqual(stock.ProjectFlockKandangId, owner) {
			return false
		}
	}
	return true
}

func uintPtrEqual(a *uint, b *uint) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

func (s *recordingService) resolveRecordingStockOwnerProjectFlockKandangID(
	ctx context.Context,
	pfk *entity.ProjectFlockKandang,
	recordTime time.Time,
) (*uint, error) {
	if pfk == nil || pfk.Id == 0 {
		return nil, nil
	}

	category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
	if category == "" {
		loaded, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, pfk.Id)
		if err == nil && loaded != nil {
			pfk = loaded
			category = strings.ToUpper(strings.TrimSpace(loaded.ProjectFlock.Category))
		}
	}

	if category != strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
		owner := pfk.Id
		return &owner, nil
	}

	if s.TransferLayingRepo == nil {
		return nil, nil
	}

	transfer, err := s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, pfk.Id)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		s.Log.Errorf("Failed to resolve transfer laying for recording stock owner (target_pfk=%d): %+v", pfk.Id, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan atribusi recording stock")
	}
	if transfer == nil {
		return nil, nil
	}

	sourceProjectFlockKandangID, err := s.resolveTransferSourceProjectFlockKandangID(ctx, transfer)
	if err != nil {
		s.Log.Errorf("Failed to resolve transfer source kandang for transfer %d: %+v", transfer.Id, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan sumber kandang growing")
	}
	if sourceProjectFlockKandangID == 0 {
		return nil, nil
	}

	sourceChickinDate, err := s.getEarliestChickInDateByProjectFlockKandangID(ctx, sourceProjectFlockKandangID)
	if err != nil {
		s.Log.Errorf("Failed to resolve earliest chick-in date for source kandang %d: %+v", sourceProjectFlockKandangID, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan umur ayam dari growing")
	}
	if sourceChickinDate == nil || sourceChickinDate.IsZero() {
		owner := sourceProjectFlockKandangID
		return &owner, nil
	}

	thresholdDay, err := s.resolveLayingDepreciationThresholdDay(ctx, pfk)
	if err != nil {
		s.Log.Errorf("Failed to resolve laying threshold day for kandang %d: %+v", pfk.Id, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan standar umur laying")
	}
	if thresholdDay <= 0 {
		thresholdDay = commonSvc.DepreciationStartAgeDay(resolveHouseType(pfk))
	}
	if thresholdDay <= 0 {
		thresholdDay = commonSvc.DepreciationStartAgeDay("close_house")
	}

	recordDate := normalizeDateOnlyUTC(recordTime)
	chickinDate := normalizeDateOnlyUTC(*sourceChickinDate)
	ageDay := commonSvc.FlockAgeDay(chickinDate, recordDate)
	if ageDay < thresholdDay {
		owner := sourceProjectFlockKandangID
		return &owner, nil
	}

	owner := pfk.Id
	return &owner, nil
}

func resolveHouseType(pfk *entity.ProjectFlockKandang) string {
	if pfk == nil || pfk.Kandang.HouseType == nil {
		return ""
	}
	return strings.TrimSpace(*pfk.Kandang.HouseType)
}

func (s *recordingService) resolveLayingDepreciationThresholdDay(ctx context.Context, pfk *entity.ProjectFlockKandang) (int, error) {
	houseType := commonSvc.NormalizeDepreciationHouseType(resolveHouseType(pfk))
	if houseType == "" {
		return 0, nil
	}

	var row struct {
		StandardWeek int `gorm:"column:standard_week"`
	}
	err := s.Repository.DB().WithContext(ctx).
		Table("house_depreciation_standards").
		Select("standard_week").
		Where("house_type::text = ?", houseType).
		Where("standard_week > 0").
		Order("effective_date DESC NULLS LAST").
		Order("id DESC").
		Limit(1).
		Scan(&row).Error
	if err != nil {
		return 0, err
	}
	if row.StandardWeek <= 0 {
		return 0, nil
	}
	return (row.StandardWeek * 7) + 1, nil
}

func (s *recordingService) resolveTransferSourceProjectFlockKandangID(ctx context.Context, transfer *entity.LayingTransfer) (uint, error) {
	if transfer == nil || transfer.Id == 0 {
		return 0, nil
	}

	var row struct {
		SourceProjectFlockKandangID uint `gorm:"column:source_project_flock_kandang_id"`
	}
	err := s.Repository.DB().WithContext(ctx).
		Table("laying_transfer_sources").
		Select("source_project_flock_kandang_id").
		Where("laying_transfer_id = ?", transfer.Id).
		Where("deleted_at IS NULL").
		Where("source_project_flock_kandang_id > 0").
		Order("id ASC").
		Limit(1).
		Take(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		if transfer.SourceProjectFlockKandangId != nil && *transfer.SourceProjectFlockKandangId != 0 {
			return *transfer.SourceProjectFlockKandangId, nil
		}
		return 0, nil
	}
	if err != nil {
		return 0, err
	}
	if row.SourceProjectFlockKandangID != 0 {
		return row.SourceProjectFlockKandangID, nil
	}
	if transfer.SourceProjectFlockKandangId != nil && *transfer.SourceProjectFlockKandangId != 0 {
		return *transfer.SourceProjectFlockKandangId, nil
	}
	return 0, nil
}

func (s *recordingService) getEarliestChickInDateByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*time.Time, error) {
	if projectFlockKandangID == 0 {
		return nil, nil
	}

	var row struct {
		ChickInDate *time.Time `gorm:"column:chick_in_date"`
	}
	err := s.Repository.DB().WithContext(ctx).
		Table("project_chickins").
		Select("MIN(chick_in_date) AS chick_in_date").
		Where("project_flock_kandang_id = ?", projectFlockKandangID).
		Where("deleted_at IS NULL").
		Scan(&row).Error
	if err != nil {
		return nil, err
	}
	return row.ChickInDate, nil
}

func (s *recordingService) syncFarmDepreciationManualInputFromRecordingStocks(
	ctx context.Context,
	tx *gorm.DB,
	projectFlockKandangID uint,
	fallbackCutoverDate time.Time,
) error {
	if projectFlockKandangID == 0 {
		return nil
	}

	targetDB := s.Repository.DB()
	if tx != nil {
		targetDB = tx
	}

	projectFlockID, err := s.resolveProjectFlockIDByProjectFlockKandangID(ctx, targetDB, projectFlockKandangID)
	if err != nil {
		return err
	}
	if projectFlockID == 0 {
		return nil
	}

	totalCost, err := s.sumNoTransferRecordingStockCostByProjectFlockID(ctx, targetDB, projectFlockID)
	if err != nil {
		return err
	}

	existing, err := s.getFarmDepreciationManualInputByProjectFlockID(ctx, targetDB, projectFlockID)
	if err != nil {
		return err
	}

	cutoverDate := normalizeDateOnlyUTC(fallbackCutoverDate)
	if existing != nil && !existing.CutoverDate.IsZero() {
		cutoverDate = normalizeDateOnlyUTC(existing.CutoverDate)
	}
	if cutoverDate.IsZero() {
		earliestDate, dateErr := s.getEarliestNoTransferRecordingDateByProjectFlockID(ctx, targetDB, projectFlockID)
		if dateErr != nil {
			return dateErr
		}
		if earliestDate != nil && !earliestDate.IsZero() {
			cutoverDate = normalizeDateOnlyUTC(*earliestDate)
		}
	}
	if cutoverDate.IsZero() {
		cutoverDate = normalizeDateOnlyUTC(time.Now().UTC())
	}

	now := time.Now().UTC()
	row := entity.FarmDepreciationManualInput{
		ProjectFlockId: projectFlockID,
		TotalCost:      totalCost,
		CutoverDate:    cutoverDate,
	}
	if existing != nil {
		row.Note = existing.Note
	}

	return targetDB.WithContext(ctx).
		Clauses(clause.OnConflict{
			Columns: []clause.Column{{Name: "project_flock_id"}},
			DoUpdates: clause.Assignments(map[string]any{
				"total_cost":   row.TotalCost,
				"cutover_date": row.CutoverDate,
				"updated_at":   now,
			}),
		}).
		Create(&row).Error
}

func (s *recordingService) resolveProjectFlockIDByProjectFlockKandangID(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (uint, error) {
	var row struct {
		ProjectFlockID uint `gorm:"column:project_flock_id"`
	}
	err := db.WithContext(ctx).
		Table("project_flock_kandangs").
		Select("project_flock_id").
		Where("id = ?", projectFlockKandangID).
		Take(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return 0, nil
	}
	if err != nil {
		return 0, err
	}
	return row.ProjectFlockID, nil
}

func (s *recordingService) sumNoTransferRecordingStockCostByProjectFlockID(ctx context.Context, db *gorm.DB, projectFlockID uint) (float64, error) {
	if projectFlockID == 0 {
		return 0, nil
	}

	var total float64
	err := db.WithContext(ctx).
		Table("recording_stocks AS rs").
		Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)").
		Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
		Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
		Joins(
			"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?",
			fifo.UsableKeyRecordingStock.String(),
			fifo.StockableKeyPurchaseItems.String(),
			entity.StockAllocationStatusActive,
			entity.StockAllocationPurposeConsume,
		).
		Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
		Where("pfk.project_flock_id = ?", projectFlockID).
		Where("rs.project_flock_kandang_id IS NULL").
		Scan(&total).Error
	if err != nil {
		return 0, err
	}
	return total, nil
}

func (s *recordingService) getFarmDepreciationManualInputByProjectFlockID(
	ctx context.Context,
	db *gorm.DB,
	projectFlockID uint,
) (*entity.FarmDepreciationManualInput, error) {
	if projectFlockID == 0 {
		return nil, nil
	}

	var row entity.FarmDepreciationManualInput
	err := db.WithContext(ctx).
		Where("project_flock_id = ?", projectFlockID).
		Take(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &row, nil
}

func (s *recordingService) getEarliestNoTransferRecordingDateByProjectFlockID(
	ctx context.Context,
	db *gorm.DB,
	projectFlockID uint,
) (*time.Time, error) {
	if projectFlockID == 0 {
		return nil, nil
	}

	var row struct {
		RecordDate *time.Time `gorm:"column:record_date"`
	}
	err := db.WithContext(ctx).
		Table("recording_stocks AS rs").
		Select("MIN(r.record_datetime) AS record_date").
		Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
		Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
		Where("pfk.project_flock_id = ?", projectFlockID).
		Where("rs.project_flock_kandang_id IS NULL").
		Scan(&row).Error
	if err != nil {
		return nil, err
	}
	return row.RecordDate, nil
}

func (s *recordingService) resolveEggRequestsToFarmWarehouses(
	ctx context.Context,
	pfk *entity.ProjectFlockKandang,
	actorID uint,
	eggs []validation.Egg,
) ([]validation.Egg, error) {
	if len(eggs) == 0 {
		return eggs, nil
	}

	locationID, farmName := farmContextFromProjectFlockKandang(pfk)
	if locationID == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Farm recording tidak valid")
	}

	farmWarehouse, err := s.findFirstFarmWarehouse(ctx, locationID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Farm %s belum memiliki gudang", farmName))
		}
		s.Log.Errorf("Failed to resolve farm warehouse for egg recording: %+v", err)
		return nil, err
	}

	idSet := make(map[uint]struct{}, len(eggs))
	for _, egg := range eggs {
		if egg.ProductWarehouseId != 0 {
			idSet[egg.ProductWarehouseId] = struct{}{}
		}
	}
	if len(idSet) == 0 {
		return eggs, nil
	}

	ids := make([]uint, 0, len(idSet))
	for id := range idSet {
		ids = append(ids, id)
	}

	var sourceWarehouses []entity.ProductWarehouse
	if err := s.ProductWarehouseRepo.DB().WithContext(ctx).
		Preload("Warehouse").
		Where("id IN ?", ids).
		Find(&sourceWarehouses).Error; err != nil {
		s.Log.Errorf("Failed to load egg source product warehouses: %+v", err)
		return nil, err
	}
	if len(sourceWarehouses) != len(ids) {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse telur tidak ditemukan")
	}

	sourceByID := make(map[uint]entity.ProductWarehouse, len(sourceWarehouses))
	resolvedBySource := make(map[uint]uint, len(sourceWarehouses))
	for _, source := range sourceWarehouses {
		if err := ensureEggSourceMatchesRecordingScope(source, locationID, pfk.KandangId); err != nil {
			return nil, err
		}
		sourceByID[source.Id] = source
	}

	normalized := make([]validation.Egg, len(eggs))
	copy(normalized, eggs)
	for i := range normalized {
		source := sourceByID[normalized[i].ProductWarehouseId]
		if resolvedID, ok := resolvedBySource[source.Id]; ok {
			normalized[i].ProductWarehouseId = resolvedID
			continue
		}

		resolvedID, err := s.ProductWarehouseRepo.EnsureProductWarehouse(ctx, source.ProductId, farmWarehouse.Id, nil, actorID)
		if err != nil {
			s.Log.Errorf("Failed to ensure egg farm product warehouse: %+v", err)
			return nil, err
		}
		resolvedBySource[source.Id] = resolvedID
		normalized[i].ProductWarehouseId = resolvedID
	}

	return normalized, nil
}

func farmContextFromProjectFlockKandang(pfk *entity.ProjectFlockKandang) (uint, string) {
	if pfk == nil {
		return 0, "tidak diketahui"
	}

	if pfk.ProjectFlock.LocationId != 0 {
		name := strings.TrimSpace(pfk.ProjectFlock.Location.Name)
		if name == "" {
			name = strings.TrimSpace(pfk.Kandang.Location.Name)
		}
		if name == "" {
			name = fmt.Sprintf("#%d", pfk.ProjectFlock.LocationId)
		}
		return pfk.ProjectFlock.LocationId, name
	}

	if pfk.Kandang.LocationId != 0 {
		name := strings.TrimSpace(pfk.Kandang.Location.Name)
		if name == "" {
			name = fmt.Sprintf("#%d", pfk.Kandang.LocationId)
		}
		return pfk.Kandang.LocationId, name
	}

	return 0, "tidak diketahui"
}

func (s *recordingService) findFirstFarmWarehouse(ctx context.Context, locationID uint) (*entity.Warehouse, error) {
	var warehouse entity.Warehouse
	if err := s.Repository.DB().WithContext(ctx).
		Model(&entity.Warehouse{}).
		Where("location_id = ? AND type = ?", locationID, utils.WarehouseTypeLokasi).
		Order("id ASC").
		First(&warehouse).Error; err != nil {
		return nil, err
	}
	return &warehouse, nil
}

func ensureEggSourceMatchesRecordingScope(source entity.ProductWarehouse, locationID uint, kandangID uint) error {
	if source.Id == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Product warehouse telur tidak ditemukan")
	}
	if source.ProductId == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Produk telur tidak valid")
	}
	if source.WarehouseId == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Gudang telur tidak valid")
	}
	if source.Warehouse.LocationId == nil || *source.Warehouse.LocationId != locationID {
		return fiber.NewError(fiber.StatusBadRequest, "Produk telur harus berasal dari farm yang sama")
	}

	switch strings.ToUpper(strings.TrimSpace(source.Warehouse.Type)) {
	case string(utils.WarehouseTypeLokasi):
		return nil
	case string(utils.WarehouseTypeKandang):
		if source.Warehouse.KandangId == nil || *source.Warehouse.KandangId != kandangID {
			return fiber.NewError(fiber.StatusBadRequest, "Produk telur harus berasal dari kandang recording yang sama")
		}
		return nil
	default:
		return fiber.NewError(fiber.StatusBadRequest, "Produk telur harus berasal dari gudang farm atau kandang recording")
	}
}

func (s *recordingService) shouldNormalizeEggRequestsOnUpdate(ctx context.Context, existingEggs []entity.RecordingEgg) (bool, error) {
	if len(existingEggs) == 0 {
		return true, nil
	}

	idSet := make(map[uint]struct{}, len(existingEggs))
	for _, egg := range existingEggs {
		if egg.ProductWarehouseId != 0 {
			idSet[egg.ProductWarehouseId] = struct{}{}
		}
	}
	if len(idSet) == 0 {
		return true, nil
	}

	ids := make([]uint, 0, len(idSet))
	for id := range idSet {
		ids = append(ids, id)
	}

	var productWarehouses []entity.ProductWarehouse
	if err := s.ProductWarehouseRepo.DB().WithContext(ctx).
		Preload("Warehouse").
		Where("id IN ?", ids).
		Find(&productWarehouses).Error; err != nil {
		s.Log.Errorf("Failed to load existing egg product warehouses: %+v", err)
		return false, err
	}
	if len(productWarehouses) != len(ids) {
		return false, fiber.NewError(fiber.StatusBadRequest, "Product warehouse telur tidak ditemukan")
	}

	for _, productWarehouse := range productWarehouses {
		if strings.EqualFold(strings.TrimSpace(productWarehouse.Warehouse.Type), string(utils.WarehouseTypeLokasi)) {
			return true, nil
		}
	}

	return false, nil
}

func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error {
	idSet := make(map[uint]struct{})

	for _, stock := range stocks {
		if stock.ProductWarehouseId != 0 {
			idSet[stock.ProductWarehouseId] = struct{}{}
		}
	}
	for _, dep := range depletions {
		if dep.ProductWarehouseId != 0 {
			idSet[dep.ProductWarehouseId] = struct{}{}
		}
		if dep.SourceProductWarehouseId != nil && *dep.SourceProductWarehouseId != 0 {
			idSet[*dep.SourceProductWarehouseId] = struct{}{}
		}
	}
	for _, egg := range eggs {
		if egg.ProductWarehouseId != 0 {
			idSet[egg.ProductWarehouseId] = struct{}{}
		}
	}

	if len(idSet) == 0 {
		return nil
	}
	ids := make([]uint, 0, len(idSet))
	for id := range idSet {
		ids = append(ids, id)
	}
	if err := recordingutil.EnsureProductWarehousesExist(c.Context(), s.ProductWarehouseRepo, ids); err != nil {
		s.Log.Errorf("Failed to validate product warehouses: %+v", err)
		return fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	return nil
}

func (s *recordingService) ensureProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string, label string) error {
	if len(ids) == 0 {
		return nil
	}
	if err := s.validateWarehouseIDs(ctx, ids, func(ctx context.Context, ids []uint) error {
		return recordingutil.EnsureProductWarehousesByFlags(ctx, s.Repository, ids, flags, label)
	}, label); err != nil {
		s.Log.Errorf("Failed to validate %s product warehouses: %+v", label, err)
		return err
	}
	return nil
}

func (s *recordingService) validateWarehouseIDs(
	ctx context.Context,
	ids []uint,
	validator func(ctx context.Context, ids []uint) error,
	label string,
) error {
	if len(ids) == 0 || validator == nil {
		return nil
	}
	if err := validator(ctx, ids); err != nil {
		return fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	return nil
}

func (s *recordingService) computeRecordingDay(ctx context.Context, projectFlockKandangID uint, recordTime time.Time) (int, error) {
	if projectFlockKandangID == 0 {
		return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
	}

	populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
	if err != nil {
		s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi")
	}

	var chickinDate time.Time
	for _, pop := range populations {
		if pop.ProjectChickin == nil || pop.ProjectChickin.ChickInDate.IsZero() {
			continue
		}
		if chickinDate.IsZero() || pop.ProjectChickin.ChickInDate.Before(chickinDate) {
			chickinDate = pop.ProjectChickin.ChickInDate
		}
	}
	if chickinDate.IsZero() {
		return 0, fiber.NewError(fiber.StatusBadRequest, "Tanggal chick in tidak ditemukan")
	}

	chickinDay := time.Date(chickinDate.Year(), chickinDate.Month(), chickinDate.Day(), 0, 0, 0, 0, time.UTC)
	recordDay := time.Date(recordTime.Year(), recordTime.Month(), recordTime.Day(), 0, 0, 0, 0, time.UTC)
	diff := int(recordDay.Sub(chickinDay).Hours() / 24)
	if diff < 0 {
		return 0, fiber.NewError(fiber.StatusBadRequest, "Record date tidak boleh sebelum tanggal chick in")
	}

	return diff + 1, nil
}

func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error {
	day := 0
	if recording.Day != nil {
		day = *recording.Day
	}

	totalDepletionQty, err := s.Repository.SumRecordingDepletions(tx, recording.Id)
	if err != nil {
		return fmt.Errorf("sumRecordingDepletions: %w", err)
	}

	prevRecording, err := s.Repository.FindPreviousRecording(tx, recording.ProjectFlockKandangId, day)
	if err != nil {
		return fmt.Errorf("getPreviousRecording: %w", err)
	}

	var prevCumIntake float64
	if prevRecording != nil {
		if prevRecording.CumIntake != nil {
			prevCumIntake = float64(*prevRecording.CumIntake)
		}
	}

	totalChick, err := s.Repository.GetTotalChick(tx, recording.ProjectFlockKandangId)
	if err != nil {
		return fmt.Errorf("getTotalChick: %w", err)
	}

	usageInGrams, err := s.Repository.GetFeedUsageInGrams(tx, recording.Id)
	if err != nil {
		return fmt.Errorf("getFeedUsageInGrams: %w", err)
	}

	totalEggQty, totalEggWeightGrams, err := s.Repository.GetEggSummaryByRecording(tx, recording.Id)
	if err != nil {
		return fmt.Errorf("getEggSummaryByRecording: %w", err)
	}

	cumulativeEggQty, err := s.Repository.GetCumulativeEggQtyByProjectFlockKandang(tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
	if err != nil {
		return fmt.Errorf("getCumulativeEggQtyByProjectFlockKandang: %w", err)
	}

	initialChickin, err := s.Repository.GetTotalChickinByProjectFlockKandang(tx, recording.ProjectFlockKandangId)
	if err != nil {
		return fmt.Errorf("getTotalChickinByProjectFlockKandang: %w", err)
	}

	currentDepletion := float64(totalDepletionQty)
	cumDepletionQty, err := s.Repository.GetCumulativeDepletionByProjectFlockKandangUntil(tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
	if err != nil {
		return fmt.Errorf("getCumulativeDepletionByProjectFlockKandangUntil: %w", err)
	}

	updates := map[string]any{
		"total_depletion_qty": currentDepletion,
	}
	recording.TotalDepletionQty = &currentDepletion
	recording.TotalDepletionCumQty = &cumDepletionQty

	var remainingChick float64
	totalChickFloat := float64(totalChick)
	if totalChick > 0 || initialChickin > 0 {
		baseChick := initialChickin
		if baseChick <= 0 {
			baseChick = totalChickFloat + cumDepletionQty
		}
		// Use remaining population at this record date (chickin - cumulative depletion).
		if baseChick > 0 {
			remainingChick = baseChick - cumDepletionQty
		} else {
			remainingChick = totalChickFloat
		}
		if remainingChick < 0 {
			remainingChick = 0
		}
		updates["total_chick_qty"] = remainingChick
		recording.TotalChickQty = &remainingChick

		cumRate := 0.0
		if baseChick > 0 {
			cumRate = (cumDepletionQty / baseChick) * 100
		}
		updates["cum_depletion_rate"] = cumRate
		recording.CumDepletionRate = &cumRate
	} else {
		updates["total_chick_qty"] = gorm.Expr("NULL")
		updates["cum_depletion_rate"] = gorm.Expr("NULL")
		recording.TotalChickQty = nil
		recording.CumDepletionRate = nil
	}

	depletionRate := recordingutil.ComputeDepletionRate(prevRecording, currentDepletion, int64(remainingChick))
	recording.DepletionRate = &depletionRate

	var feedIntake float64
	if remainingChick > 0 && usageInGrams > 0 {
		feedIntake = usageInGrams / remainingChick
		updates["feed_intake"] = feedIntake
		recording.FeedIntake = &feedIntake
	} else {
		updates["feed_intake"] = gorm.Expr("NULL")
		recording.FeedIntake = nil
	}

	var henDay float64
	if remainingChick > 0 && totalEggQty >= 0 {
		henDay = (totalEggQty / remainingChick) * 100
		updates["hen_day"] = henDay
		recording.HenDay = &henDay
	} else {
		updates["hen_day"] = gorm.Expr("NULL")
		recording.HenDay = nil
	}

	var henHouse float64
	if initialChickin > 0 && cumulativeEggQty >= 0 {
		henHouse = cumulativeEggQty / initialChickin
		updates["hen_house"] = henHouse
		recording.HenHouse = &henHouse
	} else {
		updates["hen_house"] = gorm.Expr("NULL")
		recording.HenHouse = nil
	}

	var eggMass float64
	if initialChickin > 0 && totalEggWeightGrams > 0 {
		// totalEggWeightGrams is in grams; egg mass uses initial chick population.
		eggMass = totalEggWeightGrams / initialChickin
		updates["egg_mass"] = eggMass
		recording.EggMass = &eggMass
	} else {
		updates["egg_mass"] = gorm.Expr("NULL")
		recording.EggMass = nil
	}

	var eggWeight float64
	if totalEggQty > 0 && totalEggWeightGrams > 0 {
		eggWeight = totalEggWeightGrams / totalEggQty
		updates["egg_weight"] = eggWeight
		recording.EggWeight = &eggWeight
	} else {
		updates["egg_weight"] = gorm.Expr("NULL")
		recording.EggWeight = nil
	}

	var fcrValue float64
	isGrowing := false
	if s.ProjectFlockKandangRepo != nil {
		if pfk, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recording.ProjectFlockKandangId); err == nil {
			if strings.EqualFold(pfk.ProjectFlock.Category, string(utils.ProjectFlockCategoryGrowing)) {
				isGrowing = true
			}
		}
	}

	if isGrowing {
		week := 0
		if recording.Day != nil && *recording.Day > 0 {
			week = (*recording.Day-1)/7 + 1
		}
		if week > 0 && s.Repository != nil {
			meanBw, ok, err := s.Repository.GetUniformityMeanBwByWeek(tx, recording.ProjectFlockKandangId, week)
			if err != nil {
				return fmt.Errorf("getUniformityMeanBwByWeek: %w", err)
			}
			if ok && meanBw > 0 && feedIntake > 0 {
				fcrValue = feedIntake / meanBw
			}
		}
		updates["fcr_value"] = fcrValue
		recording.FcrValue = &fcrValue
	} else {
		if usageInGrams > 0 && totalEggWeightGrams > 0 {
			fcrValue = usageInGrams / totalEggWeightGrams
			updates["fcr_value"] = fcrValue
			recording.FcrValue = &fcrValue
		} else {
			updates["fcr_value"] = gorm.Expr("NULL")
			recording.FcrValue = nil
		}
	}

	if usageInGrams > 0 && totalChick > 0 {
		var cumIntakeValue float64
		if prevRecording == nil || prevRecording.CumIntake == nil {
			cumIntakeValue = usageInGrams / float64(totalChick)
		} else {
			remaining := float64(totalChick) - cumDepletionQty
			if remaining <= 0 {
				remaining = float64(totalChick)
			}
			cumIntakeValue = prevCumIntake + (usageInGrams / remaining)
		}
		cumIntakeRounded := int(math.Round(cumIntakeValue))
		updates["cum_intake"] = cumIntakeRounded
		recording.CumIntake = &cumIntakeRounded
	} else if prevRecording != nil && prevRecording.CumIntake != nil {
		updates["cum_intake"] = *prevRecording.CumIntake
		recording.CumIntake = prevRecording.CumIntake
	} else {
		updates["cum_intake"] = gorm.Expr("NULL")
		recording.CumIntake = nil
	}

	if err := s.Repository.WithTx(tx).PatchOne(ctx, recording.Id, updates, nil); err != nil {
		return err
	}

	return nil
}

func (s *recordingService) createRecordingApproval(
	ctx context.Context,
	db *gorm.DB,
	recordingID uint,
	step approvalutils.ApprovalStep,
	action entity.ApprovalAction,
	actorID uint,
	notes *string,
) error {
	if recordingID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Recording tidak valid untuk approval")
	}
	if actorID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
	}

	var svc commonSvc.ApprovalService
	if db != nil {
		svc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
	} else if s.ApprovalSvc != nil {
		svc = s.ApprovalSvc
	} else {
		svc = commonSvc.NewApprovalService(s.ApprovalRepo)
	}

	_, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowRecording, recordingID, step, &action, actorID, notes)
	return err
}

// ---- Reflow Inventory Helpers (moved from split files) ----

func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) {
	if s == nil || s.Log == nil {
		return
	}
	usage := 0.0
	if stock.UsageQty != nil {
		usage = *stock.UsageQty
	}
	pending := 0.0
	if stock.PendingQty != nil {
		pending = *stock.PendingQty
	}
	s.Log.Infof(
		"[recording-stock] action=%s recording_id=%d stock_id=%d pw=%d usage=%.3f pending=%.3f %s",
		action,
		stock.RecordingId,
		stock.Id,
		stock.ProductWarehouseId,
		usage,
		pending,
		extra,
	)
}

func (s *recordingService) logEggTrace(action string, egg entity.RecordingEgg, extra string) {
	if s == nil || s.Log == nil {
		return
	}
	weight := 0.0
	if egg.Weight != nil {
		weight = *egg.Weight
	}
	s.Log.Infof(
		"[recording-egg] action=%s recording_id=%d egg_id=%d pw=%d qty=%d weight=%.3f total_qty=%.3f total_used=%.3f %s",
		action,
		egg.RecordingId,
		egg.Id,
		egg.ProductWarehouseId,
		egg.Qty,
		weight,
		egg.TotalQty,
		egg.TotalUsed,
		extra,
	)
}

func (s *recordingService) logDepletionTrace(action string, dep entity.RecordingDepletion, extra string) {
	if s == nil || s.Log == nil {
		return
	}
	sourceWarehouseID := uint(0)
	if dep.SourceProductWarehouseId != nil {
		sourceWarehouseID = *dep.SourceProductWarehouseId
	}
	s.Log.Infof(
		"[recording-depletion] action=%s recording_id=%d depletion_id=%d source_pw=%d dest_pw=%d qty=%.3f usage=%.3f pending=%.3f %s",
		action,
		dep.RecordingId,
		dep.Id,
		sourceWarehouseID,
		dep.ProductWarehouseId,
		dep.Qty,
		dep.UsageQty,
		dep.PendingQty,
		extra,
	)
}

type recordingStockLogState struct {
	latestByWarehouse map[uint]float64
	loaded            map[uint]bool
}

func newRecordingStockLogState() *recordingStockLogState {
	return &recordingStockLogState{
		latestByWarehouse: make(map[uint]float64),
		loaded:            make(map[uint]bool),
	}
}

func shouldWriteRecordingStockLog(note string, actorID uint) bool {
	return strings.TrimSpace(note) != "" && actorID != 0
}

func (s *recordingService) appendRecordingStockLog(
	ctx context.Context,
	tx *gorm.DB,
	state *recordingStockLogState,
	log *entity.StockLog,
) error {
	if log == nil || log.ProductWarehouseId == 0 {
		return nil
	}
	if s.StockLogRepo == nil {
		return errors.New("stock log repository is not available")
	}
	if state == nil {
		state = newRecordingStockLogState()
	}

	pwID := log.ProductWarehouseId
	if !state.loaded[pwID] {
		repoTx := s.StockLogRepo
		if tx != nil {
			repoTx = rStockLogs.NewStockLogRepository(tx)
		}
		stockLogs, err := repoTx.GetByProductWarehouse(ctx, pwID, 1)
		if err != nil {
			if s.Log != nil {
				s.Log.Errorf("Failed to get stock logs for product_warehouse_id=%d: %+v", pwID, err)
			}
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
		}
		if len(stockLogs) > 0 {
			state.latestByWarehouse[pwID] = stockLogs[0].Stock
		} else {
			state.latestByWarehouse[pwID] = 0
		}
		state.loaded[pwID] = true
	}

	baseStock := state.latestByWarehouse[pwID]
	log.Stock = baseStock + log.Increase - log.Decrease

	if tx != nil {
		if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil {
			return err
		}
	} else {
		if err := s.StockLogRepo.CreateOne(ctx, log, nil); err != nil {
			return err
		}
	}

	state.latestByWarehouse[pwID] = log.Stock
	return nil
}

func (s *recordingService) logRecordingEggUsage(
	ctx context.Context,
	tx *gorm.DB,
	eggs []entity.RecordingEgg,
	note string,
	actorID uint,
) error {
	if len(eggs) == 0 || s.StockLogRepo == nil {
		return nil
	}
	if !shouldWriteRecordingStockLog(note, actorID) {
		return nil
	}

	logState := newRecordingStockLogState()
	for _, egg := range eggs {
		if egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
			continue
		}
		log := &entity.StockLog{
			ProductWarehouseId: egg.ProductWarehouseId,
			CreatedBy:          actorID,
			Decrease:           float64(egg.Qty),
			LoggableType:       string(utils.StockLogTypeRecording),
			LoggableId:         egg.RecordingId,
			Notes:              note,
		}
		if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
			return err
		}
	}

	return nil
}

func (s *recordingService) logRecordingEggRollback(
	ctx context.Context,
	tx *gorm.DB,
	eggs []entity.RecordingEgg,
	note string,
	actorID uint,
) error {
	if len(eggs) == 0 || s.StockLogRepo == nil {
		return nil
	}
	if !shouldWriteRecordingStockLog(note, actorID) {
		return nil
	}

	logState := newRecordingStockLogState()
	for _, egg := range eggs {
		if egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
			continue
		}
		log := &entity.StockLog{
			ProductWarehouseId: egg.ProductWarehouseId,
			CreatedBy:          actorID,
			Decrease:           float64(egg.Qty),
			LoggableType:       string(utils.StockLogTypeRecording),
			LoggableId:         egg.RecordingId,
			Notes:              note,
		}
		if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
			return err
		}
	}

	return nil
}

func (s *recordingService) reflowApplyRecordingStocks(
	ctx context.Context,
	tx *gorm.DB,
	stocks []entity.RecordingStock,
	note string,
	actorID uint,
) error {
	if len(stocks) == 0 {
		return nil
	}
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for consuming recording stocks")
		return errors.New("fifo v2 service is not available")
	}
	shouldWriteLog := shouldWriteRecordingStockLog(note, actorID)
	if shouldWriteLog && s.StockLogRepo == nil {
		return errors.New("stock log repository is not available")
	}
	logState := newRecordingStockLogState()

	for _, stock := range stocks {
		if stock.Id == 0 {
			continue
		}
		s.logStockTrace("reflow_apply:start", stock, "")

		var desired float64
		if stock.UsageQty != nil {
			desired = *stock.UsageQty
		}
		var pending float64
		if stock.PendingQty != nil {
			pending = *stock.PendingQty
		}
		desiredTotal := desired + pending

		if err := s.Repository.UpdateStockUsage(tx, stock.Id, desiredTotal, 0); err != nil {
			return err
		}
		if err := s.reflowRecordingScope(
			ctx,
			tx,
			stock.ProductWarehouseId,
			stock.RecordingId,
			recordingLaneUsable,
			recordingFunctionStockOut,
			recordingSourceStocks,
		); err != nil {
			s.Log.Errorf("Failed to reflow FIFO v2 stock for recording stock %d: %+v", stock.Id, err)
			return err
		}

		refreshed, err := s.Repository.GetStockByID(tx, stock.Id)
		if err != nil {
			return err
		}
		actualUsage := 0.0
		actualPending := 0.0
		if refreshed.UsageQty != nil {
			actualUsage = *refreshed.UsageQty
		}
		if refreshed.PendingQty != nil {
			actualPending = *refreshed.PendingQty
		}
		s.logStockTrace("reflow_apply:done", *refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desiredTotal, actualUsage, actualPending))

		logDecrease := recordingStockRollbackQty(*refreshed)
		if logDecrease > 0 && shouldWriteLog {
			log := &entity.StockLog{
				ProductWarehouseId: refreshed.ProductWarehouseId,
				CreatedBy:          actorID,
				Decrease:           logDecrease,
				LoggableType:       string(utils.StockLogTypeRecording),
				LoggableId:         refreshed.RecordingId,
				Notes:              note,
			}
			if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
				return err
			}
		}
	}

	return nil
}

func (s *recordingService) reflowResetRecordingStocks(
	ctx context.Context,
	tx *gorm.DB,
	stocks []entity.RecordingStock,
	note string,
	actorID uint,
) error {
	if len(stocks) == 0 {
		return nil
	}
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for releasing recording stocks")
		return errors.New("fifo v2 service is not available")
	}
	shouldWriteLog := shouldWriteRecordingStockLog(note, actorID)
	if shouldWriteLog && s.StockLogRepo == nil {
		return errors.New("stock log repository is not available")
	}
	logState := newRecordingStockLogState()

	for _, stock := range stocks {
		if stock.Id == 0 {
			continue
		}

		rollbackQty := recordingStockRollbackQty(stock)
		s.logStockTrace("reflow_reset:start", stock, fmt.Sprintf("rollback_qty=%.3f", rollbackQty))

		if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
			return err
		}
		if err := s.reflowRecordingScope(
			ctx,
			tx,
			stock.ProductWarehouseId,
			stock.RecordingId,
			recordingLaneUsable,
			recordingFunctionStockOut,
			recordingSourceStocks,
		); err != nil {
			s.Log.Errorf("Failed to reflow FIFO v2 rollback for recording stock %d: %+v", stock.Id, err)
			return err
		}
		s.logStockTrace("reflow_reset:done", stock, fmt.Sprintf("rollback_qty=%.3f", rollbackQty))

		if rollbackQty > 0 && shouldWriteLog {
			log := &entity.StockLog{
				ProductWarehouseId: stock.ProductWarehouseId,
				CreatedBy:          actorID,
				Increase:           rollbackQty,
				LoggableType:       string(utils.StockLogTypeRecording),
				LoggableId:         stock.RecordingId,
				Notes:              note,
			}
			if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
				return err
			}
		}
	}

	return nil
}

func recordingStockRollbackQty(stock entity.RecordingStock) float64 {
	usage := 0.0
	if stock.UsageQty != nil {
		usage = *stock.UsageQty
	}
	pending := 0.0
	if stock.PendingQty != nil {
		pending = *stock.PendingQty
	}
	if usage < 0 {
		usage = 0
	}
	if pending < 0 {
		pending = 0
	}
	return usage + pending
}

type desiredStock struct {
	Usage   float64
	Pending float64
}

func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock) []desiredStock {
	desired := make([]desiredStock, len(stocks))
	for i := range stocks {
		if stocks[i].UsageQty != nil {
			desired[i].Usage = *stocks[i].UsageQty
		}
		if stocks[i].PendingQty != nil {
			desired[i].Pending = *stocks[i].PendingQty
		}
		zero := 0.0
		stocks[i].UsageQty = &zero
		stocks[i].PendingQty = &zero
	}
	return desired
}

func (s *recordingService) reflowSyncRecordingStocks(
	ctx context.Context,
	tx *gorm.DB,
	recordingID uint,
	existing []entity.RecordingStock,
	incoming []validation.Stock,
	ownerProjectFlockKandangID *uint,
	note string,
	actorID uint,
) error {
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for syncing recording stocks")
		return errors.New("fifo v2 service is not available")
	}

	existingByWarehouse := make(map[uint][]entity.RecordingStock)
	for _, stock := range existing {
		existingByWarehouse[stock.ProductWarehouseId] = append(existingByWarehouse[stock.ProductWarehouseId], stock)
	}

	stocksToApply := make([]entity.RecordingStock, 0, len(incoming))
	for _, item := range incoming {
		list := existingByWarehouse[item.ProductWarehouseId]
		var stock entity.RecordingStock
		if len(list) > 0 {
			stock = list[0]
			existingByWarehouse[item.ProductWarehouseId] = list[1:]
		} else {
			zero := 0.0
			stock = entity.RecordingStock{
				RecordingId:           recordingID,
				ProductWarehouseId:    item.ProductWarehouseId,
				ProjectFlockKandangId: ownerProjectFlockKandangID,
				UsageQty:              &zero,
				PendingQty:            &zero,
			}
			if err := s.Repository.CreateStock(tx, &stock); err != nil {
				return err
			}
		}
		stock.ProjectFlockKandangId = ownerProjectFlockKandangID
		if stock.Id != 0 {
			if err := tx.Model(&entity.RecordingStock{}).
				Where("id = ?", stock.Id).
				Updates(map[string]any{
					"project_flock_kandang_id": ownerProjectFlockKandangID,
				}).Error; err != nil {
				return err
			}
		}

		desired := item.Qty
		stock.UsageQty = &desired
		zero := 0.0
		stock.PendingQty = &zero
		stocksToApply = append(stocksToApply, stock)
	}

	var leftovers []entity.RecordingStock
	for _, list := range existingByWarehouse {
		leftovers = append(leftovers, list...)
	}
	if len(leftovers) > 0 {
		if err := s.reflowResetRecordingStocks(ctx, tx, leftovers, note, actorID); err != nil {
			return err
		}
		ids := make([]uint, 0, len(leftovers))
		for _, stock := range leftovers {
			if stock.Id != 0 {
				ids = append(ids, stock.Id)
			}
		}
		if len(ids) > 0 {
			if err := s.Repository.DeleteStocksByIDs(tx, ids); err != nil {
				return err
			}
		}
	}

	if len(stocksToApply) == 0 {
		return nil
	}
	return s.reflowApplyRecordingStocks(ctx, tx, stocksToApply, note, actorID)
}

func (s *recordingService) reflowApplyRecordingDepletionsOut(
	ctx context.Context,
	tx *gorm.DB,
	depletions []entity.RecordingDepletion,
	note string,
	actorID uint,
) error {
	if len(depletions) == 0 {
		return nil
	}
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for consuming recording depletions")
		return errors.New("fifo v2 service is not available")
	}
	shouldWriteLog := shouldWriteRecordingStockLog(note, actorID)
	if shouldWriteLog && s.StockLogRepo == nil {
		return errors.New("stock log repository is not available")
	}
	logState := newRecordingStockLogState()

	for _, depletion := range depletions {
		if depletion.Id == 0 {
			continue
		}
		s.logDepletionTrace("reflow_apply:start", depletion, "")

		sourceWarehouseID := uint(0)
		if depletion.SourceProductWarehouseId != nil {
			sourceWarehouseID = *depletion.SourceProductWarehouseId
		}
		if sourceWarehouseID == 0 {
			return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
		}

		desired := depletion.Qty + depletion.PendingQty
		if err := s.Repository.UpdateDepletionQuantities(tx, depletion.Id, desired, desired, 0); err != nil {
			return err
		}
		if err := s.reflowRecordingScope(
			ctx,
			tx,
			sourceWarehouseID,
			depletion.RecordingId,
			recordingLaneUsable,
			recordingFunctionDepletionOut,
			recordingSourceDepletions,
		); err != nil {
			s.Log.Errorf("Failed to reflow FIFO v2 stock for recording depletion %d: %+v", depletion.Id, err)
			return err
		}

		refreshed, err := s.Repository.GetDepletionByID(tx, depletion.Id)
		if err != nil {
			return err
		}
		s.logDepletionTrace("reflow_apply:done", *refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desired, refreshed.UsageQty, refreshed.PendingQty))

		consumeQty := refreshed.UsageQty
		if refreshed.PendingQty > 0 {
			consumeQty += refreshed.PendingQty
		}
		if err := s.allocatePopulationForDepletion(ctx, tx, *refreshed, consumeQty); err != nil {
			return err
		}

		logDecrease := refreshed.UsageQty
		if refreshed.PendingQty > 0 {
			logDecrease += refreshed.PendingQty
		}
		if logDecrease > 0 && shouldWriteLog {
			log := &entity.StockLog{
				ProductWarehouseId: sourceWarehouseID,
				CreatedBy:          actorID,
				Decrease:           logDecrease,
				LoggableType:       string(utils.StockLogTypeRecording),
				LoggableId:         refreshed.RecordingId,
				Notes:              note,
			}
			if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
				return err
			}
		}

		destDelta := refreshed.Qty + refreshed.PendingQty
		if refreshed.ProductWarehouseId != 0 && destDelta > 0 && shouldWriteLog {
			if refreshed.ProductWarehouseId == sourceWarehouseID {
				continue
			}
			log := &entity.StockLog{
				ProductWarehouseId: refreshed.ProductWarehouseId,
				CreatedBy:          actorID,
				Increase:           destDelta,
				LoggableType:       string(utils.StockLogTypeRecording),
				LoggableId:         refreshed.RecordingId,
				Notes:              note,
			}
			if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
				return err
			}
		}
	}

	return nil
}

func (s *recordingService) reflowResetRecordingDepletionsOut(
	ctx context.Context,
	tx *gorm.DB,
	depletions []entity.RecordingDepletion,
	note string,
	actorID uint,
) error {
	if len(depletions) == 0 {
		return nil
	}
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for releasing recording depletions")
		return errors.New("fifo v2 service is not available")
	}
	shouldWriteLog := shouldWriteRecordingStockLog(note, actorID)
	if shouldWriteLog && s.StockLogRepo == nil {
		return errors.New("stock log repository is not available")
	}
	logState := newRecordingStockLogState()
	for _, depletion := range depletions {
		if depletion.Id == 0 {
			continue
		}
		s.logDepletionTrace("reflow_reset:start", depletion, "")

		sourceWarehouseID := uint(0)
		if depletion.SourceProductWarehouseId != nil {
			sourceWarehouseID = *depletion.SourceProductWarehouseId
		}
		if sourceWarehouseID == 0 {
			return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
		}

		logIncrease := depletion.Qty + depletion.PendingQty
		destDelta := depletion.Qty + depletion.PendingQty

		if err := s.Repository.UpdateDepletionQuantities(tx, depletion.Id, 0, 0, 0); err != nil {
			return err
		}

		if err := s.reflowRecordingScope(
			ctx,
			tx,
			sourceWarehouseID,
			depletion.RecordingId,
			recordingLaneUsable,
			recordingFunctionDepletionOut,
			recordingSourceDepletions,
		); err != nil {
			s.Log.Errorf("Failed to reflow FIFO v2 source rollback for recording depletion %d: %+v", depletion.Id, err)
			return err
		}
		if depletion.ProductWarehouseId != 0 {
			if err := s.reflowRecordingScope(
				ctx,
				tx,
				depletion.ProductWarehouseId,
				depletion.RecordingId,
				recordingLaneStockable,
				recordingFunctionDepletionIn,
				recordingSourceDepletions,
			); err != nil {
				s.Log.Errorf("Failed to reflow FIFO v2 destination rollback for recording depletion %d: %+v", depletion.Id, err)
				return err
			}
		}
		s.logDepletionTrace("reflow_reset:done", depletion, "")

		if logIncrease > 0 && shouldWriteLog {
			log := &entity.StockLog{
				ProductWarehouseId: sourceWarehouseID,
				CreatedBy:          actorID,
				Increase:           logIncrease,
				LoggableType:       string(utils.StockLogTypeRecording),
				LoggableId:         depletion.RecordingId,
				Notes:              note,
			}
			if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
				return err
			}
		}

		if depletion.ProductWarehouseId != 0 && destDelta > 0 && shouldWriteLog {
			if depletion.ProductWarehouseId == sourceWarehouseID {
				continue
			}
			log := &entity.StockLog{
				ProductWarehouseId: depletion.ProductWarehouseId,
				CreatedBy:          actorID,
				Decrease:           destDelta,
				LoggableType:       string(utils.StockLogTypeRecording),
				LoggableId:         depletion.RecordingId,
				Notes:              note,
			}
			if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
				return err
			}
		}
	}

	return nil
}

func (s *recordingService) allocatePopulationForDepletion(
	ctx context.Context,
	tx *gorm.DB,
	depletion entity.RecordingDepletion,
	consumeQty float64,
) error {
	if consumeQty <= 0 {
		return nil
	}
	if tx == nil {
		return errors.New("transaction is required")
	}

	sourceWarehouseID := uint(0)
	if depletion.SourceProductWarehouseId != nil {
		sourceWarehouseID = *depletion.SourceProductWarehouseId
	}
	if sourceWarehouseID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan")
	}

	var projectFlockKandangID uint
	if depletion.SourceProjectFlockKandangId != nil {
		projectFlockKandangID = *depletion.SourceProjectFlockKandangId
	}
	if projectFlockKandangID == 0 {
		if err := tx.WithContext(ctx).
			Table("recordings").
			Select("project_flock_kandangs_id").
			Where("id = ?", depletion.RecordingId).
			Scan(&projectFlockKandangID).Error; err != nil {
			return err
		}
	}
	if projectFlockKandangID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak ditemukan untuk depletion")
	}

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

	return fifoV2.AllocatePopulationConsumption(
		ctx,
		tx,
		populations,
		sourceWarehouseID,
		fifo.UsableKeyRecordingDepletion.String(),
		depletion.Id,
		consumeQty,
	)
}

func (s *recordingService) reflowApplyRecordingDepletionsIn(
	ctx context.Context,
	tx *gorm.DB,
	depletions []entity.RecordingDepletion,
) error {
	if len(depletions) == 0 {
		return nil
	}
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for applying recording depletion reflow")
		return errors.New("fifo v2 service is not available")
	}

	for _, depletion := range depletions {
		if depletion.Id == 0 || depletion.ProductWarehouseId == 0 || depletion.Qty <= 0 {
			continue
		}
		s.logDepletionTrace("reflow_apply:start", depletion, "")
		if err := s.reflowRecordingScope(
			ctx,
			tx,
			depletion.ProductWarehouseId,
			depletion.RecordingId,
			recordingLaneStockable,
			recordingFunctionDepletionIn,
			recordingSourceDepletions,
		); err != nil {
			s.Log.Errorf("Failed to reflow FIFO v2 stock for recording depletion %d: %+v", depletion.Id, err)
			return err
		}
		s.logDepletionTrace("reflow_apply:done", depletion, "")
	}

	return nil
}

func (s *recordingService) reflowResetRecordingDepletionsIn(
	ctx context.Context,
	tx *gorm.DB,
	depletions []entity.RecordingDepletion,
) error {
	if len(depletions) == 0 {
		return nil
	}
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for reducing recording depletions")
		return errors.New("fifo v2 service is not available")
	}

	for _, depletion := range depletions {
		if depletion.Id == 0 || depletion.ProductWarehouseId == 0 || depletion.Qty <= 0 {
			continue
		}
		s.logDepletionTrace("reflow_reset:start", depletion, "")

		if err := s.Repository.UpdateDepletionQuantities(tx, depletion.Id, 0, 0, 0); err != nil {
			return err
		}
		if depletion.SourceProductWarehouseId != nil && *depletion.SourceProductWarehouseId != 0 {
			if err := s.reflowRecordingScope(
				ctx,
				tx,
				*depletion.SourceProductWarehouseId,
				depletion.RecordingId,
				recordingLaneUsable,
				recordingFunctionDepletionOut,
				recordingSourceDepletions,
			); err != nil {
				s.Log.Errorf("Failed to reflow FIFO v2 source stock for recording depletion %d: %+v", depletion.Id, err)
				return err
			}
		}
		if err := s.reflowRecordingScope(
			ctx,
			tx,
			depletion.ProductWarehouseId,
			depletion.RecordingId,
			recordingLaneStockable,
			recordingFunctionDepletionIn,
			recordingSourceDepletions,
		); err != nil {
			s.Log.Errorf("Failed to reflow FIFO v2 destination stock for recording depletion %d: %+v", depletion.Id, err)
			return err
		}

		s.logDepletionTrace("reflow_reset:done", depletion, "")
	}

	return nil
}

type desiredDepletion struct {
	Qty     float64
	Pending float64
}

func resetDepletionQuantitiesForFIFO(depletions []entity.RecordingDepletion) []desiredDepletion {
	desired := make([]desiredDepletion, len(depletions))
	for i := range depletions {
		desired[i].Qty = depletions[i].Qty
		desired[i].Pending = depletions[i].PendingQty
		depletions[i].Qty = 0
		depletions[i].UsageQty = 0
		depletions[i].PendingQty = 0
	}
	return desired
}

func applyDepletionDesiredQuantities(depletions []entity.RecordingDepletion, desired []desiredDepletion) {
	for i := range depletions {
		if i >= len(desired) {
			break
		}
		depletions[i].Qty = desired[i].Qty
		depletions[i].PendingQty = desired[i].Pending
	}
}

func sumDepletionQty(items []entity.RecordingDepletion) float64 {
	var total float64
	for _, item := range items {
		if item.Qty > 0 {
			total += item.Qty
		}
	}
	return total
}

func (s *recordingService) resyncPopulationUsageForDepletions(
	ctx context.Context,
	tx *gorm.DB,
	recordingProjectFlockKandangID uint,
	depletions []entity.RecordingDepletion,
) error {
	kandangIDs := map[uint]struct{}{}
	if recordingProjectFlockKandangID != 0 {
		kandangIDs[recordingProjectFlockKandangID] = struct{}{}
	}

	sourceWarehouseIDs := make([]uint, 0)
	sourceWarehouseSeen := map[uint]struct{}{}
	for _, dep := range depletions {
		if dep.SourceProductWarehouseId == nil || *dep.SourceProductWarehouseId == 0 {
			continue
		}
		pwID := *dep.SourceProductWarehouseId
		if _, exists := sourceWarehouseSeen[pwID]; exists {
			continue
		}
		sourceWarehouseSeen[pwID] = struct{}{}
		sourceWarehouseIDs = append(sourceWarehouseIDs, pwID)
	}

	if len(sourceWarehouseIDs) > 0 {
		sourceKandangIDs, err := s.Repository.GetProjectFlockKandangIDsByPopulationWarehouseIDs(ctx, tx, sourceWarehouseIDs)
		if err != nil {
			return err
		}

		for _, kandangID := range sourceKandangIDs {
			if kandangID != 0 {
				kandangIDs[kandangID] = struct{}{}
			}
		}
	}

	for kandangID := range kandangIDs {
		if err := s.Repository.ResyncProjectFlockPopulationUsage(ctx, tx, kandangID); err != nil {
			return err
		}
	}

	return nil
}

func (s *recordingService) ensureDepletionWithinPopulation(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, newTotal float64, existingTotal float64) error {
	if projectFlockKandangId == 0 || newTotal <= 0 {
		return nil
	}
	totalChick, err := s.Repository.GetTotalChick(tx, projectFlockKandangId)
	if err != nil {
		return err
	}
	// totalChick already reflects existing depletions; add them back to compare the delta.
	available := float64(totalChick) + existingTotal
	if newTotal > available {
		return fiber.NewError(fiber.StatusBadRequest, "Depletion melebihi populasi yang tersedia")
	}
	return nil
}

func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, projectFlockKandangID uint) (uint, error) {
	if projectFlockKandangID == 0 {
		return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
	}
	populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
	if err != nil {
		s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi")
	}

	// Prioritize populations that still have effective remaining qty.
	for _, pop := range populations {
		if pop.ProductWarehouseId == 0 {
			continue
		}
		remaining := pop.TotalQty - pop.TotalUsedQty
		if remaining > 0 {
			return pop.ProductWarehouseId, nil
		}
	}

	for _, pop := range populations {
		if pop.ProductWarehouseId > 0 && pop.TotalQty > 0 {
			return pop.ProductWarehouseId, nil
		}
	}
	for _, pop := range populations {
		if pop.ProductWarehouseId > 0 {
			return pop.ProductWarehouseId, nil
		}
	}
	return 0, fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan")
}

func (s *recordingService) reflowApplyRecordingEggsIn(
	ctx context.Context,
	tx *gorm.DB,
	eggs []entity.RecordingEgg,
	note string,
	actorID uint,
) error {
	if len(eggs) == 0 {
		return nil
	}
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for applying recording egg reflow")
		return errors.New("fifo v2 service is not available")
	}
	shouldWriteLog := shouldWriteRecordingStockLog(note, actorID)
	if shouldWriteLog && s.StockLogRepo == nil {
		return errors.New("stock log repository is not available")
	}
	logState := newRecordingStockLogState()

	for _, egg := range eggs {
		if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
			continue
		}
		s.logEggTrace("reflow_apply:start", egg, "")

		if err := s.Repository.UpdateEggTotalQty(tx, egg.Id, float64(egg.Qty)); err != nil {
			return err
		}
		if err := s.reflowRecordingScope(
			ctx,
			tx,
			egg.ProductWarehouseId,
			egg.RecordingId,
			recordingLaneStockable,
			recordingFunctionEggIn,
			recordingSourceEggs,
		); err != nil {
			s.Log.Errorf("Failed to reflow FIFO v2 stock for recording egg %d: %+v", egg.Id, err)
			return err
		}
		s.logEggTrace("reflow_apply:done", egg, "")

		if shouldWriteLog {
			log := &entity.StockLog{
				ProductWarehouseId: egg.ProductWarehouseId,
				CreatedBy:          actorID,
				Increase:           float64(egg.Qty),
				LoggableType:       string(utils.StockLogTypeRecording),
				LoggableId:         egg.RecordingId,
				Notes:              note,
			}
			if err := s.appendRecordingStockLog(ctx, tx, logState, log); err != nil {
				return err
			}
		}
	}

	return nil
}

func (s *recordingService) reflowResetRecordingEggsIn(
	ctx context.Context,
	tx *gorm.DB,
	eggs []entity.RecordingEgg,
) error {
	if len(eggs) == 0 {
		return nil
	}
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for reducing recording eggs")
		return errors.New("fifo v2 service is not available")
	}

	for _, egg := range eggs {
		if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
			continue
		}
		s.logEggTrace("reflow_reset:start", egg, "")
		if err := s.Repository.UpdateEggTotalQty(tx, egg.Id, 0); err != nil {
			return err
		}
		if err := s.reflowRecordingScope(
			ctx,
			tx,
			egg.ProductWarehouseId,
			egg.RecordingId,
			recordingLaneStockable,
			recordingFunctionEggIn,
			recordingSourceEggs,
		); err != nil {
			s.Log.Errorf("Failed to reflow FIFO v2 stock for recording egg %d: %+v", egg.Id, err)
			return err
		}
		s.logEggTrace("reflow_reset:done", egg, "")
	}

	return nil
}

func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error {
	for _, egg := range eggs {
		if egg.TotalUsed > 0 {
			return fiber.NewError(fiber.StatusBadRequest, "Recording egg sudah digunakan sehingga tidak dapat diubah")
		}
	}
	return nil
}

func (s *recordingService) recalculateFrom(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, from time.Time) error {
	if tx == nil || projectFlockKandangId == 0 || from.IsZero() {
		return nil
	}

	fromUTC := from.UTC()
	records, err := s.Repository.ListByProjectFlockKandangID(ctx, tx, projectFlockKandangId, &fromUTC)
	if err != nil {
		return err
	}

	for i := range records {
		if err := s.computeAndUpdateMetrics(ctx, tx, &records[i]); err != nil {
			return err
		}
	}

	return nil
}

func (s *recordingService) reflowRollbackRecordingInventory(ctx context.Context, tx *gorm.DB, recordingID uint, note string, actorID uint) error {
	if recordingID == 0 || tx == nil {
		return nil
	}
	if err := s.requireFIFO(); err != nil {
		return err
	}

	oldDepletions, err := s.Repository.ListDepletions(tx, recordingID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		s.Log.Errorf("Failed to list depletions: %+v", err)
		return err
	}

	oldEggs, err := s.Repository.ListEggs(tx, recordingID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		s.Log.Errorf("Failed to list eggs: %+v", err)
		return err
	}
	if err := ensureRecordingEggsUnused(oldEggs); err != nil {
		return err
	}
	if err := s.reflowResetRecordingDepletionsOut(ctx, tx, oldDepletions, note, actorID); err != nil {
		return err
	}

	oldStocks, err := s.Repository.ListStocks(tx, recordingID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		s.Log.Errorf("Failed to list stocks: %+v", err)
		return err
	}
	if err := s.reflowResetRecordingStocks(ctx, tx, oldStocks, note, actorID); err != nil {
		return err
	}

	if err := s.reflowResetRecordingDepletionsIn(ctx, tx, oldDepletions); err != nil {
		return err
	}
	if err := s.reflowResetRecordingEggsIn(ctx, tx, oldEggs); err != nil {
		return err
	}

	if err := s.logRecordingEggRollback(ctx, tx, oldEggs, note, actorID); err != nil {
		return err
	}

	return nil
}

func (s *recordingService) requireFIFO() error {
	if s.FifoStockV2Svc == nil {
		s.Log.Errorf("FIFO v2 service is not available for recording operations")
		return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is required for recording operations")
	}
	return nil
}
