package repository

import (
	"context"
	"fmt"
	"strings"
	"time"

	"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
	"gorm.io/gorm"
)

type ClosingRepository interface {
	repository.BaseRepository[entity.ProjectFlock]
	GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
	GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error)
	SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
	SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
	SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
	SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
	SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
	SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
	GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
	FetchSapronakIncoming(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error)
	FetchSapronakIncomingDetails(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
	FetchSapronakUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error)
	FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
	FetchSapronakChickinUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error)
	FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
	FetchSapronakUsageAllocatedDetails(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
	FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
	FetchSapronakTransfers(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
	FetchSapronakSales(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
	FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
	GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
}

type ClosingRepositoryImpl struct {
	*repository.BaseRepositoryImpl[entity.ProjectFlock]
}

func NewClosingRepository(db *gorm.DB) ClosingRepository {
	return &ClosingRepositoryImpl{
		BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
	}
}

type SapronakRow struct {
	Id                   uint64    `gorm:"column:id"`
	SortDate             time.Time `gorm:"column:sort_date"`
	DateText             string    `gorm:"column:date_text"`
	ReferenceNumber      string    `gorm:"column:reference_number"`
	TransactionType      string    `gorm:"column:transaction_type"`
	ProductName          string    `gorm:"column:product_name"`
	ProductCategory      string    `gorm:"column:product_category"`
	ProductSubCategory   string    `gorm:"column:product_sub_category"`
	SourceWarehouse      string    `gorm:"column:source_warehouse"`
	DestinationWarehouse string    `gorm:"column:destination_warehouse"`
	Destination          string    `gorm:"column:destination"`
	Quantity             float64   `gorm:"column:quantity"`
	UnitID               uint      `gorm:"column:unit_id"`
	Unit                 string    `gorm:"column:unit"`
	Notes                string    `gorm:"column:notes"`
}

type SapronakSummaryRow struct {
	Category string `gorm:"column:category"`
	TotalQty int64  `gorm:"column:total_qty"`
	UomID    uint   `gorm:"column:uom_id"`
	UomName  string `gorm:"column:uom_name"`
}

type ExpeditionHPPRow struct {
	SupplierName string  `gorm:"column:supplier_name"`
	TotalAmount  float64 `gorm:"column:total_amount"`
}

type SapronakQueryParams struct {
	Type                   string
	WarehouseIDs           []uint
	ProjectFlockKandangIDs []uint
	Limit                  int
	Offset                 int
	Search                 string
	StartDate              *time.Time
	EndDate                *time.Time
}

func sapronakIncomingPurchaseQueryParts(params SapronakQueryParams) (string, []any) {
	if len(params.ProjectFlockKandangIDs) > 0 {
		return sapronakIncomingPurchasesScopedSQL(), []any{
			fifo.UsableKeyRecordingStock.String(),
			fifo.UsableKeyProjectChickin.String(),
			fifo.StockableKeyPurchaseItems.String(),
			entity.StockAllocationStatusActive,
			entity.StockAllocationPurposeConsume,
			fifo.UsableKeyRecordingStock.String(),
			params.ProjectFlockKandangIDs,
			fifo.UsableKeyProjectChickin.String(),
			params.ProjectFlockKandangIDs,
			params.ProjectFlockKandangIDs,
			params.WarehouseIDs,
		}
	}

	return sapronakIncomingPurchasesSQL, []any{params.WarehouseIDs}
}

func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
	db := r.DB().WithContext(ctx)

	var (
		unionParts []string
		args       []any
	)

	switch params.Type {
	case validation.SapronakTypeIncoming:
		if len(params.WarehouseIDs) == 0 {
			return []SapronakRow{}, 0, nil
		}
		purchasesSQL, purchaseArgs := sapronakIncomingPurchaseQueryParts(params)
		unionParts = append(unionParts, purchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
		args = append(args, purchaseArgs...)
		args = append(args, params.WarehouseIDs, params.WarehouseIDs)
	case validation.SapronakTypeOutgoing:
		if len(params.WarehouseIDs) > 0 {
			unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL)
			args = append(args, params.WarehouseIDs, params.WarehouseIDs)
		}
		if len(params.ProjectFlockKandangIDs) > 0 {
			unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
			args = append(args, params.ProjectFlockKandangIDs)
		}
		if len(unionParts) == 0 {
			return []SapronakRow{}, 0, nil
		}
	default:
		return nil, 0, fmt.Errorf("invalid sapronak type: %s", params.Type)
	}

	unionSQL := strings.Join(unionParts, " UNION ALL ")

	search := strings.TrimSpace(params.Search)
	searchClause := ""
	var searchArgs []any
	if search != "" {
		searchClause = `
 WHERE (
		reference_number ILIKE ?
		OR product_name ILIKE ?
		OR product_category ILIKE ?
		OR source_warehouse ILIKE ?
		OR destination_warehouse ILIKE ?
		OR CAST(quantity AS TEXT) ILIKE ?
		OR unit ILIKE ?
		OR notes ILIKE ?
		OR transaction_type ILIKE ?
	)`
		like := "%" + search + "%"
		searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
	}

	var totalResults int64
	dateClause := ""
	var dateArgs []any
	if params.StartDate != nil {
		dateClause += " AND sort_date::date >= ?"
		dateArgs = append(dateArgs, params.StartDate)
	}
	if params.EndDate != nil {
		dateClause += " AND sort_date::date <= ?"
		dateArgs = append(dateArgs, params.EndDate)
	}
	whereClause := searchClause
	if dateClause != "" {
		if whereClause == "" {
			whereClause = " WHERE " + strings.TrimPrefix(dateClause, " AND ")
		} else {
			whereClause += dateClause
		}
	}
	countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, whereClause)
	countArgs := append(append(append([]any{}, args...), searchArgs...), dateArgs...)
	if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
		return nil, 0, err
	}

	dataArgs := append(append(append([]any{}, args...), searchArgs...), dateArgs...)
	dataArgs = append(dataArgs, params.Limit, params.Offset)
	dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, whereClause)

	var rows []SapronakRow
	if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
		return nil, 0, err
	}

	return rows, totalResults, nil
}

