package service

import (
	"context"
	"errors"
	"fmt"
	"mime/multipart"
	"sort"
	"strings"
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/sirupsen/logrus"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
	rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/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"
	rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"

	"gorm.io/gorm"
)

type TransferService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
	CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
	CreateSystemTransfer(ctx context.Context, req *SystemTransferRequest) (*entity.StockTransfer, error)
	DeleteSystemTransfer(ctx context.Context, id uint, actorID uint) error
}

type transferService struct {
	Log                           *logrus.Logger
	Validate                      *validator.Validate
	StockTransferRepo             rStockTransfer.StockTransferRepository
	StockTransferDetailRepo       rStockTransfer.StockTransferDetailRepository
	StockTransferDeliveryRepo     rStockTransfer.StockTransferDeliveryRepository
	StockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository
	StockLogsRepository           rStockLogs.StockLogRepository
	ProductWarehouseRepo          rProductWarehouse.ProductWarehouseRepository
	SupplierRepo                  rSupplier.SupplierRepository
	WarehouseRepo                 warehouseRepo.WarehouseRepository
	ProjectFlockKandangRepo       projectFlockKandangRepo.ProjectFlockKandangRepository
	ProjectFlockPopulationRepo    projectFlockKandangRepo.ProjectFlockPopulationRepository
	DocumentSvc                   commonSvc.DocumentService
	FifoStockV2Svc                commonSvc.FifoStockV2Service
	ExpenseBridge                 TransferExpenseBridge
}

const transferDeleteDownstreamGuardMessage = "Transfer stock tidak dapat dihapus karena stok transfer sudah dipakai transaksi turunan. Hapus dependensi terkait secara manual terlebih dahulu."

type downstreamDependency struct {
	UsableType    string `gorm:"column:usable_type"`
	UsableID      uint64 `gorm:"column:usable_id"`
	FunctionCode  string `gorm:"column:function_code"`
	FlagGroupCode string `gorm:"column:flag_group_code"`
}

type SystemTransferProduct struct {
	ProductID  uint
	ProductQty float64
}

type SystemTransferRequest struct {
	TransferReason         string
	TransferDate           time.Time
	SourceWarehouseID      uint
	DestinationWarehouseID uint
	Products               []SystemTransferProduct
	ActorID                uint
	MovementNumber         string
	StockLogNotes          string
}

type transferMovementResult struct {
	Transfer    *entity.StockTransfer
	DetailByPID map[uint64]*entity.StockTransferDetail
}

func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, projectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository, documentSvc commonSvc.DocumentService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService {
	return &transferService{
		Log:                           utils.Log,
		Validate:                      validate,
		StockTransferRepo:             stockTransferRepo,
		StockTransferDetailRepo:       stockTransferDetailRepo,
		StockTransferDeliveryRepo:     stockTransferDeliveryRepo,
		StockTransferDeliveryItemRepo: stockTransferDeliveryItemRepo,
		StockLogsRepository:           stockLogsRepo,
		ProductWarehouseRepo:          productWarehouseRepo,
		SupplierRepo:                  supplierRepo,
		WarehouseRepo:                 warehouseRepo,
		ProjectFlockKandangRepo:       projectFlockKandangRepo,
		ProjectFlockPopulationRepo:    projectFlockPopulationRepo,
		DocumentSvc:                   documentSvc,
		FifoStockV2Svc:                fifoStockV2Svc,
		ExpenseBridge:                 expenseBridge,
	}
}

func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
	return db.
		Preload("CreatedUser").
		Preload("FromWarehouse").
		Preload("FromWarehouse.Location").
		Preload("FromWarehouse.Area").
		Preload("ToWarehouse").
		Preload("ToWarehouse.Location").
		Preload("ToWarehouse.Area").
		Preload("Details").
		Preload("Details.Product").
		Preload("Details.ExpenseNonstock").
		Preload("Details.ExpenseNonstock.Expense").
		Preload("Details.ExpenseNonstock.Expense.Supplier").
		Preload("Deliveries.Items").
		Preload("Deliveries.Supplier").
		Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB {
			return db.Where("documentable_type = ?", string(utils.DocumentableTypeTransfer))
		})
}

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

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

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

	transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
		db = s.withRelations(db)
		db = db.Where("stock_transfers.deleted_at IS NULL")
		if scope.Restrict {
			if len(scope.IDs) == 0 {
				return db.Where("1 = 0")
			}
			db = db.
				Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
				Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
				Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs)
		}
		if params.Search != "" {
			searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
			db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
				Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
				Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
					searchTerm, searchTerm, searchTerm)
		}
		return db.Order("created_at DESC").Order("updated_at DESC")
	})

	if err != nil {
		return nil, 0, err
	}

	return transfers, total, nil
}

