package service

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

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/sirupsen/logrus"

	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
	projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
)

type SapronakService interface {
	GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, map[uint][]string, error)
	GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, map[uint][]string, error)
}

type sapronakService struct {
	Log                     *logrus.Logger
	Validate                *validator.Validate
	Repository              repository.ClosingRepository
	ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
}

func NewSapronakService(
	repo repository.ClosingRepository,
	pfkRepo projectflockRepository.ProjectFlockKandangRepository,
	validate *validator.Validate,
) SapronakService {
	return &sapronakService{
		Log:                     utils.Log,
		Validate:                validate,
		Repository:              repo,
		ProjectFlockKandangRepo: pfkRepo,
	}
}

func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, map[uint][]string, error) {
	if projectFlockID == 0 {
		return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
	}
	reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
		ProjectFlockID: projectFlockID,
		Status:         "all",
		Flag:           flag,
	})
	if err != nil {
		return nil, nil, err
	}
	if len(reports) <= 1 {
		flags, err := s.collectProductFlags(c.Context(), reports)
		if err != nil {
			return nil, nil, err
		}
		return reports, flags, nil
	}

	combined := s.combineSapronakReports(reports, projectFlockID)
	flags, err := s.collectProductFlags(c.Context(), []dto.SapronakReportDTO{combined})
	if err != nil {
		return nil, nil, err
	}
	return []dto.SapronakReportDTO{combined}, flags, nil
}

func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, map[uint][]string, error) {
	if projectFlockID == 0 || pfkID == 0 {
		return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
	}

	results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
		ProjectFlockID:        projectFlockID,
		ProjectFlockKandangID: pfkID,
		Status:                "all",
		Flag:                  flag,
	})
	if err != nil {
		return nil, nil, err
	}

	for _, res := range results {
		if res.ProjectFlockID == projectFlockID && res.ProjectFlockKandangID == pfkID {
			flags, err := s.collectProductFlags(c.Context(), []dto.SapronakReportDTO{res})
			if err != nil {
				return nil, nil, err
			}
			return &res, flags, nil
		}
	}

	return nil, nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
}

func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
	pfks, err := s.loadProjectFlockKandangs(ctx, params)
	if err != nil {
		return nil, err
	}
	if len(pfks) == 0 {
		return []dto.SapronakReportDTO{}, nil
	}

	filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
	if filterStatus == "" {
		filterStatus = "all"
	}

	results := make([]dto.SapronakReportDTO, 0, len(pfks))
	for _, pfk := range pfks {
		status := "closing"
		if pfk.ClosedAt == nil {
			status = "active"
		}

		if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
			continue
		}

		// Filter sapronak data by project flock period range.
		items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
		if err != nil {
			s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
		}

		results = append(results, dto.SapronakReportDTO{
			ProjectFlockKandangID: pfk.Id,
			ProjectFlockID:        pfk.ProjectFlockId,
			ProjectName:           pfk.ProjectFlock.FlockName,
			KandangID:             pfk.KandangId,
			KandangName:           pfk.Kandang.Name,
			Period:                pfk.Period,
			Status:                status,
			TotalIncomingValue:    totalIncoming,
			TotalUsageValue:       totalUsage,
			Items:                 items,
			Groups:                groups,
		})
	}

	return results, nil
}

func (s sapronakService) collectProductFlags(ctx context.Context, reports []dto.SapronakReportDTO) (map[uint][]string, error) {
	productIDs := make(map[uint]struct{})
	for _, report := range reports {
		for _, group := range report.Groups {
			for _, item := range group.Items {
				if item.ProductID > 0 {
					productIDs[item.ProductID] = struct{}{}
				}
			}
		}
	}
	if len(productIDs) == 0 {
		return map[uint][]string{}, nil
	}

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

	products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, ids)
	if err != nil {
		return nil, err
	}

	result := make(map[uint][]string, len(products))
	for _, product := range products {
		if len(product.Flags) == 0 {
			continue
		}
		flags := make([]string, 0, len(product.Flags))
		for _, flag := range product.Flags {
			name := strings.TrimSpace(flag.Name)
			if name == "" {
				continue
			}
			flags = append(flags, strings.ToUpper(name))
		}
		if len(flags) > 0 {
			result[product.Id] = flags
		}
	}

	return result, nil
}