func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) {
	db := r.DB().WithContext(ctx)

	var (
		unionParts []string
		args       []any
	)

	switch params.Type {
	case validation.SapronakTypeIncoming:
		if len(params.WarehouseIDs) == 0 {
			return []SapronakSummaryRow{}, nil
		}
		purchasesSQL, purchaseArgs := sapronakIncomingPurchaseQueryParts(params)
		unionParts = append(unionParts, purchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
		args = append(args, purchaseArgs...)
		args = append(args, params.WarehouseIDs, params.WarehouseIDs)
	case validation.SapronakTypeOutgoing:
		if len(params.WarehouseIDs) > 0 {
			unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL)
			args = append(args, params.WarehouseIDs, params.WarehouseIDs)
		}
		if len(params.ProjectFlockKandangIDs) > 0 {
			unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
			args = append(args, params.ProjectFlockKandangIDs)
		}
		if len(unionParts) == 0 {
			return []SapronakSummaryRow{}, nil
		}
	default:
		return nil, fmt.Errorf("invalid sapronak type: %s", params.Type)
	}

	unionSQL := strings.Join(unionParts, " UNION ALL ")

	search := strings.TrimSpace(params.Search)
	searchClause := ""
	var searchArgs []any
	if search != "" {
		searchClause = `
 WHERE (
		reference_number ILIKE ?
		OR product_name ILIKE ?
		OR product_category ILIKE ?
		OR source_warehouse ILIKE ?
		OR destination_warehouse ILIKE ?
		OR CAST(quantity AS TEXT) ILIKE ?
		OR unit ILIKE ?
		OR notes ILIKE ?
		OR transaction_type ILIKE ?
	)`
		like := "%" + search + "%"
		searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
	}

	dateClause := ""
	var dateArgs []any
	if params.StartDate != nil {
		dateClause += " AND sort_date::date >= ?"
		dateArgs = append(dateArgs, params.StartDate)
	}
	if params.EndDate != nil {
		dateClause += " AND sort_date::date <= ?"
		dateArgs = append(dateArgs, params.EndDate)
	}
	whereClause := searchClause
	if dateClause != "" {
		if whereClause == "" {
			whereClause = " WHERE " + strings.TrimPrefix(dateClause, " AND ")
		} else {
			whereClause += dateClause
		}
	}

	querySQL := fmt.Sprintf(`
SELECT
	product_category AS category,
	CAST(COALESCE(SUM(quantity), 0) AS BIGINT) AS total_qty,
	unit_id AS uom_id,
	unit AS uom_name
FROM (%s) AS combined%s
GROUP BY product_category, unit_id, unit
ORDER BY product_category ASC, unit ASC
`, unionSQL, whereClause)
	queryArgs := append(append(append([]any{}, args...), searchArgs...), dateArgs...)

	var rows []SapronakSummaryRow
	if err := db.Raw(querySQL, queryArgs...).Scan(&rows).Error; err != nil {
		return nil, err
	}

	return rows, nil
}

func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
	if len(projectFlockKandangIDs) == 0 {
		return 0, 0, nil
	}

	var purchaseAgg struct {
		TotalIn float64 `gorm:"column:total_in"`
	}

	err := r.DB().WithContext(ctx).
		Table("purchase_items pi").
		Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
		Where("f.name = ?", "PAKAN").
		Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
		Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
		Scan(&purchaseAgg).Error
	if err != nil {
		return 0, 0, err
	}

	var usageAgg struct {
		TotalUsed float64 `gorm:"column:total_used"`
	}

	err = r.DB().WithContext(ctx).
		Table("recording_stocks rs").
		Joins("JOIN recordings rec ON rec.id = rs.recording_id").
		Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
		Joins("JOIN products prod ON prod.id = pw.product_id").
		Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
		Where("rs.project_flock_kandang_id IN ?", projectFlockKandangIDs).
		Where("f.name = ?", "PAKAN").
		Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
		Scan(&usageAgg).Error
	if err != nil {
		return 0, 0, err
	}

	return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
}

func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
	if len(projectFlockKandangIDs) == 0 {
		return 0, nil
	}

	var total float64
	if err := r.DB().WithContext(ctx).
		Model(&entity.ProjectChickin{}).
		Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
		Select("COALESCE(SUM(usage_qty), 0)").
		Scan(&total).Error; err != nil {
		return 0, err
	}

	return total, nil
}

func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
	if len(projectFlockKandangIDs) == 0 {
		return 0, nil
	}

	var agg struct {
		Total float64 `gorm:"column:total_culling"`
	}

	err := r.DB().WithContext(ctx).
		Table("recording_depletions rd").
		Joins("JOIN recordings rec ON rec.id = rd.recording_id").
		Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
		Joins("JOIN products prod ON prod.id = pw.product_id").
		Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
		Where("COALESCE(rd.source_project_flock_kandang_id, rec.project_flock_kandangs_id) IN ?", projectFlockKandangIDs).
		Where("f.name = ?", utils.FlagAyamCulling).
		Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
		Scan(&agg).Error
	if err != nil {
		return 0, err
	}

	return agg.Total, nil
}

func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
	if len(projectFlockKandangIDs) == 0 {
		return 0, 0, 0, nil
	}
	return r.sumMarketingAttributedByProjectFlockKandangIDs(ctx, projectFlockKandangIDs, nil)
}

func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
	if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
		return 0, 0, 0, nil
	}
	return r.sumMarketingAttributedByProjectFlockKandangIDs(ctx, projectFlockKandangIDs, flagNames)
}

func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
	if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
		return 0, nil
	}

	var agg struct {
		TotalQty float64 `gorm:"column:total_qty"`
	}

	err := r.DB().WithContext(ctx).
		Table("recording_eggs re").
		Joins("JOIN recordings rec ON rec.id = re.recording_id").
		Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
		Joins("JOIN products prod ON prod.id = pw.product_id").
		Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
		Where("COALESCE(re.project_flock_kandang_id, rec.project_flock_kandangs_id) IN ?", projectFlockKandangIDs).
		Where("f.name IN ?", flagNames).
		Select("COALESCE(SUM(re.qty), 0) AS total_qty").
		Scan(&agg).Error
	if err != nil {
		return 0, err
	}

	return agg.TotalQty, nil
}

func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
	db := r.DB().WithContext(ctx)

	if projectFlockID == 0 {
		return nil, fmt.Errorf("invalid project flock id")
	}

	query := db.
		Table("expense_realizations AS er").
		Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id").
		Joins("JOIN expenses e ON e.id = ens.expense_id").
		Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id").
		Joins("JOIN nonstocks n ON n.id = ens.nonstock_id").
		Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
		Joins("JOIN suppliers s ON s.id = e.supplier_id").
		Where("pfk.project_flock_id = ?", projectFlockID).
		Where("e.category = ?", "BOP").
		Where("e.realization_date IS NOT NULL").
		Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))

	if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
		query = query.Where("pfk.id = ?", *projectFlockKandangID)
	}

	var rows []ExpeditionHPPRow
	err := query.
		Select(
			"e.supplier_id AS supplier_id, " +
				"s.name AS supplier_name, " +
				"SUM(er.qty * er.price) AS total_amount",
		).
		Group("e.supplier_id, s.name").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}

	return rows, nil
}