func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
	scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB())
	if err != nil {
		return nil, err
	}
	if scope.Restrict {
		if len(scope.IDs) == 0 {
			return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
		}
		var count int64
		if err := s.StockTransferRepo.DB().WithContext(c.Context()).
			Table("stock_transfers").
			Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
			Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
			Where("stock_transfers.id = ?", id).
			Where("stock_transfers.deleted_at IS NULL").
			Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs).
			Count(&count).Error; err != nil {
			return nil, err
		}
		if count == 0 {
			return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
		}
	}

	transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
		return s.withRelations(db).Where("stock_transfers.deleted_at IS NULL")
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
		}
		s.Log.Errorf("Failed to fetch transfer by ID %d: %+v", id, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data transfer")
	}

	return transferPtr, nil
}

func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
	products := make([]SystemTransferProduct, 0, len(req.Products))
	for _, product := range req.Products {
		products = append(products, SystemTransferProduct{
			ProductID:  uint(product.ProductID),
			ProductQty: product.ProductQty,
		})
	}
	if err := s.validateTransferWarehousesAndProducts(c.Context(), uint(req.SourceWarehouseID), uint(req.DestinationWarehouseID), products); err != nil {
		return nil, err
	}

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

	deliveryQtyMap := make(map[uint]float64)
	for _, delivery := range req.Deliveries {
		for _, prod := range delivery.Products {
			deliveryQtyMap[prod.ProductID] += prod.ProductQty
		}
	}

	for _, product := range req.Products {
		if deliveryQtyMap[product.ProductID] > product.ProductQty {
			return nil, fiber.NewError(fiber.StatusBadRequest,
				fmt.Sprintf("Total qty delivery untuk produk %d (%v) melebihi qty transfer (%v)", product.ProductID, deliveryQtyMap[product.ProductID], product.ProductQty))
		}
	}

	for _, delivery := range req.Deliveries {
		if delivery.SupplierID == 0 {
			continue
		}
		if delivery.VehiclePlate == "" {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Vehicle plate wajib diisi ketika supplier dipilih")
		}
		if delivery.DriverName == "" {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Driver name wajib diisi ketika supplier dipilih")
		}
		if delivery.DeliveryCost <= 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery cost harus lebih dari 0 ketika supplier dipilih")
		}
		if delivery.DeliveryCostPerItem <= 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery cost per item harus lebih dari 0 ketika supplier dipilih")
		}

		supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
			}
			s.Log.Errorf("Failed to fetch supplier by ID %d: %+v", delivery.SupplierID, err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data supplier")
		}
		if supplier.Category != string(utils.SupplierCategoryBOP) {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category))
		}
	}

	transferDate, _ := utils.ParseDateString(req.TransferDate)
	expensePayloads := make([]TransferExpenseReceivingPayload, 0)
	var detailMap map[uint64]*entity.StockTransferDetail
	var createdTransfer *entity.StockTransfer

	err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
		stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx)
		stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx)

		movementResult, err := s.createTransferMovement(c.Context(), tx, &SystemTransferRequest{
			TransferReason:         req.TransferReason,
			TransferDate:           transferDate,
			SourceWarehouseID:      uint(req.SourceWarehouseID),
			DestinationWarehouseID: uint(req.DestinationWarehouseID),
			Products:               products,
			ActorID:                actorID,
		})
		if err != nil {
			return err
		}
		detailMap = movementResult.DetailByPID
		createdTransfer = movementResult.Transfer

		var deliveries []*entity.StockTransferDelivery
		for _, delivery := range req.Deliveries {
			supplierId := func() *uint64 {
				if delivery.SupplierID > 0 {
					id := uint64(delivery.SupplierID)
					return &id
				}
				return nil
			}()
			deliveries = append(deliveries, &entity.StockTransferDelivery{
				StockTransferId:   createdTransfer.Id,
				SupplierId:        supplierId,
				VehiclePlate:      delivery.VehiclePlate,
				DriverName:        delivery.DriverName,
				ShippingCostItem:  delivery.DeliveryCostPerItem,
				ShippingCostTotal: delivery.DeliveryCost,
			})
		}
		if err := stockTransferDeliveryRepoTX.CreateMany(c.Context(), deliveries, nil); err != nil {
			return err
		}

		var deliveryItems []*entity.StockTransferDeliveryItem
		for i, delivery := range deliveries {
			item := req.Deliveries[i]
			for _, prod := range item.Products {
				detail, ok := detailMap[uint64(prod.ProductID)]
				if !ok {
					return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1))
				}
				deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
					StockTransferDeliveryId: delivery.Id,
					StockTransferDetailId:   detail.Id,
					Quantity:                prod.ProductQty,
				})
			}
		}
		if err := stockTransferDeliveryItemRepoTX.CreateMany(c.Context(), deliveryItems, nil); err != nil {
			return err
		}

		if s.DocumentSvc != nil && len(files) > 0 {
			for deliveryIdx, delivery := range deliveries {
				reqDelivery := req.Deliveries[deliveryIdx]
				if reqDelivery.DocumentIndex < 0 {
					continue
				}
				if reqDelivery.DocumentIndex >= len(files) {
					return fiber.NewError(fiber.StatusBadRequest,
						fmt.Sprintf("DocumentIndex %d untuk delivery %d melebihi jumlah file yang diupload (%d)",
							reqDelivery.DocumentIndex, deliveryIdx+1, len(files)))
				}

				file := files[reqDelivery.DocumentIndex]
				documentFiles := []commonSvc.DocumentFile{{
					File:  file,
					Type:  string(utils.DocumentTypeTransfer),
					Index: &reqDelivery.DocumentIndex,
				}}
				_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
					DocumentableType: string(utils.DocumentableTypeTransfer),
					DocumentableID:   delivery.Id,
					CreatedBy:        &actorID,
					Files:            documentFiles,
				})
				if err != nil {
					s.Log.Errorf("Failed to upload document for delivery %d (delivery_id=%d, filename=%s): %+v",
						deliveryIdx+1, delivery.Id, file.Filename, err)
					return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengunggah dokumen")
				}
			}
		}

		for _, delivery := range req.Deliveries {
			if delivery.SupplierID == 0 {
				continue
			}
			for _, prod := range delivery.Products {
				detail := detailMap[uint64(prod.ProductID)]
				if detail == nil {
					continue
				}
				warehouseID := uint(req.DestinationWarehouseID)
				supplierID := uint(delivery.SupplierID)
				deliveredDate := transferDate
				expensePayloads = append(expensePayloads, TransferExpenseReceivingPayload{
					TransferDetailID: detail.Id,
					ProductID:        uint64(prod.ProductID),
					WarehouseID:      uint64(warehouseID),
					SupplierID:       uint64(supplierID),
					DeliveredQty:     prod.ProductQty,
					DeliveredDate:    &deliveredDate,
				})
			}
		}

		return nil
	})
	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
	}

	result, err := s.GetOne(c, uint(createdTransfer.Id))
	if err != nil {
		return nil, err
	}
	if len(expensePayloads) > 0 {
		if err := s.notifyExpenseItemsDelivered(c, createdTransfer.Id, expensePayloads); err != nil {
			s.Log.Errorf("Failed to sync expense for transfer_id=%d, movement_number=%s: %+v", createdTransfer.Id, createdTransfer.MovementNumber, err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal sinkronisasi data expense. Silakan cek manual di module expense")
		}
	}

	return result, nil
}