func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
	db := s.ProjectFlockKandangRepo.DB().WithContext(ctx).
		Preload("ProjectFlock").
		Preload("Kandang").
		Preload("Chickins")

	if params != nil {
		if params.ProjectFlockID > 0 {
			db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockID)
		}
		if params.KandangID > 0 {
			db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangID)
		}
		if params.ProjectFlockKandangID > 0 {
			db = db.Where("project_flock_kandangs.id = ?", params.ProjectFlockKandangID)
		}
	}

	var pfks []entity.ProjectFlockKandang
	if err := db.Find(&pfks).Error; err != nil {
		s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandangs")
	}
	return pfks, nil
}

func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO {
	if len(reports) == 0 {
		return dto.SapronakReportDTO{}
	}

	var (
		totalIncoming float64
		totalUsage    float64
		projectName   = reports[0].ProjectName
	)

	itemMap := make(map[uint]dto.SapronakItemDTO)
	groupMap := make(map[string]*dto.SapronakGroupDTO)

	ensureGroup := func(flag string) *dto.SapronakGroupDTO {
		if g, ok := groupMap[flag]; ok {
			return g
		}
		groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
		return groupMap[flag]
	}

	for _, r := range reports {
		totalIncoming += r.TotalIncomingValue
		totalUsage += r.TotalUsageValue

		for _, it := range r.Items {
			cur := itemMap[it.ProductID]
			if cur.ProductID == 0 {
				cur.ProductID = it.ProductID
				cur.ProductName = it.ProductName
				cur.Flag = it.Flag
			}
			cur.IncomingQty += it.IncomingQty
			cur.IncomingValue += it.IncomingValue
			cur.UsageQty += it.UsageQty
			cur.UsageValue += it.UsageValue
			if cur.IncomingQty >= cur.UsageQty {
				cur.RemainingQty = cur.IncomingQty - cur.UsageQty
			} else {
				cur.RemainingQty = 0
			}
			if cur.IncomingQty > 0 {
				cur.AveragePrice = cur.IncomingValue / cur.IncomingQty
			} else {
				cur.AveragePrice = it.AveragePrice
			}
			itemMap[it.ProductID] = cur
		}

		for _, g := range r.Groups {
			agg := ensureGroup(g.Flag)
			agg.TotalMasuk += g.TotalMasuk
			agg.TotalKeluar += g.TotalKeluar
			agg.SaldoAkhir += g.SaldoAkhir
			agg.TotalNilai += g.TotalNilai
			agg.Items = append(agg.Items, g.Items...)
		}
	}

	items := make([]dto.SapronakItemDTO, 0, len(itemMap))
	for _, it := range itemMap {
		items = append(items, it)
	}

	groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
	for _, g := range groupMap {
		groups = append(groups, *g)
	}

	return dto.SapronakReportDTO{
		ProjectFlockID:     projectID,
		ProjectName:        projectName,
		Status:             "combined",
		StartDate:          nil,
		TotalIncomingValue: totalIncoming,
		TotalUsageValue:    totalUsage,
		Items:              items,
		Groups:             groups,
	}
}

func mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) {
	incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows))
	for _, row := range incomingRows {
		incoming[row.ProductID] = row
	}
	usage := make(map[uint]repository.SapronakUsageRow, len(usageRows))
	for _, row := range usageRows {
		usage[row.ProductID] = row
	}
	return incoming, usage
}

