package service

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

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/sirupsen/logrus"
	common "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"
	adjustmentStockRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
	ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
	warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
	projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
	"gorm.io/gorm"
)

type AdjustmentService interface {
	Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.AdjustmentStock, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.AdjustmentStock, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
	AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error)
}

type adjustmentService struct {
	Log                        *logrus.Logger
	Validate                   *validator.Validate
	StockLogsRepository        stockLogsRepo.StockLogRepository
	WarehouseRepo              warehouseRepo.WarehouseRepository
	ProductWarehouseRepo       ProductWarehouse.ProductWarehouseRepository
	ProductRepo                productRepo.ProductRepository
	ProjectFlockKandangRepo    projectFlockKandangRepo.ProjectFlockKandangRepository
	ProjectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository
	AdjustmentStockRepository  adjustmentStockRepo.AdjustmentStockRepository
	FifoStockV2Svc             common.FifoStockV2Service
}

const (
	adjustmentLaneStockable = "STOCKABLE"
	adjustmentLaneUsable    = "USABLE"
)

func NewAdjustmentService(
	productRepo productRepo.ProductRepository,
	stockLogsRepo stockLogsRepo.StockLogRepository,
	warehouseRepo warehouseRepo.WarehouseRepository,
	productWarehouseRepo ProductWarehouse.ProductWarehouseRepository,
	adjustmentStockRepo adjustmentStockRepo.AdjustmentStockRepository,
	fifoStockV2Svc common.FifoStockV2Service,
	validate *validator.Validate,
	projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
	projectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository,
) AdjustmentService {
	return &adjustmentService{
		Log:                        utils.Log,
		Validate:                   validate,
		StockLogsRepository:        stockLogsRepo,
		WarehouseRepo:              warehouseRepo,
		ProductWarehouseRepo:       productWarehouseRepo,
		ProductRepo:                productRepo,
		ProjectFlockKandangRepo:    projectFlockKandangRepo,
		ProjectFlockPopulationRepo: projectFlockPopulationRepo,
		AdjustmentStockRepository:  adjustmentStockRepo,
		FifoStockV2Svc:             fifoStockV2Svc,
	}
}

func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) {
	if err := m.EnsureStockLogAccess(c, s.StockLogsRepository.DB(), id); err != nil {
		return nil, err
	}

	adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
		return db.
			Preload("ProductWarehouse").
			Preload("ProductWarehouse.Product").
			Preload("ProductWarehouse.Warehouse").
			Preload("ProductWarehouse.Warehouse.Location").
			Preload("ProductWarehouse.ProjectFlockKandang").
			Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock").
			Preload("StockLog.CreatedUser")
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
		}
		s.Log.Errorf("Failed to get adjustment by id: %+v", err)
		return nil, err
	}

	return adjustmentStock, nil
}

func (s *adjustmentService) DeleteOne(c *fiber.Ctx, id uint) error {
	if id == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid adjustment id")
	}
	if s.FifoStockV2Svc == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
	}
	if err := m.EnsureStockLogAccess(c, s.StockLogsRepository.DB(), id); err != nil {
		return err
	}

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

	return s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		adjustments, err := s.collectAdjustmentsForDelete(ctx, tx, id)
		if err != nil {
			return err
		}
		for _, item := range adjustments {
			if err := s.deleteSingleAdjustmentInTx(ctx, tx, item, actorID); err != nil {
				return err
			}
		}
		return nil
	})
}