const (
	sapronakIncomingPurchasesSQL = `
SELECT
	CAST(pi.id AS BIGINT) AS id,
	COALESCE(pi.received_date, '1970-01-01') AS sort_date,
	COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
	COALESCE(p.po_number, '') AS reference_number,
	'Pembelian' AS transaction_type,
	prod.name AS product_name,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_category,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_sub_category,
	'-' AS source_warehouse,
	w.name AS destination_warehouse,
	'' AS destination,
	pi.total_qty AS quantity,
	u.id AS unit_id,
	u.name AS unit,
	COALESCE(p.notes, '') AS notes
FROM purchase_items pi
JOIN purchases p ON p.id = pi.purchase_id
JOIN products prod ON prod.id = pi.product_id
JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pi.warehouse_id
WHERE pi.warehouse_id IN ?
`

	sapronakIncomingTransfersSQL = `
SELECT
	CAST(st.id AS BIGINT) AS id,
	st.transfer_date AS sort_date,
	TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
	st.movement_number AS reference_number,
	'Mutasi' AS transaction_type,
	prod.name AS product_name,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_category,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_sub_category,
	COALESCE(fw.name, '') AS source_warehouse,
	COALESCE(tw.name, '') AS destination_warehouse,
	'' AS destination,
	std.usage_qty AS quantity,
	u.id AS unit_id,
	u.name AS unit,
	st.reason AS notes
FROM stock_transfer_details std
JOIN stock_transfers st ON st.id = std.stock_transfer_id
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
JOIN products prod ON prod.id = std.product_id
JOIN uoms u ON u.id = prod.uom_id
WHERE st.to_warehouse_id IN ?
`

	sapronakIncomingAdjustmentsSQL = `
SELECT
	CAST(ast.id AS BIGINT) AS id,
	ast.created_at AS sort_date,
	COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text,
	COALESCE(ast.adj_number, '') AS reference_number,
	'Adjustment stock' AS transaction_type,
	prod.name AS product_name,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_category,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_sub_category,
	'-' AS source_warehouse,
	COALESCE(w.name, '')  AS destination_warehouse,
	'' AS destination,
	COALESCE(ast.total_qty, 0) AS quantity,
	u.id AS unit_id,
	u.name AS unit,
	'-' AS notes
FROM adjustment_stocks ast
JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id
JOIN warehouses w ON w.id = pw.warehouse_id
JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id
WHERE pw.warehouse_id IN ?
	AND COALESCE(ast.total_qty, 0) <> 0
`

	sapronakOutgoingTransfersSQL = `
SELECT
	CAST(st.id AS BIGINT) AS id,
	st.transfer_date AS sort_date,
	TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
	st.movement_number AS reference_number,
	'Mutasi' AS transaction_type,
	prod.name AS product_name,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_category,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_sub_category,
	COALESCE(fw.name, '') AS source_warehouse,
	COALESCE(tw.name, '') AS destination_warehouse,
	'' AS destination,
	std.usage_qty AS quantity,
	u.id AS unit_id,
	u.name AS unit,
	st.reason AS notes
FROM stock_transfer_details std
JOIN stock_transfers st ON st.id = std.stock_transfer_id
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
JOIN products prod ON prod.id = std.product_id
JOIN uoms u ON u.id = prod.uom_id
WHERE st.from_warehouse_id IN ?
`

	sapronakOutgoingAdjustmentsSQL = `
SELECT
	CAST(ast.id AS BIGINT) AS id,
	ast.created_at AS sort_date,
	COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text,
	COALESCE(ast.adj_number, '') AS reference_number,
	'Adjustment stock' AS transaction_type,
	prod.name AS product_name,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_category,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_sub_category,
	COALESCE(w.name, '') AS source_warehouse,
	'-' AS destination_warehouse,
	'' AS destination,
	COALESCE(ast.usage_qty, 0) AS quantity,
	u.id AS unit_id,
	u.name AS unit,
	'-' AS notes
FROM adjustment_stocks ast
JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id
JOIN warehouses w ON w.id = pw.warehouse_id
JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id
WHERE pw.warehouse_id IN ?
	AND COALESCE(ast.usage_qty, 0) <> 0
	AND EXISTS (
		SELECT 1
		FROM flags f
		WHERE f.flagable_id = pw.product_id
			AND f.flagable_type = 'products'
			AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK')
	)
`

	sapronakOutgoingMarketingsSQL = `
SELECT
	CAST(mp.id AS BIGINT) AS id,
	m.so_date AS sort_date,
	TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text,
	m.so_number AS reference_number,
	'Penjualan' AS transaction_type,
	prod.name AS product_name,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_category,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_sub_category,
	w.name AS source_warehouse,
	COALESCE(c.name, '') AS destination_warehouse,
	'' AS destination,
	mp.qty AS quantity,
	u.id AS unit_id,
	u.name AS unit,
	m.notes AS notes
FROM marketing_products mp
JOIN marketings m ON m.id = mp.marketing_id
LEFT JOIN customers c ON c.id = m.customer_id
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pw.warehouse_id
WHERE pw.project_flock_kandang_id IN ?
	AND EXISTS (
		SELECT 1
		FROM flags f
		WHERE f.flagable_id = pw.product_id
			AND f.flagable_type = 'products'
			AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK')
	)
`
)

type SapronakIncomingRow struct {
	ProductID    uint
	ProductName  string
	Flag         string
	Qty          float64
	Value        float64
	DefaultPrice float64
}

type SapronakUsageRow struct {
	ProductID    uint
	ProductName  string
	Flag         string
	Qty          float64
	DefaultPrice float64
}

type SapronakDetailRow struct {
	ProductID   uint
	ProductName string
	Flag        string
	Date        *time.Time
	Reference   string
	QtyIn       float64
	QtyOut      float64
	Price       float64
}

func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }

func (r *ClosingRepositoryImpl) sumMarketingAttributedByProjectFlockKandangIDs(
	ctx context.Context,
	projectFlockKandangIDs []uint,
	flagNames []string,
) (float64, float64, float64, error) {
	var agg struct {
		TotalWeight float64 `gorm:"column:total_weight"`
		TotalQty    float64 `gorm:"column:total_qty"`
		TotalPrice  float64 `gorm:"column:total_price"`
	}

	query := r.withCtx(ctx).
		Table("(?) AS mda", repository.MarketingDeliveryAttributionRowsQuery(r.withCtx(ctx))).
		Joins("JOIN marketing_delivery_products mdp ON mdp.id = mda.marketing_delivery_product_id").
		Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
		Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
		Joins("JOIN products prod ON prod.id = pw.product_id").
		Where("mda.project_flock_kandang_id IN ?", projectFlockKandangIDs).
		Where("mdp.delivery_date IS NOT NULL")

	if len(flagNames) > 0 {
		query = query.
			Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
			Where("f.name IN ?", flagNames)
	}

	err := query.
		Select(`
			COALESCE(SUM(CASE
				WHEN COALESCE(mdp.usage_qty, 0) > 0 THEN mdp.total_weight * (mda.allocated_qty / mdp.usage_qty)
				ELSE 0
			END), 0) AS total_weight,
			COALESCE(SUM(mda.allocated_qty), 0) AS total_qty,
			COALESCE(SUM(CASE
				WHEN COALESCE(mdp.usage_qty, 0) > 0 THEN mdp.total_price * (mda.allocated_qty / mdp.usage_qty)
				ELSE 0
			END), 0) AS total_price
		`).
		Scan(&agg).Error
	if err != nil {
		return 0, 0, 0, err
	}

	return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}

func applyDateRange(db *gorm.DB, column string, start, end *time.Time) *gorm.DB {
	if start != nil {
		db = db.Where(column+"::date >= ?", start)
	}
	if end != nil {
		db = db.Where(column+"::date <= ?", end)
	}
	return db
}

func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
	for _, j := range joins {
		if strings.TrimSpace(j) != "" {
			db = db.Joins(j)
		}
	}
	return db
}

func sapronakFlags(flags ...utils.FlagType) []string {
	out := make([]string, len(flags))
	for i, f := range flags {
		out[i] = string(f)
	}
	return out
}

func sapronakLegacyFlagByProductCategoryCase(categoryCodeExpr string) string {
	return fmt.Sprintf(
		`CASE
			WHEN UPPER(%s) = 'DOC' THEN '%s'
			WHEN UPPER(%s) = 'PLT' THEN '%s'
			WHEN UPPER(%s) IN ('RAW', 'PST', 'STR', 'FSR') THEN '%s'
			WHEN UPPER(%s) IN ('OBT', 'VTM', 'KMA') THEN '%s'
			ELSE NULL
		END`,
		categoryCodeExpr, utils.FlagDOC,
		categoryCodeExpr, utils.FlagPullet,
		categoryCodeExpr, utils.FlagPakan,
		categoryCodeExpr, utils.FlagOVK,
	)
}