func (s *transferService) DeleteOne(c *fiber.Ctx, id uint) error {
	if err := s.ensureTransferAccess(c.Context(), id, c); err != nil {
		return err
	}
	if s.FifoStockV2Svc == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
	}

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

	var deletedDetails []entity.StockTransferDetail
	err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
		var err error
		deletedDetails, err = s.deleteTransferCore(c.Context(), tx, uint64(id), actorID)
		return err
	})
	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return fiberErr
		}
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus transfer")
	}

	if len(deletedDetails) > 0 && s.ExpenseBridge != nil {
		if err := s.ExpenseBridge.OnItemsDeleted(c.Context(), uint64(id), deletedDetails); err != nil {
			s.Log.Errorf("Failed to cleanup transfer expense link for transfer_id=%d: %+v", id, err)
			return fiber.NewError(fiber.StatusInternalServerError, "Transfer berhasil dihapus, namun sinkronisasi expense gagal. Silakan cek modul expense")
		}
	}

	return nil
}

func (s *transferService) resolveTransferFlagGroup(
	ctx context.Context,
	tx *gorm.DB,
	productID uint,
) (string, error) {
	if productID == 0 {
		return "", fmt.Errorf("product id is required")
	}

	type row struct {
		FlagGroupCode string `gorm:"column:flag_group_code"`
	}
	var selected row
	err := tx.WithContext(ctx).
		Table("fifo_stock_v2_route_rules rr").
		Select("rr.flag_group_code").
		Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
		Where("rr.is_active = TRUE").
		Where("rr.lane = ?", "USABLE").
		Where("rr.function_code = ?", "STOCK_TRANSFER_OUT").
		Where("rr.source_table = ?", "stock_transfer_details").
		Where(`
			EXISTS (
				SELECT 1
				FROM products p
				LEFT JOIN product_categories pc ON pc.id = p.product_category_id
				WHERE p.id = ?
				  AND (
					EXISTS (
						SELECT 1
						FROM flags f
						JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
						WHERE f.flagable_type = ?
						  AND f.flagable_id = p.id
						  AND fm.flag_group_code = rr.flag_group_code
					)
					OR (
						NOT EXISTS (
							SELECT 1
							FROM flags f_any
							WHERE f_any.flagable_type = ?
							  AND f_any.flagable_id = p.id
						)
						AND rr.flag_group_code = ?
						AND UPPER(COALESCE(pc.code, '')) = 'EGG'
					)
				  )
			)
		`, productID, entity.FlagableTypeProduct, entity.FlagableTypeProduct, utils.LegacyFlagGroupCodeByProductCategoryCode("EGG")).
		Order("rr.id ASC").
		Limit(1).
		Take(&selected).Error
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(selected.FlagGroupCode), nil
}