func (s *adjustmentService) collectAdjustmentsForDelete(ctx context.Context, tx *gorm.DB, id uint) ([]entity.AdjustmentStock, error) {
	repoTx := s.AdjustmentStockRepository.WithTx(tx)
	adjustment, err := repoTx.GetByIDForUpdate(ctx, id)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load adjustment")
	}

	adjustments := []entity.AdjustmentStock{*adjustment}
	leftPairCode := utils.NormalizeUpper(adjustment.FunctionCode)
	isDepletionCode := leftPairCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) ||
		leftPairCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut)
	if !isDepletionCode {
		return adjustments, nil
	}
	if adjustment.PairedAdjustmentId == nil || *adjustment.PairedAdjustmentId == 0 {
		return nil, fiber.NewError(
			fiber.StatusBadRequest,
			"Adjustment depletion tidak memiliki pasangan valid. Data harus diperbaiki terlebih dahulu untuk mencegah orphan.",
		)
	}

	pair, err := repoTx.GetByIDForUpdate(ctx, *adjustment.PairedAdjustmentId)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf("Pasangan adjustment depletion (%d) tidak ditemukan. Data harus diperbaiki terlebih dahulu untuk mencegah orphan.", *adjustment.PairedAdjustmentId),
			)
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load paired adjustment")
	}
	rightPairCode := utils.NormalizeUpper(pair.FunctionCode)
	isPairDepletionCode := rightPairCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) ||
		rightPairCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut)
	if !isPairDepletionCode {
		return nil, fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf("Pasangan adjustment %d bukan depletion pair yang valid", pair.Id),
		)
	}
	if pair.PairedAdjustmentId == nil || *pair.PairedAdjustmentId != adjustment.Id {
		return nil, fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf("Pasangan adjustment depletion tidak konsisten (%d <-> %d). Perbaiki pairing terlebih dahulu.", adjustment.Id, pair.Id),
		)
	}
	isValidPair := (leftPairCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) &&
		rightPairCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut)) ||
		(leftPairCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut) &&
			rightPairCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn))
	if !isValidPair {
		return nil, fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf("Pasangan function_code depletion tidak valid (%s <-> %s)", adjustment.FunctionCode, pair.FunctionCode),
		)
	}

	adjustments = append(adjustments, *pair)
	sort.Slice(adjustments, func(i, j int) bool {
		return adjustments[i].Id < adjustments[j].Id
	})
	return adjustments, nil
}