func sapronakIncomingPurchasesScopedSQL() string {
	return `
WITH scoped_farm_allocations AS (
	SELECT
		sa.stockable_id AS purchase_item_id,
		COALESCE(SUM(sa.qty), 0) AS allocated_qty
	FROM stock_allocations sa
	LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?
	LEFT JOIN recordings rec ON rec.id = rs.recording_id AND rec.deleted_at IS NULL
	LEFT JOIN project_chickins pc ON pc.id = sa.usable_id AND sa.usable_type = ?
	WHERE sa.stockable_type = ?
		AND sa.status = ?
		AND sa.allocation_purpose = ?
		AND (
			(sa.usable_type = ? AND rs.project_flock_kandang_id IN ?)
			OR
			(sa.usable_type = ? AND pc.project_flock_kandang_id IN ?)
		)
	GROUP BY sa.stockable_id
)
SELECT
	CAST(pi.id AS BIGINT) AS id,
	COALESCE(pi.received_date, '1970-01-01') AS sort_date,
	COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
	COALESCE(p.po_number, '') AS reference_number,
	'Pembelian' AS transaction_type,
	prod.name AS product_name,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_category,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_sub_category,
	'-' AS source_warehouse,
	w.name AS destination_warehouse,
	'' AS destination,
	pi.total_qty AS quantity,
	u.id AS unit_id,
	u.name AS unit,
	COALESCE(p.notes, '') AS notes
FROM purchase_items pi
JOIN purchases p ON p.id = pi.purchase_id
JOIN products prod ON prod.id = pi.product_id
JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pi.warehouse_id
WHERE w.kandang_id IS NOT NULL
	AND (
		pi.project_flock_kandang_id IN ?
		OR (pi.project_flock_kandang_id IS NULL AND pi.warehouse_id IN ?)
	)
UNION ALL
SELECT
	CAST(pi.id AS BIGINT) AS id,
	COALESCE(pi.received_date, '1970-01-01') AS sort_date,
	COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
	COALESCE(p.po_number, '') AS reference_number,
	'Pembelian' AS transaction_type,
	prod.name AS product_name,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_category,
	COALESCE((
		SELECT string_agg(
			f.name,
			' ' ORDER BY
				CASE
					WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
					ELSE 1
				END,
				f.name
		)
		FROM flags f
		WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
	), '') AS product_sub_category,
	'-' AS source_warehouse,
	w.name AS destination_warehouse,
	'' AS destination,
	sfa.allocated_qty AS quantity,
	u.id AS unit_id,
	u.name AS unit,
	COALESCE(p.notes, '') AS notes
FROM purchase_items pi
JOIN purchases p ON p.id = pi.purchase_id
JOIN products prod ON prod.id = pi.product_id
JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pi.warehouse_id
JOIN scoped_farm_allocations sfa ON sfa.purchase_item_id = pi.id
WHERE w.kandang_id IS NULL
	AND COALESCE(sfa.allocated_qty, 0) > 0
`
}

var (
	sapronakFlagsAll     = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
	sapronakFlagsUsage   = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
	sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
)

func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB {
	actualFlags := r.DB().
		Table("flags").
		Select(`
			flagable_id,
			MIN(CASE
				WHEN UPPER(name) = 'DOC' THEN 1
				WHEN UPPER(name) = 'PULLET' THEN 2
				WHEN UPPER(name) = 'PAKAN' THEN 3
				WHEN UPPER(name) = 'OVK' THEN 4
				ELSE 5
			END) AS priority
		`).
		Where("flagable_type = ?", entity.FlagableTypeProduct).
		Where("UPPER(name) IN ?", sapronakFlagsAll).
		Group("flagable_id")

	legacyFlagExpr := sapronakLegacyFlagByProductCategoryCase("pc.code")
	subquery := r.DB().
		Table("products AS sapronak_products").
		Select(fmt.Sprintf(`
			sapronak_products.id AS flagable_id,
			CASE
				WHEN actual_flags.priority = 1 THEN '%s'
				WHEN actual_flags.priority = 2 THEN '%s'
				WHEN actual_flags.priority = 3 THEN '%s'
				WHEN actual_flags.priority = 4 THEN '%s'
				ELSE %s
			END AS name
		`,
			utils.FlagDOC,
			utils.FlagPullet,
			utils.FlagPakan,
			utils.FlagOVK,
			legacyFlagExpr,
		)).
		Joins("LEFT JOIN (?) AS actual_flags ON actual_flags.flagable_id = sapronak_products.id", actualFlags).
		Joins("LEFT JOIN product_categories pc ON pc.id = sapronak_products.product_category_id").
		Where("actual_flags.priority IS NOT NULL OR " + legacyFlagExpr + " IS NOT NULL")

	return db.Joins("JOIN (?) f ON f.flagable_id = "+productAlias+".id", subquery)
}

func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
	m := make(map[uint][]SapronakDetailRow)
	for _, row := range rows {
		m[row.ProductID] = append(m[row.ProductID], row)
	}
	return m
}

func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) {
	rows := make([]SapronakDetailRow, 0)
	if err := db.Scan(&rows).Error; err != nil {
		return nil, err
	}
	return groupSapronakDetails(rows), nil
}

// =========================
// Usage (summary + details)
// =========================

func (r *ClosingRepositoryImpl) usageQuery(
	ctx context.Context,
	table string,
	pwJoinCond string,
	joins []string,
	where string,
	args ...any,
) *gorm.DB {
	db := r.withCtx(ctx).Table(table).Select(`
		pw.product_id AS product_id,
		p.name AS product_name,
		f.name AS flag,
		COALESCE(SUM(usage_qty), 0) AS qty,
		COALESCE(p.product_price, 0) AS default_price
	`)
	db = applyJoins(db, joins...)
	db = db.
		Joins("JOIN product_warehouses pw ON "+pwJoinCond).
		Joins("JOIN products p ON p.id = pw.product_id").
		Where(where, args...)
	db = r.joinSapronakProductFlag(db, "p")
	return db
}

func (r *ClosingRepositoryImpl) fetchSapronakUsage(
	ctx context.Context,
	table string,
	pwJoinCond string,
	joins []string,
	where string,
	args ...any,
) ([]SapronakUsageRow, error) {
	rows := make([]SapronakUsageRow, 0)
	db := r.usageQuery(ctx, table, pwJoinCond, joins, where, args...)
	if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
		return nil, err
	}
	return rows, nil
}

func scanUsage(db *gorm.DB) ([]SapronakUsageRow, error) {
	rows := make([]SapronakUsageRow, 0)
	if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
		return nil, err
	}
	return rows, nil
}

func (r *ClosingRepositoryImpl) detailQuery(
	ctx context.Context,
	table string,
	pwJoinCond string,
	joins []string,
	selectSQL string,
	where string,
	args ...any,
) *gorm.DB {
	db := r.withCtx(ctx).
		Table(table).
		Joins("JOIN product_warehouses pw ON " + pwJoinCond).
		Joins("JOIN products p ON p.id = pw.product_id")

	db = applyJoins(db, joins...)
	db = r.joinSapronakProductFlag(db, "p")
	return db.Select(selectSQL).Where(where, args...)
}