type sapronakDetailMaps struct {
	Incoming    map[uint][]dto.SapronakDetailDTO
	Usage       map[uint][]dto.SapronakDetailDTO
	AdjIncoming map[uint][]dto.SapronakDetailDTO
	AdjOutgoing map[uint][]dto.SapronakDetailDTO
	TransferIn  map[uint][]dto.SapronakDetailDTO
	TransferOut map[uint][]dto.SapronakDetailDTO
	SalesOut    map[uint][]dto.SapronakDetailDTO
}

func buildSapronakDetails(
	incomingRows map[uint][]repository.SapronakDetailRow,
	usageRows map[uint][]repository.SapronakDetailRow,
	adjIncomingRows map[uint][]repository.SapronakDetailRow,
	adjOutgoingRows map[uint][]repository.SapronakDetailRow,
	transferInRows map[uint][]repository.SapronakDetailRow,
	transferOutRows map[uint][]repository.SapronakDetailRow,
	salesOutRows map[uint][]repository.SapronakDetailRow,
) sapronakDetailMaps {
	result := sapronakDetailMaps{
		Incoming:    make(map[uint][]dto.SapronakDetailDTO),
		Usage:       make(map[uint][]dto.SapronakDetailDTO),
		AdjIncoming: make(map[uint][]dto.SapronakDetailDTO),
		AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
		TransferIn:  make(map[uint][]dto.SapronakDetailDTO),
		TransferOut: make(map[uint][]dto.SapronakDetailDTO),
		SalesOut:    make(map[uint][]dto.SapronakDetailDTO),
	}

	addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
		for pid, rows := range src {
			for _, r := range rows {
				d := dto.SapronakDetailDTO{
					ProductID:      r.ProductID,
					ProductName:    r.ProductName,
					Flag:           r.Flag,
					Tanggal:        r.Date,
					NoReferensi:    r.Reference,
					JenisTransaksi: jenis,
					Harga:          r.Price,
				}
				if masuk {
					d.QtyMasuk = r.QtyIn
					d.Nilai = r.QtyIn * r.Price
				} else {
					d.QtyKeluar = r.QtyOut
					d.Nilai = r.QtyOut * r.Price
				}
				target[pid] = append(target[pid], d)
			}
		}
	}

	addRows(result.Incoming, incomingRows, "Pembelian", true)
	addRows(result.Usage, usageRows, "Pemakaian", false)
	addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true)
	// Outgoing adjustment rows here are sourced from stock allocation
	// consume flow (adjustment_stocks.usage_qty), so treat them as usage.
	addRows(result.AdjOutgoing, adjOutgoingRows, "Pemakaian", false)
	addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
	addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
	addRows(result.SalesOut, salesOutRows, "Penjualan", false)

	return result
}