func (s *adjustmentService) deleteSingleAdjustmentInTx(
	ctx context.Context,
	tx *gorm.DB,
	adjustment entity.AdjustmentStock,
	actorID uint,
) error {
	repoTx := s.AdjustmentStockRepository.WithTx(tx)
	productID, err := repoTx.FindProductIDByProductWarehouseID(ctx, adjustment.ProductWarehouseId)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to load product warehouse context")
	}

	routeMeta, err := s.resolveRouteByFunctionCode(ctx, productID, adjustment.FunctionCode)
	if err != nil {
		return err
	}
	isAyamProduct, err := repoTx.IsAyamProduct(ctx, productID)
	if err != nil {
		s.Log.Errorf("Failed to resolve AYAM flag for product %d: %+v", productID, err)
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product flag")
	}

	stockLogRepoTx := stockLogsRepo.NewStockLogRepository(tx)
	notes := fmt.Sprintf("ADJUSTMENT DELETE#%s", utils.NormalizeTrim(adjustment.AdjNumber))

	switch routeMeta.Lane {
	case adjustmentLaneStockable:
		deps, allowPending, err := s.resolveAdjustmentDependenciesAndPolicy(
			ctx,
			tx,
			fifo.StockableKeyAdjustmentIn.String(),
			[]uint{adjustment.Id},
		)
		if err != nil {
			return err
		}
		if len(deps) > 0 && isAyamProduct {
			return fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf(
					"Adjustment tidak dapat dihapus karena produk AYAM sudah dipakai transaksi turunan. Dependensi aktif: %s. Alasan block: produk AYAM yang sudah terpakai tidak dapat dihapus.",
					formatAdjustmentDependencySummary(deps),
				),
			)
		}
		if len(deps) > 0 && !allowPending {
			return fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf(
					"Adjustment tidak dapat dihapus karena stok adjustment sudah dipakai transaksi turunan. Dependensi aktif: %s. Alasan block: pending disabled by config.",
					formatAdjustmentDependencySummary(deps),
				),
			)
		}

		oldQty := adjustment.TotalQty
		if oldQty > 0 {
			if err := repoTx.UpdateTotalQty(ctx, adjustment.Id, 0); err != nil {
				return err
			}
			asOf := adjustment.CreatedAt
			if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
				FlagGroupCode:      routeMeta.FlagGroupCode,
				ProductWarehouseID: adjustment.ProductWarehouseId,
				AsOf:               &asOf,
				Tx:                 tx,
			}); err != nil {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
			}
			if err := s.createAdjustmentStockLog(
				ctx,
				stockLogRepoTx,
				adjustment.Id,
				adjustment.ProductWarehouseId,
				notes,
				actorID,
				0,
				oldQty,
			); err != nil {
				return err
			}
		}
	case adjustmentLaneUsable:
		activeBeforeRollback, err := repoTx.CountActiveConsumeAllocationsByUsable(ctx, fifo.UsableKeyAdjustmentOut.String(), adjustment.Id)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate adjustment allocations before rollback")
		}
		rollbackRes, err := s.FifoStockV2Svc.Rollback(ctx, common.FifoStockV2RollbackRequest{
			ProductWarehouseID: adjustment.ProductWarehouseId,
			Usable: common.FifoStockV2Ref{
				ID:            adjustment.Id,
				LegacyTypeKey: fifo.UsableKeyAdjustmentOut.String(),
			},
			Reason: notes,
			Tx:     tx,
		})
		if err != nil {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to rollback FIFO v2 adjustment: %v", err))
		}
		activeAfterRollback, err := repoTx.CountActiveConsumeAllocationsByUsable(ctx, fifo.UsableKeyAdjustmentOut.String(), adjustment.Id)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate adjustment allocations after rollback")
		}
		if activeAfterRollback > 0 {
			return fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf(
					"Adjustment tidak dapat dihapus karena masih ada alokasi aktif ADJUSTMENT_OUT=%d (sebelum rollback=%d, sesudah rollback=%d).",
					adjustment.Id,
					activeBeforeRollback,
					activeAfterRollback,
				),
			)
		}

		releasedQty := 0.0
		if rollbackRes != nil {
			releasedQty = rollbackRes.ReleasedQty
		}
		if releasedQty > 0 {
			if err := s.createAdjustmentStockLog(
				ctx,
				stockLogRepoTx,
				adjustment.Id,
				adjustment.ProductWarehouseId,
				notes,
				actorID,
				releasedQty,
				0,
			); err != nil {
				return err
			}
		}
	default:
		return fiber.NewError(fiber.StatusBadRequest, "Unsupported adjustment lane")
	}

	if err := repoTx.DeleteStockLogsByAdjustmentID(ctx, adjustment.Id); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete adjustment stock logs")
	}
	if err := repoTx.DeleteAdjustmentByID(ctx, adjustment.Id); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete adjustment")
	}
	return nil
}

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

	productID := req.ProductID
	if productID == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Product is required")
	}

	qty := req.Qty
	if qty <= 0 {
		qty = req.Quantity
	}
	if qty <= 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
	}

	functionCode := utils.NormalizeUpper(req.TransactionSubtype)
	if functionCode == "" {
		functionCode = utils.NormalizeUpper(req.TransactionSubType)
	}
	if functionCode == "" {
		functionCode = utils.NormalizeUpper(req.FunctionCode)
	}
	if functionCode == "" {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Transaction subtype is required")
	}
	if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut) {
		return nil, fiber.NewError(
			fiber.StatusBadRequest,
			"RECORDING_DEPLETION_OUT tidak boleh diinput manual. Gunakan RECORDING_DEPLETION_IN, sistem akan otomatis membuat depletion-out AYAM",
		)
	}

	warehouseID, err := s.resolveWarehouseID(c.Context(), req)
	if err != nil {
		return nil, err
	}

	note := utils.NormalizeTrim(req.Notes)
	if note == "" {
		note = utils.NormalizeTrim(req.Note)
	}
	grandTotal := math.Round((qty*req.Price)*1000) / 1000

	ctx := c.Context()
	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return nil, err
	}
	if err := m.EnsureWarehouseAccess(c, s.WarehouseRepo.DB(), warehouseID); err != nil {
		return nil, err
	}
	if err := common.EnsureRelations(ctx,
		common.RelationCheck{Name: "Product", ID: &productID, Exists: s.ProductRepo.IdExists},
		common.RelationCheck{Name: "Warehouse", ID: &warehouseID, Exists: s.WarehouseRepo.IdExists},
	); err != nil {
		return nil, err
	}

	routeMeta, err := s.resolveRouteByFunctionCode(ctx, productID, functionCode)
	if err != nil {
		return nil, err
	}

	transactionType := utils.ResolveAdjustmentTransactionType(routeMeta.FunctionCode)

	var createdAdjustmentStockId uint

	var projectFlockKandangID *uint
	pfkID, err := s.getActiveProjectFlockKandangID(ctx, warehouseID)
	if err == nil && pfkID > 0 {
		projectFlockKandangID = &pfkID
	}

	pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, productID, warehouseID, projectFlockKandangID)
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
		}

		newPW := &entity.ProductWarehouse{
			ProductId:             productID,
			WarehouseId:           warehouseID,
			Quantity:              0,
			ProjectFlockKandangId: projectFlockKandangID,
		}

		if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
		}
		pw = newPW
	}

	if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
		ctx,
		s.StockLogsRepository.DB(),
		[]uint{pw.Id},
	); err != nil {
		return nil, err
	}
	err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		productWarehouseRepoTX := ProductWarehouse.NewProductWarehouseRepository(tx)
		stockLogRepoTX := stockLogsRepo.NewStockLogRepository(tx)
		adjustmentStockRepoTX := s.AdjustmentStockRepository.WithTx(tx)

		productWarehouse, err := productWarehouseRepoTX.FindByProductWarehouseAndPfk(ctx, productID, warehouseID, projectFlockKandangID)
		if err != nil {
			s.Log.Errorf("Failed to get product warehouse: %+v", err)
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
		}

		if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) {
			if routeMeta.Lane != adjustmentLaneStockable {
				return fiber.NewError(fiber.StatusBadRequest, "Transaction subtype depletion in harus lane STOCKABLE")
			}
			if projectFlockKandangID == nil || *projectFlockKandangID == 0 {
				return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id aktif wajib tersedia untuk depletion conversion")
			}
			if s.FifoStockV2Svc == nil {
				return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
			}

			sourcePW, err := adjustmentStockRepoTX.FindAyamSourceProductWarehouse(ctx, warehouseID, *projectFlockKandangID)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusBadRequest, "Produk sumber AYAM pada project flock kandang yang sama tidak ditemukan")
				}
				return err
			}
			if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
				ctx,
				tx,
				[]uint{productWarehouse.Id, sourcePW.Id},
			); err != nil {
				return err
			}

			sourceRoute, err := s.resolveRouteByFunctionCode(
				ctx,
				sourcePW.ProductId,
				string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut),
			)
			if err != nil {
				return err
			}
			if sourceRoute.Lane != adjustmentLaneUsable {
				return fiber.NewError(fiber.StatusBadRequest, "Route depletion out untuk produk AYAM tidak valid")
			}

			sourceCode, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
			if err != nil {
				return err
			}
			sourceAdjustment := &entity.AdjustmentStock{
				ProductWarehouseId: sourcePW.Id,
				TransactionType:    transactionType,
				FunctionCode:       sourceRoute.FunctionCode,
				UsageQty:           qty,
				Price:              req.Price,
				GrandTotal:         grandTotal,
				AdjNumber:          sourceCode,
			}
			if err := adjustmentStockRepoTX.CreateOne(ctx, sourceAdjustment, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to create depletion source adjustment stock record")
			}

			destCode, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
			if err != nil {
				return err
			}
			destinationAdjustment := &entity.AdjustmentStock{
				ProductWarehouseId: productWarehouse.Id,
				TransactionType:    transactionType,
				FunctionCode:       routeMeta.FunctionCode,
				TotalQty:           qty,
				Price:              req.Price,
				GrandTotal:         grandTotal,
				AdjNumber:          destCode,
			}
			if err := adjustmentStockRepoTX.CreateOne(ctx, destinationAdjustment, nil); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to create depletion destination adjustment stock record")
			}
			if err := adjustmentStockRepoTX.UpdatePairedAdjustmentID(ctx, sourceAdjustment.Id, destinationAdjustment.Id); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to link depletion source adjustment pair")
			}
			if err := adjustmentStockRepoTX.UpdatePairedAdjustmentID(ctx, destinationAdjustment.Id, sourceAdjustment.Id); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to link depletion destination adjustment pair")
			}
			sourceAdjustment.PairedAdjustmentId = &destinationAdjustment.Id
			destinationAdjustment.PairedAdjustmentId = &sourceAdjustment.Id

			sourceAsOf := sourceAdjustment.CreatedAt
			if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
				FlagGroupCode:      sourceRoute.FlagGroupCode,
				ProductWarehouseID: sourcePW.Id,
				AsOf:               &sourceAsOf,
				Tx:                 tx,
			}); err != nil {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to auto depletion-out AYAM via FIFO v2: %v", err))
			}

			destinationAsOf := destinationAdjustment.CreatedAt
			if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
				FlagGroupCode:      routeMeta.FlagGroupCode,
				ProductWarehouseID: destinationAdjustment.ProductWarehouseId,
				AsOf:               &destinationAsOf,
				Tx:                 tx,
			}); err != nil {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to auto depletion-in destination via FIFO v2: %v", err))
			}

			refreshedSource, err := adjustmentStockRepoTX.GetByID(ctx, sourceAdjustment.Id, nil)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion source adjustment stock")
			}
			refreshedDestination, err := adjustmentStockRepoTX.GetByID(ctx, destinationAdjustment.Id, nil)
			if err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion destination adjustment stock")
			}
			consumedPopulationQty := refreshedSource.UsageQty + refreshedSource.PendingQty
			if consumedPopulationQty > 0 {
				if err := s.allocatePopulationForDepletionAdjustment(
					ctx,
					tx,
					*projectFlockKandangID,
					sourcePW.Id,
					refreshedSource.Id,
					consumedPopulationQty,
				); err != nil {
					return err
				}
				if err := adjustmentStockRepoTX.ResyncProjectFlockPopulationUsage(ctx, *projectFlockKandangID); err != nil {
					return fiber.NewError(fiber.StatusInternalServerError, "Failed to resync project flock population usage")
				}
			}

			if err := s.createAdjustmentStockLog(
				ctx,
				stockLogRepoTX,
				refreshedSource.Id,
				refreshedSource.ProductWarehouseId,
				note,
				actorID,
				0,
				refreshedSource.UsageQty+refreshedSource.PendingQty,
			); err != nil {
				return err
			}
			if err := s.createAdjustmentStockLog(
				ctx,
				stockLogRepoTX,
				refreshedDestination.Id,
				refreshedDestination.ProductWarehouseId,
				note,
				actorID,
				refreshedDestination.TotalQty,
				0,
			); err != nil {
				return err
			}

			createdAdjustmentStockId = destinationAdjustment.Id
			return nil
		}

		adjustmentStock := &entity.AdjustmentStock{
			ProductWarehouseId: productWarehouse.Id,
			TransactionType:    transactionType,
			FunctionCode:       routeMeta.FunctionCode,
			Price:              req.Price,
			GrandTotal:         grandTotal,
		}
		switch routeMeta.Lane {
		case adjustmentLaneStockable:
			adjustmentStock.TotalQty = qty
		case adjustmentLaneUsable:
			adjustmentStock.UsageQty = qty
		}
		code, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
		if err != nil {
			return err
		}
		adjustmentStock.AdjNumber = code
		if err := adjustmentStockRepoTX.CreateOne(ctx, adjustmentStock, nil); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
		}

		var increaseQty float64
		var decreaseQty float64

		if routeMeta.Lane != adjustmentLaneStockable && routeMeta.Lane != adjustmentLaneUsable {
			return fiber.NewError(fiber.StatusBadRequest, "Unsupported transaction subtype lane")
		}
		if s.FifoStockV2Svc == nil {
			return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
		}

		asOf := adjustmentStock.CreatedAt
		if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
			FlagGroupCode:      routeMeta.FlagGroupCode,
			ProductWarehouseID: productWarehouse.Id,
			AsOf:               &asOf,
			Tx:                 tx,
		}); err != nil {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
		}

		refreshedAdjustment, err := adjustmentStockRepoTX.GetByID(ctx, adjustmentStock.Id, nil)
		if err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh adjustment stock")
		}
		switch routeMeta.Lane {
		case adjustmentLaneStockable:
			increaseQty = refreshedAdjustment.TotalQty
		case adjustmentLaneUsable:
			decreaseQty = refreshedAdjustment.UsageQty
		}

		if err := s.createAdjustmentStockLog(
			ctx,
			stockLogRepoTX,
			adjustmentStock.Id,
			productWarehouse.Id,
			note,
			actorID,
			increaseQty,
			decreaseQty,
		); err != nil {
			return err
		}

		createdAdjustmentStockId = adjustmentStock.Id
		return nil
	})

	if err != nil {
		s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
		var fiberErr *fiber.Error
		if errors.As(err, &fiberErr) {
			return nil, err
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction")
	}

	return s.GetOne(c, createdAdjustmentStockId)
}