func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
	if s.ExpenseBridge == nil || transferID == 0 || len(payloads) == 0 {
		return nil
	}
	return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
}

func (s *transferService) 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 fetch warehouse by ID %d: %+v", warehouseID, err)
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
	}

	if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
		return 0, nil
	}

	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("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId))
		}
		s.Log.Errorf("Failed to fetch active project flock kandang for kandang_id=%d: %+v", *warehouse.KandangId, err)
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
	}

	return uint(projectFlockKandang.Id), nil
}

func (s *transferService) ensureTransferAccess(ctx context.Context, id uint, c *fiber.Ctx) error {
	scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB())
	if err != nil {
		return err
	}
	if !scope.Restrict {
		return nil
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Transfer not found")
	}

	var count int64
	if err := s.StockTransferRepo.DB().WithContext(ctx).
		Table("stock_transfers").
		Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
		Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
		Where("stock_transfers.id = ?", id).
		Where("stock_transfers.deleted_at IS NULL").
		Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs).
		Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Transfer not found")
	}

	return nil
}

func (s *transferService) ensureDeletePolicyForDownstreamConsumption(ctx context.Context, tx *gorm.DB, detailIDs []uint64) error {
	dependencies, err := s.loadActiveTransferDownstreamDependencies(ctx, tx, detailIDs)
	if err != nil {
		s.Log.Errorf("Failed to load downstream stock transfer consumption: %+v", err)
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transaksi turunan transfer stock")
	}
	if len(dependencies) == 0 {
		return nil
	}
	ayamDependency, err := s.hasAyamDownstreamConsumption(ctx, tx, detailIDs)
	if err != nil {
		s.Log.Errorf("Failed to validate AYAM downstream dependency for transfer delete: %+v", err)
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi dependensi AYAM pada transfer stock")
	}
	if ayamDependency {
		return fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf(
				"%s Dependensi aktif: %s. Alasan block: produk AYAM yang sudah terpakai tidak dapat dihapus.",
				transferDeleteDownstreamGuardMessage,
				formatDownstreamDependencySummary(dependencies),
			),
		)
	}

	denyReason := ""
	for _, dep := range dependencies {
		policy, policyErr := commonSvc.ResolveFifoPendingPolicy(ctx, tx, commonSvc.FifoPendingPolicyInput{
			Lane:          "USABLE",
			FlagGroupCode: dep.FlagGroupCode,
			FunctionCode:  dep.FunctionCode,
			LegacyTypeKey: dep.UsableType,
		})
		if policyErr != nil {
			s.Log.Errorf("Failed to resolve FIFO pending policy for transfer dependency: %+v", policyErr)
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal membaca konfigurasi FIFO v2")
		}
		if !policy.Found || !policy.AllowPending {
			denyReason = "pending disabled by config"
			break
		}
	}
	if denyReason == "" {
		return nil
	}

	return fiber.NewError(
		fiber.StatusBadRequest,
		fmt.Sprintf(
			"%s Dependensi aktif: %s. Alasan block: %s.",
			transferDeleteDownstreamGuardMessage,
			formatDownstreamDependencySummary(dependencies),
			denyReason,
		),
	)
}