func (r *ClosingRepositoryImpl) fetchSapronakDetails(
	ctx context.Context,
	table string,
	pwJoinCond string,
	joins []string,
	selectSQL string,
	where string,
	args ...any,
) (map[uint][]SapronakDetailRow, error) {
	return scanAndGroupDetails(r.detailQuery(ctx, table, pwJoinCond, joins, selectSQL, where, args...))
}

func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error) {
	if pfkID == 0 {
		return nil, nil
	}
	db := r.usageQuery(
		ctx,
		"recording_stocks rs",
		"pw.id = rs.product_warehouse_id",
		[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
		"rs.project_flock_kandang_id = ? AND f.name IN ?",
		pfkID,
		sapronakFlagsUsage,
	)
	db = applyDateRange(db, "r.record_datetime", start, end)
	return scanUsage(db)
}

func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error) {
	if pfkID == 0 {
		return []SapronakUsageRow{}, nil
	}
	db := r.usageQuery(
		ctx,
		"project_chickins pc",
		"pw.id = pc.product_warehouse_id",
		nil,
		"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
		pfkID,
		sapronakFlagsChickin,
	)
	db = applyDateRange(db, "pc.chick_in_date", start, end)
	return scanUsage(db)
}

func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
	db := r.detailQuery(
		ctx,
		"recording_stocks rs",
		"pw.id = rs.product_warehouse_id",
		[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"}, // penting: supaya alias r valid
		`
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			r.record_datetime AS date,
			CAST(r.id AS TEXT) AS reference,
			0 AS qty_in,
			COALESCE(rs.usage_qty,0) AS qty_out,
			COALESCE(p.product_price,0) AS price
		`,
		"rs.project_flock_kandang_id = ? AND f.name IN ?",
		pfkID,
		sapronakFlagsUsage,
	)
	db = applyDateRange(db, "r.record_datetime", start, end)
	return scanAndGroupDetails(db)
}

func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
	db := r.detailQuery(
		ctx,
		"project_chickins pc",
		"pw.id = pc.product_warehouse_id",
		nil,
		`
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			pc.chick_in_date AS date,
			CAST(pc.id AS TEXT) AS reference,
			0 AS qty_in,
			COALESCE(pc.usage_qty,0) AS qty_out,
			COALESCE(p.product_price,0) AS price
		`,
		"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
		pfkID,
		sapronakFlagsChickin,
	)
	db = applyDateRange(db, "pc.chick_in_date", start, end)
	return scanAndGroupDetails(db)
}

func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
	if projectFlockKandangID == 0 {
		return map[uint][]SapronakDetailRow{}, nil
	}

	dateExpr := "COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, pc.chick_in_date, r.record_datetime)"
	query := r.withCtx(ctx).
		Table("stock_allocations AS sa").
		Select(`
			p_resolve.id AS product_id,
			p_resolve.name AS product_name,
			f.name AS flag,
			COALESCE(
				pi.received_date,
				st.transfer_date,
				lt.transfer_date,
				ast.created_at,
				pc.chick_in_date,
				r.record_datetime
			) AS date,
			COALESCE(
				po.po_number,
				st.movement_number,
				lt.transfer_number,
				CONCAT('ADJ-', ast.id),
				CONCAT('CHICKIN-', pc.id),
				CAST(r.id AS TEXT),
				''
			) AS reference,
			0 AS qty_in,
			COALESCE(SUM(sa.qty), 0) AS qty_out,
			COALESCE(pi.price, p_resolve.product_price, 0) AS price
		`).
		Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
		Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()).
		Joins("LEFT JOIN recordings r ON r.id = rs.recording_id").
		Joins("LEFT JOIN project_chickins pc_used ON pc_used.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()).
		Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
		Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
		Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
		Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
		Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
		Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
		Joins("LEFT JOIN product_warehouses pw_ltt ON pw_ltt.id = ltt.product_warehouse_id").
		Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
		Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
		Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
		Joins("LEFT JOIN product_warehouses pw_pc ON pw_pc.id = pc.product_warehouse_id").
		Joins("LEFT JOIN products p_resolve ON p_resolve.id = COALESCE(pi.product_id, pw_ltt.product_id, pw_pc.product_id, pw.product_id)").
		Where("sa.status = ?", entity.StockAllocationStatusActive).
		Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
		Where("f.name IN ?", sapronakFlagsAll).
		Where(`
			(sa.usable_type = ? AND rs.project_flock_kandang_id = ? AND f.name IN ?)
			OR
			(sa.usable_type = ? AND pc_used.project_flock_kandang_id = ? AND f.name IN ?)
		`,
			fifo.UsableKeyRecordingStock.String(), projectFlockKandangID, sapronakFlagsUsage,
			fifo.UsableKeyProjectChickin.String(), projectFlockKandangID, sapronakFlagsChickin,
		)
	query = r.joinSapronakProductFlag(query, "p_resolve").
		Group(`
			p_resolve.id, p_resolve.name, f.name,
			pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, pc.chick_in_date, r.record_datetime,
			po.po_number, st.movement_number, lt.transfer_number, ast.id, pc.id, r.id,
			pi.price, p_resolve.product_price
		`)
	query = applyDateRange(query, dateExpr, start, end)

	return scanAndGroupDetails(query)
}

func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) *gorm.DB {
	db := r.withCtx(ctx).
		Table("purchase_items AS pi").
		Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
		Joins("JOIN products p ON p.id = pi.product_id").
		Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
		Where("f.name IN ?", sapronakFlagsAll).
		Where("pi.received_date IS NOT NULL")
	if projectFlockKandangID > 0 {
		db = db.Where(
			"w.kandang_id = ? AND (pi.project_flock_kandang_id = ? OR pi.project_flock_kandang_id IS NULL)",
			kandangID,
			projectFlockKandangID,
		)
	} else {
		db = db.Where("w.kandang_id = ?", kandangID)
	}
	db = applyDateRange(db, "pi.received_date", start, end)
	return r.joinSapronakProductFlag(db, "p")
}