func (s *adjustmentService) resolveWarehouseID(ctx context.Context, req *validation.Create) (uint, error) {
	if req == nil {
		return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid request")
	}

	if req.WarehouseID > 0 {
		return req.WarehouseID, nil
	}

	if req.ProjectFlockKandangID != nil && *req.ProjectFlockKandangID > 0 {
		kandangID, err := s.AdjustmentStockRepository.FindKandangIDByProjectFlockKandangID(ctx, *req.ProjectFlockKandangID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id tidak valid atau tidak aktif")
			}
			return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project_flock_kandang_id context")
		}

		warehouse, err := s.WarehouseRepo.GetLatestByKandangID(ctx, kandangID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse untuk project_flock_kandang_id %d tidak ditemukan", *req.ProjectFlockKandangID))
			}
			return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve warehouse by project_flock_kandang_id")
		}
		return warehouse.Id, nil
	}

	return 0, fiber.NewError(fiber.StatusBadRequest, "warehouse_id atau project_flock_kandang_id wajib diisi")
}

func (s *adjustmentService) resolveRouteByFunctionCode(
	ctx context.Context,
	productID uint,
	functionCode string,
) (*adjustmentStockRepo.AdjustmentRouteResolution, error) {
	rows, err := s.AdjustmentStockRepository.FindRoutesByFunctionCode(ctx, productID, functionCode)
	if err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Transaction subtype tidak kompatibel dengan konfigurasi FIFO v2 produk")
	}

	selected := rows[0]
	for _, row := range rows {
		if row.Lane != selected.Lane {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Transaction subtype ambigu: lane FIFO v2 lebih dari satu")
		}
	}

	selected.FunctionCode = functionCode
	switch selected.Lane {
	case adjustmentLaneStockable, adjustmentLaneUsable:
		return &selected, nil
	default:
		return nil, fiber.NewError(fiber.StatusBadRequest, "Transaction subtype memiliki lane FIFO v2 yang tidak didukung")
	}
}