func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
	// Filter by project flock period (start = first chickin or pfk created_at, end = closed_at if any).
	startDate, endDate := sapronakPeriodRange(pfk)
	incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.Id, pfk.KandangId, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.Id, pfk.KandangId, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	chickinUsageRows, err := s.Repository.FetchSapronakChickinUsage(ctx, pfk.Id, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	chickinUsageDetailsRows, err := s.Repository.FetchSapronakChickinUsageDetails(ctx, pfk.Id, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	usageAllocatedDetails, err := s.Repository.FetchSapronakUsageAllocatedDetails(ctx, pfk.Id, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	if len(usageAllocatedDetails) > 0 {
		usageDetailsRows = usageAllocatedDetails
		chickinUsageDetailsRows = map[uint][]repository.SapronakDetailRow{}
	}
	adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	transIncomingRows, transOutgoingRows, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}
	salesOutRows, err := s.Repository.FetchSapronakSalesAllocatedDetails(ctx, pfk.Id, startDate, endDate)
	if err != nil {
		return nil, nil, 0, 0, err
	}

	filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
	matchesFlag := func(f string) bool {
		if filterFlag == "" {
			return true
		}
		candidate := strings.ToUpper(f)
		if filterFlag == "DOC" || filterFlag == "PULLET" {
			return candidate == "DOC" || candidate == "PULLET"
		}
		return candidate == filterFlag
	}
	dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO {
		result := make(map[uint][]dto.SapronakDetailDTO, len(src))
		seen := make(map[string]struct{})
		for pid, rows := range src {
			for _, d := range rows {
				dateKey := ""
				if d.Tanggal != nil {
					dateKey = d.Tanggal.Format("2006-01-02")
				}
				qtyKey := d.QtyMasuk
				if qtyKey == 0 {
					qtyKey = d.QtyKeluar
				}

				ref := strings.TrimSpace(d.NoReferensi)
				key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey)
				if ref == "" {
					key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag)))
				}
				if _, ok := seen[key]; ok {
					continue
				}
				seen[key] = struct{}{}
				result[pid] = append(result[pid], d)
			}
		}
		return result
	}

	// For project flocks with category GROWING, pullet usage from chickin
	// should not be counted yet. Only when category is LAYING we allow
	// pullet usage to contribute to qty_used.
	isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying))
	hasChickin := len(pfk.Chickins) > 0

	if !isLaying {
		filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
		for _, row := range chickinUsageRows {
			if strings.ToUpper(row.Flag) == "DOC" {
				filteredUsage = append(filteredUsage, row)
			}
		}
		chickinUsageRows = filteredUsage

		filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows))
		for pid, rows := range chickinUsageDetailsRows {
			for _, d := range rows {
				if strings.ToUpper(d.Flag) == "DOC" {
					filteredDetail[pid] = append(filteredDetail[pid], d)
				}
			}
		}
		chickinUsageDetailsRows = filteredDetail
	}

	for pid, rows := range chickinUsageDetailsRows {
		if len(rows) == 0 {
			continue
		}
		usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
	}

	detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows)
	incomingDetails := detailMaps.Incoming
	usageDetails := detailMaps.Usage
	adjIncoming := detailMaps.AdjIncoming
	adjOutgoing := detailMaps.AdjOutgoing
	transIncoming := detailMaps.TransferIn
	transOutgoing := detailMaps.TransferOut
	salesOutgoing := detailMaps.SalesOut

	allUsageRows := append(usageRows, chickinUsageRows...)
	incoming, usage := mapIncomingUsage(incomingRows, allUsageRows)
	itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
	groupMap := make(map[string]*dto.SapronakGroupDTO)

	transIncoming = dedupTransfers(transIncoming)
	transOutgoing = dedupTransfers(transOutgoing)

	ensureGroup := func(flag string) *dto.SapronakGroupDTO {
		if g, ok := groupMap[flag]; ok {
			return g
		}
		groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
		return groupMap[flag]
	}

	resolveFlagName := func(productID uint, details []dto.SapronakDetailDTO) (string, string) {
		flag := ""
		name := ""
		if item, ok := itemMap[productID]; ok {
			flag = item.Flag
			name = item.ProductName
		}
		if flag == "" && len(details) > 0 {
			flag = details[0].Flag
		}
		if name == "" && len(details) > 0 {
			name = details[0].ProductName
		}
		return flag, name
	}

	for _, row := range incoming {
		if !matchesFlag(row.Flag) {
			continue
		}
		avgPrice := row.DefaultPrice
		if row.Qty > 0 && row.Value > 0 {
			avgPrice = row.Value / row.Qty
		}

		itemMap[row.ProductID] = dto.SapronakItemDTO{
			ProductID:     row.ProductID,
			ProductName:   row.ProductName,
			Flag:          row.Flag,
			IncomingQty:   row.Qty,
			IncomingValue: row.Value,
			RemainingQty:  row.Qty,
			AveragePrice:  avgPrice,
		}
	}

	for _, row := range usage {
		if !matchesFlag(row.Flag) {
			continue
		}
		existing := itemMap[row.ProductID]
		price := existing.AveragePrice
		if price == 0 {
			price = row.DefaultPrice
		}

		usageValue := row.Qty * price

		existing.ProductID = row.ProductID
		if existing.ProductName == "" {
			existing.ProductName = row.ProductName
		}
		if existing.Flag == "" {
			existing.Flag = row.Flag
		}
		existing.AveragePrice = price
		existing.UsageQty += row.Qty
		existing.UsageValue += usageValue
		if existing.IncomingQty >= existing.UsageQty {
			existing.RemainingQty = existing.IncomingQty - existing.UsageQty
		} else {
			existing.RemainingQty = 0
		}

		itemMap[row.ProductID] = existing
	}

	for productID, details := range adjIncoming {
		for _, d := range details {
			if !matchesFlag(d.Flag) {
				continue
			}
			existing := itemMap[productID]
			if existing.Flag == "" {
				existing.Flag = d.Flag
			}
			if existing.ProductName == "" {
				existing.ProductName = d.ProductName
			}
			existing.IncomingQty += d.QtyMasuk
			existing.IncomingValue += d.Nilai
			if existing.IncomingQty > 0 {
				existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
			}
			if existing.IncomingQty >= existing.UsageQty {
				existing.RemainingQty = existing.IncomingQty - existing.UsageQty
			} else {
				existing.RemainingQty = 0
			}
			itemMap[productID] = existing
		}
	}

	for productID, details := range adjOutgoing {
		for _, d := range details {
			if !matchesFlag(d.Flag) {
				continue
			}
			existing := itemMap[productID]
			if existing.Flag == "" {
				existing.Flag = d.Flag
			}
			if existing.ProductName == "" {
				existing.ProductName = d.ProductName
			}
			// Adjustment keluar should reduce stock without inflating usage-based HPP.
			remaining := existing.IncomingQty - existing.UsageQty - d.QtyKeluar
			if remaining < 0 {
				remaining = 0
			}
			existing.RemainingQty = remaining
			itemMap[productID] = existing
		}
	}

	for productID, details := range transIncoming {
		for _, d := range details {
			if !matchesFlag(d.Flag) {
				continue
			}
			existing := itemMap[productID]
			if existing.Flag == "" {
				existing.Flag = d.Flag
			}
			if existing.ProductName == "" {
				existing.ProductName = d.ProductName
			}
			existing.IncomingQty += d.QtyMasuk
			existing.IncomingValue += d.Nilai
			if existing.IncomingQty > 0 {
				existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
			}
			if existing.IncomingQty >= existing.UsageQty {
				existing.RemainingQty = existing.IncomingQty - existing.UsageQty
			} else {
				existing.RemainingQty = 0
			}
			itemMap[productID] = existing
		}
	}

	items := make([]dto.SapronakItemDTO, 0, len(itemMap))
	var totalIncoming, totalUsage float64
	for _, item := range itemMap {
		totalIncoming += item.IncomingValue
		totalUsage += item.UsageValue
		items = append(items, item)
	}

	for productID, details := range incomingDetails {
		flag, name := resolveFlagName(productID, details)
		if !matchesFlag(flag) {
			continue
		}
		group := ensureGroup(flag)
		for _, d := range details {
			if d.Flag == "" {
				d.Flag = flag
			}
			if d.ProductName == "" {
				d.ProductName = name
			}
			group.Items = append(group.Items, d)
			group.TotalMasuk += d.QtyMasuk
			group.TotalNilai += d.Nilai
			group.SaldoAkhir += d.QtyMasuk
		}
	}

	for productID, details := range adjIncoming {
		flag, name := resolveFlagName(productID, details)
		if !matchesFlag(flag) {
			continue
		}
		group := ensureGroup(flag)
		for _, d := range details {
			if d.Flag == "" {
				d.Flag = flag
			}
			if d.ProductName == "" {
				d.ProductName = name
			}
			group.Items = append(group.Items, d)
			group.TotalMasuk += d.QtyMasuk
			group.TotalNilai += d.Nilai
			group.SaldoAkhir += d.QtyMasuk
		}
	}

	for productID, details := range usageDetails {
		flag, name := resolveFlagName(productID, details)
		if !matchesFlag(flag) {
			continue
		}
		group := ensureGroup(flag)
		for _, d := range details {
			if d.Flag == "" {
				d.Flag = flag
			}
			if d.ProductName == "" {
				d.ProductName = name
			}
			group.Items = append(group.Items, d)
			group.TotalKeluar += d.QtyKeluar
			group.SaldoAkhir -= d.QtyKeluar
		}
	}

	for productID, details := range adjOutgoing {
		flag, name := resolveFlagName(productID, details)
		if !matchesFlag(flag) {
			continue
		}
		group := ensureGroup(flag)
		for _, d := range details {
			if d.Flag == "" {
				d.Flag = flag
			}
			if d.ProductName == "" {
				d.ProductName = name
			}
			group.Items = append(group.Items, d)
			group.TotalKeluar += d.QtyKeluar
			group.SaldoAkhir -= d.QtyKeluar
		}
	}

	for productID, details := range transIncoming {
		flag, name := resolveFlagName(productID, details)
		if !matchesFlag(flag) {
			continue
		}
		group := ensureGroup(flag)
		for _, d := range details {
			if d.Flag == "" {
				d.Flag = flag
			}
			if d.ProductName == "" {
				d.ProductName = name
			}
			group.Items = append(group.Items, d)
			group.TotalMasuk += d.QtyMasuk
			group.TotalNilai += d.Nilai
			group.SaldoAkhir += d.QtyMasuk
		}
	}

	for productID, details := range transOutgoing {
		flag, name := resolveFlagName(productID, details)
		if !matchesFlag(flag) {
			continue
		}
		if hasChickin && (strings.EqualFold(flag, "DOC") || strings.EqualFold(flag, "PULLET") || strings.EqualFold(flag, "LAYER")) {
			continue
		}
		group := ensureGroup(flag)
		for _, d := range details {
			if d.Flag == "" {
				d.Flag = flag
			}
			if d.ProductName == "" {
				d.ProductName = name
			}
			group.Items = append(group.Items, d)
			group.TotalKeluar += d.QtyKeluar
			group.SaldoAkhir -= d.QtyKeluar
		}
	}

	for productID, details := range salesOutgoing {
		flag, name := resolveFlagName(productID, details)
		if !matchesFlag(flag) {
			continue
		}
		// For chicken, we don't count sales as sapronak outflow.
		if strings.EqualFold(flag, "DOC") || strings.EqualFold(flag, "PULLET") || strings.EqualFold(flag, "LAYER") {
			continue
		}
		group := ensureGroup(flag)
		for _, d := range details {
			if d.Flag == "" {
				d.Flag = flag
			}
			if d.ProductName == "" {
				d.ProductName = name
			}
			group.Items = append(group.Items, d)
			group.TotalKeluar += d.QtyKeluar
			group.SaldoAkhir -= d.QtyKeluar
		}
	}

	groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
	for _, g := range groupMap {
		groups = append(groups, *g)
	}

	return items, groups, totalIncoming, totalUsage, nil
}

func sapronakPeriodRange(pfk entity.ProjectFlockKandang) (*time.Time, *time.Time) {
	if len(pfk.Chickins) == 0 {
		start := dateOnlyUTC(pfk.CreatedAt)
		return &start, pfk.ClosedAt
	}

	minDate := pfk.Chickins[0].ChickInDate
	for _, c := range pfk.Chickins[1:] {
		if c.ChickInDate.Before(minDate) {
			minDate = c.ChickInDate
		}
	}

	start := dateOnlyUTC(minDate)
	return &start, pfk.ClosedAt
}