func (r *ClosingRepositoryImpl) incomingFarmPurchaseAllocationBase(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) *gorm.DB {
	db := r.withCtx(ctx).
		Table("stock_allocations AS sa").
		Joins("JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
		Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
		Joins("JOIN products p ON p.id = pi.product_id").
		Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
		Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()).
		Joins("LEFT JOIN recordings rec ON rec.id = rs.recording_id AND rec.deleted_at IS NULL").
		Joins("LEFT JOIN project_chickins pc ON pc.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()).
		Where("sa.status = ?", entity.StockAllocationStatusActive).
		Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("w.kandang_id IS NULL").
		Where("(sa.usable_type = ? AND rs.project_flock_kandang_id = ?) OR (sa.usable_type = ? AND pc.project_flock_kandang_id = ?)",
			fifo.UsableKeyRecordingStock.String(),
			projectFlockKandangID,
			fifo.UsableKeyProjectChickin.String(),
			projectFlockKandangID,
		).
		Where("f.name IN ?", sapronakFlagsAll).
		Where("pi.received_date IS NOT NULL")
	db = applyDateRange(db, "pi.received_date", start, end)
	return r.joinSapronakProductFlag(db, "p")
}

func mergeSapronakIncomingRows(primary []SapronakIncomingRow, extra []SapronakIncomingRow) []SapronakIncomingRow {
	if len(extra) == 0 {
		return primary
	}

	type key struct {
		productID uint
		flag      string
	}

	merged := make(map[key]*SapronakIncomingRow, len(primary)+len(extra))
	order := make([]key, 0, len(primary)+len(extra))

	add := func(rows []SapronakIncomingRow) {
		for _, row := range rows {
			k := key{productID: row.ProductID, flag: row.Flag}
			if existing, ok := merged[k]; ok {
				existing.Qty += row.Qty
				existing.Value += row.Value
				if existing.ProductName == "" {
					existing.ProductName = row.ProductName
				}
				if existing.DefaultPrice == 0 {
					existing.DefaultPrice = row.DefaultPrice
				}
				continue
			}

			copyRow := row
			merged[k] = &copyRow
			order = append(order, k)
		}
	}

	add(primary)
	add(extra)

	result := make([]SapronakIncomingRow, 0, len(order))
	for _, k := range order {
		result = append(result, *merged[k])
	}
	return result
}

func mergeSapronakDetailMaps(primary map[uint][]SapronakDetailRow, extra map[uint][]SapronakDetailRow) map[uint][]SapronakDetailRow {
	if len(primary) == 0 && len(extra) == 0 {
		return map[uint][]SapronakDetailRow{}
	}
	if len(extra) == 0 {
		return primary
	}
	if len(primary) == 0 {
		return extra
	}

	for productID, rows := range extra {
		primary[productID] = append(primary[productID], rows...)
	}
	return primary
}

func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error) {
	rows := make([]SapronakIncomingRow, 0)
	db := r.incomingPurchaseBase(ctx, projectFlockKandangID, kandangID, start, end).Select(`
		pi.product_id AS product_id,
		p.name AS product_name,
		f.name AS flag,
		COALESCE(SUM(pi.total_qty), 0) AS qty,
		COALESCE(SUM(pi.total_qty * pi.price), 0) AS value,
		COALESCE(p.product_price, 0) AS default_price
	`)
	if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
		return nil, err
	}

	if projectFlockKandangID == 0 {
		return rows, nil
	}

	farmRows := make([]SapronakIncomingRow, 0)
	farmDB := r.incomingFarmPurchaseAllocationBase(ctx, projectFlockKandangID, start, end).Select(`
		pi.product_id AS product_id,
		p.name AS product_name,
		f.name AS flag,
		COALESCE(SUM(sa.qty), 0) AS qty,
		COALESCE(SUM(sa.qty * pi.price), 0) AS value,
		COALESCE(p.product_price, 0) AS default_price
	`)
	if err := farmDB.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&farmRows).Error; err != nil {
		return nil, err
	}

	return mergeSapronakIncomingRows(rows, farmRows), nil
}

func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
	rows, err := scanAndGroupDetails(
		r.incomingPurchaseBase(ctx, projectFlockKandangID, kandangID, start, end).Select(`
			pi.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			pi.received_date AS date,
			COALESCE(po.po_number, '') AS reference,
			COALESCE(pi.total_qty,0) AS qty_in,
			0 AS qty_out,
				COALESCE(pi.price,0) AS price
			`),
	)
	if err != nil {
		return nil, err
	}

	if projectFlockKandangID == 0 {
		return rows, nil
	}

	farmRows, err := scanAndGroupDetails(
		r.incomingFarmPurchaseAllocationBase(ctx, projectFlockKandangID, start, end).Select(`
			pi.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			pi.received_date AS date,
			COALESCE(po.po_number, '') AS reference,
			COALESCE(SUM(sa.qty),0) AS qty_in,
			0 AS qty_out,
			COALESCE(pi.price,0) AS price
		`).Group(`
			pi.id, pi.product_id, p.name, f.name,
			pi.received_date, po.po_number, pi.price
		`),
	)
	if err != nil {
		return nil, err
	}

	return mergeSapronakDetailMaps(rows, farmRows), nil
}

type stockLogSapronakRow struct {
	ID             uint       `gorm:"column:id"`
	ProductID      uint       `gorm:"column:product_id"`
	ProductName    string     `gorm:"column:product_name"`
	Flag           string     `gorm:"column:flag"`
	CreatedAt      *time.Time `gorm:"column:created_at"`
	Increase       float64    `gorm:"column:increase"`
	Decrease       float64    `gorm:"column:decrease"`
	Price          float64    `gorm:"column:price"`
	MovementNumber string     `gorm:"column:movement_number"`
}

func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID uint, logType any, withMovement bool) ([]stockLogSapronakRow, error) {
	rows := make([]stockLogSapronakRow, 0)

	movementSelect := "'' AS movement_number"
	joins := []string{}
	if withMovement {
		movementSelect = "COALESCE(st.movement_number,'') AS movement_number"
		joins = append(joins, "JOIN stock_transfers st ON st.id = sl.loggable_id")
	}

	db := r.withCtx(ctx).
		Table("stock_logs sl").
		Select(`
			sl.id AS id,
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			sl.created_at AS created_at,
			COALESCE(sl.increase,0) AS increase,
			COALESCE(sl.decrease,0) AS decrease,
			COALESCE(p.product_price,0) AS price,
			` + movementSelect + `
		`).
		Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
		Joins("JOIN products p ON p.id = pw.product_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id")

	db = applyJoins(db, joins...)
	db = r.joinSapronakProductFlag(db, "p")

	if err := db.
		Where("sl.loggable_type = ?", logType).
		Where("w.kandang_id = ?", kandangID).
		Where("f.name IN ?", sapronakFlagsAll).
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	return rows, nil
}

func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) {
	in := make(map[uint][]SapronakDetailRow)
	out := make(map[uint][]SapronakDetailRow)

	for _, row := range rows {
		base := SapronakDetailRow{
			ProductID:   row.ProductID,
			ProductName: row.ProductName,
			Flag:        row.Flag,
			Date:        row.CreatedAt,
			Reference:   refFn(row),
			Price:       row.Price,
		}

		if row.Increase > 0 {
			d := base
			d.QtyIn = row.Increase
			in[row.ProductID] = append(in[row.ProductID], d)
		}
		if row.Decrease > 0 {
			d := base
			d.QtyOut = row.Decrease
			out[row.ProductID] = append(out[row.ProductID], d)
		}
	}

	return in, out
}