func (s *adjustmentService) resolveAdjustmentDependenciesAndPolicy(
	ctx context.Context,
	tx *gorm.DB,
	stockableType string,
	stockableIDs []uint,
) ([]adjustmentStockRepo.AdjustmentDownstreamDependency, bool, error) {
	deps, err := s.AdjustmentStockRepository.WithTx(tx).LoadDownstreamDependencies(ctx, stockableType, stockableIDs)
	if err != nil {
		s.Log.Errorf("Failed to load downstream adjustment dependencies: %+v", err)
		return nil, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate downstream adjustment dependencies")
	}
	if len(deps) == 0 {
		return nil, true, nil
	}

	allowPending := true
	for _, dep := range deps {
		policy, policyErr := common.ResolveFifoPendingPolicy(ctx, tx, common.FifoPendingPolicyInput{
			Lane:          adjustmentLaneUsable,
			FlagGroupCode: dep.FlagGroupCode,
			FunctionCode:  dep.FunctionCode,
			LegacyTypeKey: dep.UsableType,
		})
		if policyErr != nil {
			s.Log.Errorf("Failed to resolve FIFO pending policy for adjustment dependency: %+v", policyErr)
			return nil, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to read FIFO v2 configuration")
		}
		if !policy.Found || !policy.AllowPending {
			allowPending = false
			break
		}
	}

	return deps, allowPending, nil
}