func (s *transferService) loadActiveTransferDownstreamDependencies(
	ctx context.Context,
	tx *gorm.DB,
	detailIDs []uint64,
) ([]downstreamDependency, error) {
	if len(detailIDs) == 0 {
		return nil, nil
	}

	db := s.StockTransferRepo.DB().WithContext(ctx)
	if tx != nil {
		db = tx.WithContext(ctx)
	}

	var rows []downstreamDependency
	err := db.Table("stock_allocations").
		Select("usable_type, usable_id, COALESCE(function_code,'') AS function_code, COALESCE(flag_group_code,'') AS flag_group_code").
		Where("stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
		Where("stockable_id IN ?", detailIDs).
		Where("status = ?", entity.StockAllocationStatusActive).
		Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("deleted_at IS NULL").
		Group("usable_type, usable_id, function_code, flag_group_code").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}

	return rows, nil
}

func formatDownstreamDependencySummary(rows []downstreamDependency) string {
	if len(rows) == 0 {
		return "-"
	}

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

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

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

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

func (s *transferService) hasAyamDownstreamConsumption(ctx context.Context, tx *gorm.DB, detailIDs []uint64) (bool, error) {
	if len(detailIDs) == 0 {
		return false, nil
	}

	db := s.StockTransferRepo.DB().WithContext(ctx)
	if tx != nil {
		db = tx.WithContext(ctx)
	}

	var found int64
	err := db.Table("stock_allocations sa").
		Joins("JOIN stock_transfer_details std ON std.id = sa.stockable_id AND std.deleted_at IS NULL").
		Joins("JOIN flags f ON f.flagable_type = ? AND f.flagable_id = std.product_id", entity.FlagableTypeProduct).
		Joins("JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.flag_group_code = ? AND fm.is_active = TRUE", "AYAM").
		Where("sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
		Where("sa.stockable_id IN ?", detailIDs).
		Where("sa.status = ?", entity.StockAllocationStatusActive).
		Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("sa.deleted_at IS NULL").
		Count(&found).Error
	if err != nil {
		return false, err
	}

	return found > 0, nil
}

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

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

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

func (s *transferService) appendStockLog(
	ctx context.Context,
	stockLogRepo rStockLogs.StockLogRepository,
	productWarehouseID uint,
	actorID uint,
	increase float64,
	decrease float64,
	loggableID uint,
	notes string,
) error {
	if productWarehouseID == 0 || (increase <= 1e-6 && decrease <= 1e-6) {
		return nil
	}

	stockLog := &entity.StockLog{
		ProductWarehouseId: productWarehouseID,
		CreatedBy:          actorID,
		Increase:           increase,
		Decrease:           decrease,
		LoggableType:       string(utils.StockLogTypeTransfer),
		LoggableId:         loggableID,
		Notes:              notes,
	}

	stockLogs, err := stockLogRepo.GetByProductWarehouse(ctx, productWarehouseID, 1)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
	}
	if len(stockLogs) > 0 {
		latestStockLog := stockLogs[0]
		stockLog.Stock = latestStockLog.Stock + increase - decrease
	} else {
		stockLog.Stock = increase - decrease
	}
	if err := stockLogRepo.CreateOne(ctx, stockLog, nil); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat stock log saat delete transfer")
	}

	return nil
}