func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
	poByWarehouse := r.DB().
		Table("(?) AS ranked_po",
			r.DB().
				Table("purchase_items pi").
				Select(`
					pi.product_warehouse_id,
					po.po_number,
					pi.received_date,
					ROW_NUMBER() OVER (PARTITION BY pi.product_warehouse_id ORDER BY pi.received_date ASC, pi.id ASC) AS rn
				`).
				Joins("JOIN purchases po ON po.id = pi.purchase_id").
				Where("pi.received_date IS NOT NULL"),
		).
		Select("ranked_po.product_warehouse_id, ranked_po.po_number, ranked_po.received_date").
		Where("ranked_po.rn = 1")

	incomingQuery := r.withCtx(ctx).
		Table("adjustment_stocks AS ast").
		Select(`
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			ast.created_at AS date,
			'ADJ-' || CAST(ast.id AS TEXT) AS reference,
			COALESCE(ast.total_qty, 0) AS qty_in,
			0 AS qty_out,
			COALESCE(ast.price, p.product_price, 0) AS price
		`).
		Joins("JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Joins("JOIN products p ON p.id = pw.product_id").
		Where("w.kandang_id = ?", kandangID).
		Where("f.name IN ?", sapronakFlagsAll).
		Where("COALESCE(ast.total_qty, 0) > 0")
	incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p")
	incomingQuery = applyDateRange(incomingQuery, "ast.created_at", start, end)
	incoming, err := scanAndGroupDetails(incomingQuery)
	if err != nil {
		return nil, nil, err
	}

	dateExpr := "COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at)"
	outgoingQuery := r.withCtx(ctx).
		Table("stock_allocations AS sa").
		Select(`
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date,
			COALESCE(
				po.po_number,
				st.movement_number,
				lt.transfer_number,
				pfp_po.po_number,
				CASE WHEN ast_in.id IS NOT NULL THEN 'ADJ-' || CAST(ast_in.id AS TEXT) END,
				CASE WHEN ast.id IS NOT NULL THEN 'ADJ-' || CAST(ast.id AS TEXT) END,
				CASE WHEN pc.id IS NOT NULL THEN 'CHICKIN-' || CAST(pc.id AS TEXT) END
			) AS reference,
			0 AS qty_in,
			COALESCE(SUM(sa.qty), 0) AS qty_out,
			COALESCE(pi.price, ast_in.price, ast.price, p.product_price, 0) AS price
		`).
		Joins("JOIN adjustment_stocks ast ON ast.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyAdjustmentOut.String()).
		Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
		Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
		Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
		Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
		Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
		Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
		Joins("LEFT JOIN adjustment_stocks ast_in ON ast_in.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
		Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
		Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
		Joins("LEFT JOIN (?) pfp_po ON pfp_po.product_warehouse_id = pfp.product_warehouse_id", poByWarehouse).
		Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Joins("JOIN products p ON p.id = pw.product_id").
		Where("sa.status = ?", entity.StockAllocationStatusActive).
		Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("w.kandang_id = ?", kandangID).
		Where("f.name IN ?", sapronakFlagsAll).
		Where("f.name NOT IN ?", sapronakFlags(utils.FlagDOC, utils.FlagPullet)).
		Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, pi.price, ast_in.price, ast.price, p.product_price")
	outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
	outgoingQuery = applyDateRange(outgoingQuery, dateExpr, start, end)
	outgoing, err := scanAndGroupDetails(outgoingQuery)
	if err != nil {
		return nil, nil, err
	}

	return incoming, outgoing, nil
}

func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
	incomingQuery := r.withCtx(ctx).
		Table("stock_transfer_details AS std").
		Select(`
			std.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			st.transfer_date::timestamp AS date,
			COALESCE(st.movement_number, '') AS reference,
			COALESCE(std.total_qty, 0) AS qty_in,
			0 AS qty_out,
			COALESCE(p.product_price, 0) AS price
		`).
		Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
		Joins("LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id").
		Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Joins("JOIN products p ON p.id = std.product_id").
		Where("w.kandang_id = ?", kandangID).
		Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
		Where("f.name IN ?", sapronakFlagsAll)
	incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p")
	incomingQuery = applyDateRange(incomingQuery, "st.transfer_date", start, end)
	incoming, err := scanAndGroupDetails(incomingQuery)
	if err != nil {
		return nil, nil, err
	}

	incomingLayingQuery := r.withCtx(ctx).
		Table("laying_transfer_targets AS ltt").
		Select(`
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			lt.transfer_date::timestamp AS date,
			COALESCE(lt.transfer_number, '') AS reference,
			COALESCE(ltt.total_qty, 0) AS qty_in,
			0 AS qty_out,
			COALESCE(p.product_price, 0) AS price
		`).
		Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
		Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lt.source_product_warehouse_id").
		Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id").
		Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Joins("JOIN products p ON p.id = pw.product_id").
		Where("w.kandang_id = ?", kandangID).
		Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)").
		Where("f.name IN ?", sapronakFlagsAll)
	incomingLayingQuery = r.joinSapronakProductFlag(incomingLayingQuery, "p")
	incomingLayingQuery = applyDateRange(incomingLayingQuery, "lt.transfer_date", start, end)
	incomingLaying, err := scanAndGroupDetails(incomingLayingQuery)
	if err != nil {
		return nil, nil, err
	}
	for pid, rows := range incomingLaying {
		incoming[pid] = append(incoming[pid], rows...)
	}

	outgoingQuery := r.withCtx(ctx).
		Table("stock_allocations AS sa").
		Select(`
			std.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			st.transfer_date::timestamp AS date,
			COALESCE(st.movement_number, '') AS reference,
			0 AS qty_in,
			COALESCE(SUM(sa.qty), 0) AS qty_out,
			COALESCE(p.product_price, 0) AS price
		`).
		Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
		Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
		Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id").
		Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
		Joins("JOIN products p ON p.id = std.product_id").
		Where("sa.status = ?", entity.StockAllocationStatusActive).
		Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("w.kandang_id = ?", kandangID).
		Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
		Where("f.name IN ?", sapronakFlagsAll).
		Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
	outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
	outgoingQuery = applyDateRange(outgoingQuery, "st.transfer_date", start, end)
	outgoing, err := scanAndGroupDetails(outgoingQuery)
	if err != nil {
		return nil, nil, err
	}

	outgoingLayingQuery := r.withCtx(ctx).
		Table("stock_allocations AS sa").
		Select(`
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			lt.transfer_date::timestamp AS date,
			COALESCE(lt.transfer_number, '') AS reference,
			0 AS qty_in,
			COALESCE(SUM(sa.qty), 0) AS qty_out,
			COALESCE(p.product_price, 0) AS price
		`).
		Joins("JOIN laying_transfers lt ON lt.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
		Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
		Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
		Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
		Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Joins("JOIN products p ON p.id = pw.product_id").
		Where("sa.status = ?", entity.StockAllocationStatusActive).
		Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("w.kandang_id = ?", kandangID).
		Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
		Where("f.name IN ?", sapronakFlagsAll).
		Group("lt.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
	outgoingLayingQuery = r.joinSapronakProductFlag(outgoingLayingQuery, "p")
	outgoingLayingQuery = applyDateRange(outgoingLayingQuery, "lt.transfer_date", start, end)
	outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
	if err != nil {
		return nil, nil, err
	}
	for pid, rows := range outgoingLaying {
		outgoing[pid] = append(outgoing[pid], rows...)
	}

	return incoming, outgoing, nil
}

func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
	attributedProjectFlockKandangExpr := `
		COALESCE(
			pc.project_flock_kandang_id,
			pi.project_flock_kandang_id,
			source_pw.project_flock_kandang_id,
			ltt.target_project_flock_kandang_id,
			pw.project_flock_kandang_id
		)
	`

	query := r.withCtx(ctx).
		Table("stock_allocations AS sa").
		Select(`
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			COALESCE(mdp.delivery_date, mdp.created_at) AS date,
			COALESCE(m.so_number, '') AS reference,
			0 AS qty_in,
			COALESCE(SUM(sa.qty), 0) AS qty_out,
			COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
		`).
		Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
		Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
		Joins("JOIN marketings m ON m.id = mp.marketing_id").
		Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
		Joins("JOIN products p ON p.id = pw.product_id").
		Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
		Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
		Joins("LEFT JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id").
		Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
		Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
		Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
		Where("sa.status = ?", entity.StockAllocationStatusActive).
		Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where(attributedProjectFlockKandangExpr+" = ?", projectFlockKandangID).
		Where("f.name IN ?", sapronakFlagsAll).
		Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")

	query = r.joinSapronakProductFlag(query, "p")
	query = applyDateRange(query, "COALESCE(mdp.delivery_date, mdp.created_at)", start, end)
	sales, err := scanAndGroupDetails(query)
	if err != nil {
		return nil, err
	}

	nonFifoQuery := r.withCtx(ctx).
		Table("marketing_delivery_products AS mdp").
		Select(`
			pw.product_id AS product_id,
			p.name AS product_name,
			f.name AS flag,
			COALESCE(mdp.delivery_date, mdp.created_at) AS date,
			COALESCE(m.so_number, '') AS reference,
			0 AS qty_in,
			COALESCE(mdp.usage_qty, 0) AS qty_out,
			COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
		`).
		Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
		Joins("JOIN marketings m ON m.id = mp.marketing_id").
		Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
		Joins("JOIN products p ON p.id = pw.product_id").
		Joins("LEFT JOIN stock_allocations sa ON sa.usable_id = mdp.id AND sa.usable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?",
			fifo.UsableKeyMarketingDelivery.String(),
			entity.StockAllocationStatusActive,
			entity.StockAllocationPurposeConsume,
		).
		Where("mdp.usage_qty > 0").
		Where("sa.id IS NULL").
		Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
		Where("f.name IN ?", sapronakFlagsAll).
		Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")

	nonFifoQuery = r.joinSapronakProductFlag(nonFifoQuery, "p")
	nonFifoQuery = applyDateRange(nonFifoQuery, "COALESCE(mdp.delivery_date, mdp.created_at)", start, end)
	nonFifoSales, err := scanAndGroupDetails(nonFifoQuery)
	if err != nil {
		return nil, err
	}

	for pid, rows := range nonFifoSales {
		sales[pid] = append(sales[pid], rows...)
	}

	return sales, nil
}

func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
	if projectFlockKandangID == 0 {
		return map[uint][]SapronakDetailRow{}, nil
	}

	pfpType := fifo.StockableKeyProjectFlockPopulation.String()
	dateExpr := fmt.Sprintf(`
		CASE
			WHEN sa.stockable_type = '%s' THEN COALESCE(
				pi_pc.received_date,
				st_pc.transfer_date,
				lt_pc.transfer_date,
				ast_pc.created_at,
				pc.chick_in_date
			)
			ELSE COALESCE(
				pi.received_date,
				st.transfer_date,
				lt.transfer_date,
				ast.created_at
			)
		END
	`, pfpType)

	attributedProjectFlockKandangExpr := `
		COALESCE(
			pc.project_flock_kandang_id,
			pi.project_flock_kandang_id,
			source_pw.project_flock_kandang_id,
			ltt.target_project_flock_kandang_id,
			pw_sales.project_flock_kandang_id
		)
	`

	query := r.withCtx(ctx).
		Table("stock_allocations AS sa").
		Select(fmt.Sprintf(`
			p_resolve.id AS product_id,
			p_resolve.name AS product_name,
			f.name AS flag,
			CASE
				WHEN sa.stockable_type = '%s' THEN COALESCE(
					pi_pc.received_date,
					st_pc.transfer_date,
					lt_pc.transfer_date,
					ast_pc.created_at,
					pc.chick_in_date
				)
				ELSE COALESCE(
					pi.received_date,
					st.transfer_date,
					lt.transfer_date,
					ast.created_at
				)
			END AS date,
			CASE
				WHEN sa.stockable_type = '%s' THEN COALESCE(
					po_pc.po_number,
					st_pc.movement_number,
					lt_pc.transfer_number,
					CASE WHEN ast_pc.id IS NOT NULL THEN CONCAT('ADJ-', ast_pc.id) END,
					CONCAT('CHICKIN-', pc.id),
					''
				)
				ELSE COALESCE(
					po.po_number,
					st.movement_number,
					lt.transfer_number,
					CASE WHEN ast.id IS NOT NULL THEN CONCAT('ADJ-', ast.id) END,
					''
				)
			END AS reference,
			0 AS qty_in,
			COALESCE(SUM(sa.qty), 0) AS qty_out,
			CASE
				WHEN sa.stockable_type = '%s' THEN COALESCE(pi_pc.price, p_resolve.product_price, 0)
				ELSE COALESCE(pi.price, p_resolve.product_price, 0)
			END AS price
		`, pfpType, pfpType, pfpType)).
		Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
		Joins("JOIN product_warehouses pw_sales ON pw_sales.id = mdp.product_warehouse_id").
		Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
		Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
		Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
		Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
		Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
		Joins("LEFT JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id").
		Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
		Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
		Joins("LEFT JOIN product_warehouses pw_ltt ON pw_ltt.id = ltt.product_warehouse_id").
		Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
		Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
		Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
		Joins("LEFT JOIN stock_allocations sa_pc ON sa_pc.usable_type = ? AND sa_pc.usable_id = pc.id", fifo.UsableKeyProjectChickin.String()).
		Joins("LEFT JOIN purchase_items pi_pc ON pi_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
		Joins("LEFT JOIN purchases po_pc ON po_pc.id = pi_pc.purchase_id").
		Joins("LEFT JOIN stock_transfer_details std_pc ON std_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
		Joins("LEFT JOIN stock_transfers st_pc ON st_pc.id = std_pc.stock_transfer_id").
		Joins("LEFT JOIN laying_transfer_targets ltt_pc ON ltt_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
		Joins("LEFT JOIN laying_transfers lt_pc ON lt_pc.id = ltt_pc.laying_transfer_id").
		Joins("LEFT JOIN adjustment_stocks ast_pc ON ast_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
		Joins("LEFT JOIN product_warehouses pw_pc ON pw_pc.id = pc.product_warehouse_id").
		Joins(fmt.Sprintf("LEFT JOIN products p_resolve ON p_resolve.id = CASE WHEN sa.stockable_type = '%s' THEN pw_pc.product_id ELSE COALESCE(pi.product_id, pw_ltt.product_id, pw.product_id) END", pfpType)).
		Where("sa.status = ?", entity.StockAllocationStatusActive).
		Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
		Where(attributedProjectFlockKandangExpr+" = ?", projectFlockKandangID).
		Where("f.name IN ?", sapronakFlagsAll).
		Group(`
			p_resolve.id, p_resolve.name, f.name,
			pi_pc.received_date, st_pc.transfer_date, lt_pc.transfer_date, ast_pc.created_at, pc.chick_in_date,
			pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at,
			po_pc.po_number, st_pc.movement_number, lt_pc.transfer_number, ast_pc.id, pc.id,
			po.po_number, st.movement_number, lt.transfer_number, ast.id,
			pi_pc.price, pi.price, p_resolve.product_price, sa.stockable_type
		`)

	query = r.joinSapronakProductFlag(query, "p_resolve")
	query = applyDateRange(query, dateExpr, start, end)
	return scanAndGroupDetails(query)
}

func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
	if len(productIDs) == 0 {
		return []entity.Product{}, nil
	}

	var products []entity.Product
	err := r.DB().WithContext(ctx).
		Preload("Flags").
		Where("id IN ?", productIDs).
		Find(&products).Error
	if err != nil {
		return nil, err
	}

	return products, nil
}