func formatAdjustmentDependencySummary(rows []adjustmentStockRepo.AdjustmentDownstreamDependency) string {
	if len(rows) == 0 {
		return "-"
	}

	grouped := make(map[string]map[uint64]struct{})
	for _, row := range rows {
		label := utils.NormalizeUpper(row.UsableType)
		if label == "" {
			label = "UNKNOWN"
		}
		if _, ok := grouped[label]; !ok {
			grouped[label] = make(map[uint64]struct{})
		}
		grouped[label][row.UsableID] = struct{}{}
	}

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

	parts := make([]string, 0, len(labels))
	for _, label := range labels {
		ids := make([]uint64, 0, len(grouped[label]))
		for id := range grouped[label] {
			ids = append(ids, id)
		}
		sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
		idParts := make([]string, 0, len(ids))
		for _, id := range ids {
			idParts = append(idParts, fmt.Sprintf("%d", id))
		}
		parts = append(parts, fmt.Sprintf("%s=%s", label, strings.Join(idParts, "|")))
	}

	return strings.Join(parts, ", ")
}

func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
	warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
		}
		s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
	}

	if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
		return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
	}

	projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
		}
		s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
	}

	return uint(projectFlockKandang.Id), nil
}

func (s *adjustmentService) createAdjustmentStockLog(
	ctx context.Context,
	stockLogRepo stockLogsRepo.StockLogRepository,
	adjustmentID uint,
	productWarehouseID uint,
	note string,
	actorID uint,
	increaseQty float64,
	decreaseQty float64,
) error {
	if stockLogRepo == nil || adjustmentID == 0 || productWarehouseID == 0 {
		return nil
	}
	if increaseQty == 0 && decreaseQty == 0 {
		return nil
	}

	stockLogs, err := stockLogRepo.GetByProductWarehouse(ctx, productWarehouseID, 1)
	if err != nil {
		s.Log.Errorf("Failed to get stock logs: %+v", err)
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
	}

	currentStock := 0.0
	if len(stockLogs) > 0 {
		currentStock = stockLogs[0].Stock
	}

	newLog := &entity.StockLog{
		LoggableType:       string(utils.StockLogTypeAdjustment),
		LoggableId:         adjustmentID,
		Notes:              note,
		ProductWarehouseId: productWarehouseID,
		CreatedBy:          actorID,
		Increase:           increaseQty,
		Decrease:           decreaseQty,
		Stock:              currentStock + increaseQty - decreaseQty,
	}

	return stockLogRepo.CreateOne(ctx, newLog, nil)
}

func (s *adjustmentService) allocatePopulationForDepletionAdjustment(
	ctx context.Context,
	tx *gorm.DB,
	projectFlockKandangID uint,
	sourceProductWarehouseID uint,
	adjustmentID uint,
	consumeQty float64,
) error {
	if consumeQty <= 0 {
		return nil
	}
	if tx == nil {
		return errors.New("transaction is required")
	}
	if projectFlockKandangID == 0 || sourceProductWarehouseID == 0 || adjustmentID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid depletion adjustment population context")
	}
	if s.ProjectFlockPopulationRepo == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not available")
	}

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

	return fifoV2.AllocatePopulationConsumption(
		ctx,
		tx,
		populations,
		sourceProductWarehouseID,
		fifo.UsableKeyAdjustmentOut.String(),
		adjustmentID,
		consumeQty,
	)
}

func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
	if err := s.Validate.Struct(query); err != nil {
		return nil, 0, err
	}
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.Limit <= 0 {
		query.Limit = 10
	}
	offset := (query.Page - 1) * query.Limit

	var isProductsExist bool
	isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))

	if err != nil {
		return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
	}
	if query.WarehouseID > 0 && !isWarehousesExist {
		return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
	}

	isProductsExist, err = s.ProductRepo.IdExists(c.Context(), uint(query.ProductID))

	if err != nil {
		s.Log.Errorf("Failed to check product existence: %+v", err)
		return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
	}
	if query.ProductID > 0 && !isProductsExist {
		return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
	}

	scope, scopeErr := m.ResolveLocationScope(c, s.AdjustmentStockRepository.DB())
	if scopeErr != nil {
		return nil, 0, scopeErr
	}
	if scope.Restrict {
		if len(scope.IDs) == 0 {
			return []*entity.AdjustmentStock{}, 0, nil
		}
	}

	functionCode := utils.NormalizeUpper(query.TransactionSubtype)
	if functionCode == "" {
		functionCode = utils.NormalizeUpper(query.FunctionCode)
	}
	transactionType := utils.NormalizeUpper(query.TransactionType)

	adjustmentStocks, total, err := s.AdjustmentStockRepository.FindHistory(
		c.Context(),
		adjustmentStockRepo.AdjustmentHistoryFilter{
			ProductID:       query.ProductID,
			WarehouseID:     query.WarehouseID,
			TransactionType: transactionType,
			FunctionCode:    functionCode,
			ScopeRestrict:   scope.Restrict,
			ScopeIDs:        scope.IDs,
			Offset:          offset,
			Limit:           query.Limit,
		},
		nil,
	)
	if err != nil {
		s.Log.Errorf("Failed to get adjustments: %+v", err)
		return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
	}

	return adjustmentStocks, total, nil
}
