package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"
	"time"

	"gitlab.com/mbugroup/lti-api.git/internal/config"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
	repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"

	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
	expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
	marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
	areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
	customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
	productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
	supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
	warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
	chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
	recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
	purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"

	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"

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

type RepportService interface {
	GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
	GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error)
	GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error)
	UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error)
	GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error)
	GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
	GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
	GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
	GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error)
	GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
	GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error)
	DB() *gorm.DB
}

type repportService struct {
	Log                          *logrus.Logger
	Validate                     *validator.Validate
	db                           *gorm.DB
	ExpenseRealizationRepo       expenseRepo.ExpenseRealizationRepository
	ExpenseDepreciationRepo      repportRepo.ExpenseDepreciationRepository
	MarketingDeliveryRepo        marketingRepo.MarketingDeliveryProductRepository
	PurchaseRepo                 purchaseRepo.PurchaseRepository
	ChickinRepo                  chickinRepo.ProjectChickinRepository
	RecordingRepo                recordingRepo.RecordingRepository
	ApprovalSvc                  approvalService.ApprovalService
	HppSvc                       approvalService.HppService
	HppV2Svc                     approvalService.HppV2Service
	HppCostRepo                  commonRepo.HppCostRepository
	PurchaseSupplierRepo         repportRepo.PurchaseSupplierRepository
	DebtSupplierRepo             repportRepo.DebtSupplierRepository
	HppPerKandangRepo            repportRepo.HppPerKandangRepository
	ProductionResultRepo         repportRepo.ProductionResultRepository
	CustomerPaymentRepo          repportRepo.CustomerPaymentRepository
	CustomerRepo                 customerRepo.CustomerRepository
	StandardGrowthDetailRepo     productionStandardRepository.StandardGrowthDetailRepository
	ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
}

type HppCostAggregate struct {
	FeedCost    float64
	OvkCost     float64
	DocCost     float64
	DocQty      float64
	BudgetCost  float64
	ExpenseCost float64
}

func NewRepportService(
	db *gorm.DB,
	validate *validator.Validate,
	expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
	expenseDepreciationRepo repportRepo.ExpenseDepreciationRepository,
	marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
	purchaseRepo purchaseRepo.PurchaseRepository,
	chickinRepo chickinRepo.ProjectChickinRepository,
	recordingRepo recordingRepo.RecordingRepository,
	approvalSvc approvalService.ApprovalService,
	hppSvc approvalService.HppService,
	hppV2Svc approvalService.HppV2Service,
	hppCostRepo commonRepo.HppCostRepository,
	purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
	debtSupplierRepo repportRepo.DebtSupplierRepository,
	hppPerKandangRepo repportRepo.HppPerKandangRepository,
	productionResultRepo repportRepo.ProductionResultRepository,
	customerPaymentRepo repportRepo.CustomerPaymentRepository,
	customerRepo customerRepo.CustomerRepository,
	standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
	productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
) RepportService {
	return &repportService{
		Log:                          utils.Log,
		Validate:                     validate,
		db:                           db,
		ExpenseRealizationRepo:       expenseRealizationRepo,
		ExpenseDepreciationRepo:      expenseDepreciationRepo,
		MarketingDeliveryRepo:        marketingDeliveryRepo,
		PurchaseRepo:                 purchaseRepo,
		ChickinRepo:                  chickinRepo,
		RecordingRepo:                recordingRepo,
		ApprovalSvc:                  approvalSvc,
		HppSvc:                       hppSvc,
		HppV2Svc:                     hppV2Svc,
		HppCostRepo:                  hppCostRepo,
		PurchaseSupplierRepo:         purchaseSupplierRepo,
		DebtSupplierRepo:             debtSupplierRepo,
		HppPerKandangRepo:            hppPerKandangRepo,
		ProductionResultRepo:         productionResultRepo,
		CustomerPaymentRepo:          customerPaymentRepo,
		CustomerRepo:                 customerRepo,
		StandardGrowthDetailRepo:     standardGrowthDetailRepo,
		ProductionStandardDetailRepo: productionStandardDetailRepo,
	}
}

func (s *repportService) DB() *gorm.DB {
	return s.db
}

func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

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

	realizations, total, err := s.ExpenseRealizationRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params)
	if err != nil {
		s.Log.Errorf("GetAllWithFilters error: %v", err)
		return nil, 0, err
	}

	result := dto.ToRepportExpenseListDTOs(realizations)

	expenseIDs := make([]uint, 0, len(result))
	for i := range result {
		expenseIDs = append(expenseIDs, uint(result[i].Id))
	}

	approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, expenseIDs, func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	})
	if err != nil {
		s.Log.Warnf("LatestByTargets error: %v", err)
	}

	for i := range result {
		expenseIDAsUint := uint(result[i].Id)
		if approval, exists := approvals[expenseIDAsUint]; exists && approval != nil {
			mapped := approvalDTO.ToApprovalDTO(*approval)
			result[i].LatestApproval = &mapped
		}
	}

	return result, total, nil
}

func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) {
	params, filters, err := s.parseExpenseDepreciationQuery(ctx)
	if err != nil {
		return nil, nil, err
	}
	if err := s.Validate.Struct(params); err != nil {
		return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	if s.ExpenseDepreciationRepo == nil {
		return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
	}

	location, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
	}
	periodDate, err := time.ParseInLocation("2006-01-02", params.Period, location)
	if err != nil {
		return nil, nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD")
	}

	candidateRows, err := s.ExpenseDepreciationRepo.GetCandidateFarms(
		ctx.Context(),
		periodDate,
		params.AreaIDs,
		params.LocationIDs,
		params.ProjectFlockIDs,
	)
	if err != nil {
		return nil, nil, err
	}

	limit := params.Limit
	if limit <= 0 {
		limit = 10
	}
	if len(candidateRows) == 0 {
		meta := &dto.ExpenseDepreciationMetaDTO{
			Page:         params.Page,
			Limit:        limit,
			TotalPages:   1,
			TotalResults: 0,
			Filters:      filters,
		}
		return []dto.ExpenseDepreciationRowDTO{}, meta, nil
	}

	farmIDs := make([]uint, 0, len(candidateRows))
	farmNameByID := make(map[uint]string, len(candidateRows))
	for _, row := range candidateRows {
		farmIDs = append(farmIDs, row.ProjectFlockID)
		farmNameByID[row.ProjectFlockID] = row.FarmName
	}

	snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot)
	if params.ForceRecompute {
		computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, farmIDs, farmNameByID)
		if computeErr != nil {
			return nil, nil, computeErr
		}
		if len(computedSnapshots) > 0 {
			if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
				return nil, nil, err
			}
			snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(computedSnapshots))
			for _, row := range computedSnapshots {
				snapshotByFarmID[row.ProjectFlockId] = row
			}
		}
	} else {
		snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs)
		if err != nil {
			return nil, nil, err
		}
		snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots))
		for _, row := range snapshots {
			snapshotByFarmID[row.ProjectFlockId] = row
		}

		missingFarmIDs := make([]uint, 0)
		for _, farmID := range farmIDs {
			if _, exists := snapshotByFarmID[farmID]; exists {
				continue
			}
			missingFarmIDs = append(missingFarmIDs, farmID)
		}

		if len(missingFarmIDs) > 0 {
			computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID)
			if computeErr != nil {
				return nil, nil, computeErr
			}
			if len(computedSnapshots) > 0 {
				if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
					return nil, nil, err
				}
				for _, row := range computedSnapshots {
					snapshotByFarmID[row.ProjectFlockId] = row
				}
			}
		}
	}

	rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows))
	for _, candidate := range candidateRows {
		snapshot, exists := snapshotByFarmID[candidate.ProjectFlockID]
		if !exists {
			rows = append(rows, dto.ExpenseDepreciationRowDTO{
				ProjectFlockID:               int64(candidate.ProjectFlockID),
				FarmName:                     candidate.FarmName,
				Period:                       params.Period,
				DepreciationPercentEffective: 0,
				DepreciationValue:            0,
				PulletCostDayNTotal:          0,
				Components:                   map[string]any{},
			})
			continue
		}
		rows = append(rows, dto.ExpenseDepreciationRowDTO{
			ProjectFlockID:               int64(snapshot.ProjectFlockId),
			FarmName:                     candidate.FarmName,
			Period:                       params.Period,
			DepreciationPercentEffective: snapshot.DepreciationPercentEffective,
			DepreciationValue:            snapshot.DepreciationValue,
			PulletCostDayNTotal:          snapshot.PulletCostDayNTotal,
			Components:                   parseSnapshotComponents(snapshot.Components),
		})
	}

	totalResults := int64(len(rows))
	totalPages := int64(0)
	if totalResults > 0 {
		totalPages = int64(math.Ceil(float64(totalResults) / float64(limit)))
	}
	if totalPages == 0 {
		totalPages = 1
	}

	offset := (params.Page - 1) * limit
	if offset < 0 {
		offset = 0
	}
	if offset > len(rows) {
		offset = len(rows)
	}
	end := offset + limit
	if end > len(rows) {
		end = len(rows)
	}

	meta := &dto.ExpenseDepreciationMetaDTO{
		Page:         params.Page,
		Limit:        limit,
		TotalPages:   totalPages,
		TotalResults: totalResults,
		Filters:      filters,
	}

	return rows[offset:end], meta, nil
}

func (s *repportService) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) {
	params, filters, err := s.parseExpenseDepreciationQuery(ctx)
	if err != nil {
		return nil, nil, err
	}
	if s.ExpenseDepreciationRepo == nil {
		return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
	}

	repoRows, err := s.ExpenseDepreciationRepo.GetLatestManualInputsByFarms(
		ctx.Context(),
		params.AreaIDs,
		params.LocationIDs,
		params.ProjectFlockIDs,
	)
	if err != nil {
		return nil, nil, err
	}

	rows := make([]dto.ExpenseDepreciationManualInputRowDTO, 0, len(repoRows))
	for _, row := range repoRows {
		rows = append(rows, dto.ExpenseDepreciationManualInputRowDTO{
			ID:             int64(row.Id),
			ProjectFlockID: int64(row.ProjectFlockID),
			FarmName:       row.FarmName,
			TotalCost:      row.TotalCost,
			CutoverDate:    row.CutoverDate.Format("2006-01-02"),
			Note:           row.Note,
		})
	}

	limit := params.Limit
	if limit <= 0 {
		limit = 10
	}
	totalResults := int64(len(rows))
	totalPages := int64(0)
	if totalResults > 0 {
		totalPages = int64(math.Ceil(float64(totalResults) / float64(limit)))
	}
	if totalPages == 0 {
		totalPages = 1
	}

	offset := (params.Page - 1) * limit
	if offset < 0 {
		offset = 0
	}
	if offset > len(rows) {
		offset = len(rows)
	}
	end := offset + limit
	if end > len(rows) {
		end = len(rows)
	}

	meta := &dto.ExpenseDepreciationMetaDTO{
		Page:         params.Page,
		Limit:        limit,
		TotalPages:   totalPages,
		TotalResults: totalResults,
		Filters:      filters,
	}

	return rows[offset:end], meta, nil
}

func (s *repportService) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error) {
	if req == nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "request is required")
	}
	if err := s.Validate.Struct(req); err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	if s.ExpenseDepreciationRepo == nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
	}
	location, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
	}
	cutoverDate, err := time.ParseInLocation("2006-01-02", req.CutoverDate, location)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "cutover_date must follow format YYYY-MM-DD")
	}

	row := entity.FarmDepreciationManualInput{
		ProjectFlockId: req.ProjectFlockID,
		TotalCost:      req.TotalCost,
		CutoverDate:    cutoverDate,
		Note:           req.Note,
	}
	if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil {
		return nil, err
	}
	if err := s.ExpenseDepreciationRepo.DeleteSnapshotsFromDate(
		ctx.Context(),
		cutoverDate,
		[]uint{row.ProjectFlockId},
	); err != nil {
		return nil, err
	}

	response := &dto.ExpenseDepreciationManualInputRowDTO{
		ID:             int64(row.Id),
		ProjectFlockID: int64(row.ProjectFlockId),
		TotalCost:      row.TotalCost,
		CutoverDate:    row.CutoverDate.Format("2006-01-02"),
		Note:           row.Note,
	}

	listRows, listErr := s.ExpenseDepreciationRepo.GetLatestManualInputsByFarms(
		ctx.Context(),
		nil,
		nil,
		[]int64{int64(row.ProjectFlockId)},
	)
	if listErr == nil {
		for _, listRow := range listRows {
			if listRow.ProjectFlockID == row.ProjectFlockId {
				response.FarmName = listRow.FarmName
				break
			}
		}
	}

	return response, nil
}

type depreciationKandangComponent struct {
	ProjectFlockKandangID uint    `json:"project_flock_kandang_id"`
	KandangID             uint    `json:"kandang_id"`
	KandangName           string  `json:"kandang_name"`
	TransferID            uint    `json:"transfer_id"`
	TransferDate          string  `json:"transfer_date"`
	SourceProjectFlockID  uint    `json:"source_project_flock_id"`
	HouseType             string  `json:"house_type"`
	DayN                  int     `json:"day_n"`
	DepreciationPercent   float64 `json:"depreciation_percent"`
	TransferQty           float64 `json:"transfer_qty"`
	PulletCostDayN        float64 `json:"pullet_cost_day_n"`
	DepreciationValue     float64 `json:"depreciation_value"`
	DepreciationSource    string  `json:"depreciation_source,omitempty"`
	ManualInputID         *uint   `json:"manual_input_id,omitempty"`
	CutoverDate           string  `json:"cutover_date,omitempty"`
	OriginDate            string  `json:"origin_date,omitempty"`
	StartScheduleDay      *int    `json:"start_schedule_day,omitempty"`
}

type depreciationFarmComponents struct {
	KandangCount int                            `json:"kandang_count"`
	Kandang      []depreciationKandangComponent `json:"kandang"`
}

func (s *repportService) computeExpenseDepreciationSnapshots(
	ctx context.Context,
	periodDate time.Time,
	farmIDs []uint,
	farmNameByID map[uint]string,
) ([]entity.FarmDepreciationSnapshot, error) {
	_ = farmNameByID

	if len(farmIDs) == 0 {
		return []entity.FarmDepreciationSnapshot{}, nil
	}
	if s.HppCostRepo == nil {
		return nil, errors.New("hpp cost repository is not configured")
	}
	if s.HppV2Svc == nil {
		return nil, errors.New("hpp v2 service is not configured")
	}

	result := make([]entity.FarmDepreciationSnapshot, 0, len(farmIDs))
	for _, farmID := range farmIDs {
		kandangIDs, err := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, farmID)
		if err != nil {
			return nil, err
		}

		components := depreciationFarmComponents{
			Kandang: make([]depreciationKandangComponent, 0, len(kandangIDs)),
		}

		totalDepreciationValue := 0.0
		totalPulletCostDayN := 0.0
		for _, kandangID := range kandangIDs {
			breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &periodDate)
			if err != nil {
				return nil, err
			}
			if breakdown == nil {
				continue
			}

			depreciationComponent := hppV2FindDepreciationComponent(breakdown)
			if depreciationComponent == nil {
				continue
			}

			for _, part := range depreciationComponent.Parts {
				if part.Total <= 0 {
					continue
				}

				houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType)
				component := depreciationKandangComponent{
					ProjectFlockKandangID: breakdown.ProjectFlockKandangID,
					KandangID:             breakdown.KandangID,
					KandangName:           breakdown.KandangName,
					SourceProjectFlockID:  hppV2DetailUint(part.Details, "source_project_flock_id"),
					HouseType:             houseType,
					DayN:                  hppV2DetailInt(part.Details, "schedule_day"),
					DepreciationPercent:   hppV2DetailFloat(part.Details, "depreciation_percent"),
					PulletCostDayN:        hppV2DetailFloat(part.Details, "pullet_cost_day_n"),
					DepreciationValue:     part.Total,
					DepreciationSource:    part.Code,
					OriginDate:            hppV2DetailString(part.Details, "origin_date"),
				}

				if component.HouseType == "" {
					component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type"))
				}

				if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil {
					component.TransferID = ref.ID
					component.TransferDate = ref.Date
					component.TransferQty = ref.Qty
				}

				if part.Code == "manual_cutover" {
					if startDay := hppV2DetailInt(part.Details, "start_schedule_day"); startDay > 0 {
						component.StartScheduleDay = &startDay
					}
					component.CutoverDate = hppV2DetailString(part.Details, "cutover_date")
					if manualID := hppV2DetailUint(part.Details, "manual_input_id"); manualID > 0 {
						component.ManualInputID = &manualID
					}
					if component.ManualInputID == nil {
						if ref := hppV2FindReference(part.References, "farm_depreciation_manual_input"); ref != nil && ref.ID > 0 {
							manualID := ref.ID
							component.ManualInputID = &manualID
						}
					}
				}

				totalPulletCostDayN += component.PulletCostDayN
				totalDepreciationValue += component.DepreciationValue
				components.Kandang = append(components.Kandang, component)
			}
		}

		components.KandangCount = len(components.Kandang)
		effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)

		componentsJSON, marshalErr := json.Marshal(components)
		if marshalErr != nil {
			return nil, marshalErr
		}

		result = append(result, entity.FarmDepreciationSnapshot{
			ProjectFlockId:               farmID,
			PeriodDate:                   periodDate,
			DepreciationPercentEffective: effectivePercent,
			DepreciationValue:            totalDepreciationValue,
			PulletCostDayNTotal:          totalPulletCostDayN,
			Components:                   componentsJSON,
		})
	}

	return result, nil
}

func hppV2FindDepreciationComponent(breakdown *approvalService.HppV2Breakdown) *approvalService.HppV2Component {
	if breakdown == nil {
		return nil
	}
	for idx := range breakdown.Components {
		if breakdown.Components[idx].Code == "DEPRECIATION" {
			return &breakdown.Components[idx]
		}
	}
	return nil
}

func hppV2FindReference(references []approvalService.HppV2Reference, refType string) *approvalService.HppV2Reference {
	if refType == "" {
		return nil
	}
	for idx := range references {
		if references[idx].Type == refType {
			return &references[idx]
		}
	}
	return nil
}

func hppV2DetailFloat(details map[string]any, key string) float64 {
	if details == nil || key == "" {
		return 0
	}

	raw, exists := details[key]
	if !exists || raw == nil {
		return 0
	}

	switch value := raw.(type) {
	case float64:
		return value
	case float32:
		return float64(value)
	case int:
		return float64(value)
	case int8:
		return float64(value)
	case int16:
		return float64(value)
	case int32:
		return float64(value)
	case int64:
		return float64(value)
	case uint:
		return float64(value)
	case uint8:
		return float64(value)
	case uint16:
		return float64(value)
	case uint32:
		return float64(value)
	case uint64:
		return float64(value)
	case string:
		parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
		if err != nil {
			return 0
		}
		return parsed
	default:
		return 0
	}
}

func hppV2DetailInt(details map[string]any, key string) int {
	return int(math.Round(hppV2DetailFloat(details, key)))
}

func hppV2DetailUint(details map[string]any, key string) uint {
	value := hppV2DetailInt(details, key)
	if value < 0 {
		return 0
	}
	return uint(value)
}

func hppV2DetailString(details map[string]any, key string) string {
	if details == nil || key == "" {
		return ""
	}
	raw, exists := details[key]
	if !exists || raw == nil {
		return ""
	}
	switch value := raw.(type) {
	case string:
		return value
	case time.Time:
		return value.Format("2006-01-02")
	default:
		return fmt.Sprintf("%v", value)
	}
}

func parseSnapshotComponents(raw []byte) any {
	if len(raw) == 0 {
		return map[string]any{}
	}
	var out any
	if err := json.Unmarshal(raw, &out); err != nil {
		return map[string]any{}
	}
	return out
}

func valueOrEmptyString(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}

func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

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

	deliveryProducts, total, err := s.MarketingDeliveryRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params)
	if err != nil {
		return nil, 0, err
	}

	customerGroups := make(map[uint][]entity.MarketingDeliveryProduct)
	for _, dp := range deliveryProducts {
		customerID := dp.MarketingProduct.Marketing.CustomerId
		customerGroups[customerID] = append(customerGroups[customerID], dp)
	}

	agingMap := make(map[int]int)
	for customerID := range customerGroups {
		transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(c.Context(), &customerID)
		if err != nil {
			continue
		}

		initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(c.Context(), customerID)
		if err != nil {
			initialBalance = 0
		}

		runningBalance := initialBalance
		for i, tx := range transactions {
			if tx.TransactionType == "SALES" {
				previousBalance := runningBalance
				runningBalance -= tx.TotalPrice
				currentBalance := runningBalance

				_, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, currentBalance)

				if paymentDate != nil {
					agingDays := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
					agingMap[int(tx.TransactionID)] = agingDays
				} else {
					agingDays := int(time.Since(tx.TransDate).Hours() / 24)
					agingMap[int(tx.TransactionID)] = agingDays
				}
			} else if tx.TransactionType == "PAYMENT" {
				runningBalance += tx.PaymentAmount
			}
		}
	}

	deliveryIDs := make([]uint, 0, len(deliveryProducts))
	for _, delivery := range deliveryProducts {
		deliveryIDs = append(deliveryIDs, delivery.Id)
	}

	attributionRows, err := s.MarketingDeliveryRepo.GetAttributionRowsByDeliveryProductIDs(c.Context(), deliveryIDs)
	if err != nil {
		return nil, 0, err
	}

	hppByDelivery := buildMarketingHppByDelivery(c.Context(), s.HppV2Svc, attributionRows)
	categoryByDelivery := buildMarketingCategoryByDelivery(deliveryProducts, attributionRows)

	items := dto.ToMarketingReportItems(deliveryProducts, hppByDelivery, categoryByDelivery, agingMap)
	return items, total, nil
}

func buildMarketingHppByDelivery(
	ctx context.Context,
	hppSvc approvalService.HppV2Service,
	attributionRows []commonRepo.MarketingDeliveryAttributionRow,
) map[uint]float64 {
	if len(attributionRows) == 0 {
		return map[uint]float64{}
	}

	hppByKandang := make(map[uint]float64)
	weightedByDelivery := make(map[uint]float64)
	totalQtyByDelivery := make(map[uint]float64)

	for _, row := range attributionRows {
		if row.MarketingDeliveryProductID == 0 || row.ProjectFlockKandangID == 0 || row.AllocatedQty <= 0 {
			continue
		}

		hppPerKg, exists := hppByKandang[row.ProjectFlockKandangID]
		if !exists {
			hppPerKg = 0
			if hppSvc != nil && utils.ProjectFlockCategory(row.ProjectFlockCategory) == utils.ProjectFlockCategoryLaying {
				if hppCost, err := hppSvc.CalculateHppCost(row.ProjectFlockKandangID, nil); err == nil && hppCost != nil {
					hppPerKg = hppCost.Real.HargaKg
				}
			}
			hppByKandang[row.ProjectFlockKandangID] = hppPerKg
		}

		weightedByDelivery[row.MarketingDeliveryProductID] += row.AllocatedQty * hppPerKg
		totalQtyByDelivery[row.MarketingDeliveryProductID] += row.AllocatedQty
	}

	result := make(map[uint]float64, len(totalQtyByDelivery))
	for deliveryID, totalQty := range totalQtyByDelivery {
		if totalQty <= 0 {
			continue
		}
		result[deliveryID] = weightedByDelivery[deliveryID] / totalQty
	}

	return result
}

func buildMarketingCategoryByDelivery(
	deliveryProducts []entity.MarketingDeliveryProduct,
	attributionRows []commonRepo.MarketingDeliveryAttributionRow,
) map[uint]string {
	result := make(map[uint]string, len(deliveryProducts))
	for _, row := range attributionRows {
		if row.MarketingDeliveryProductID == 0 || strings.TrimSpace(row.ProjectFlockCategory) == "" {
			continue
		}
		if _, exists := result[row.MarketingDeliveryProductID]; !exists {
			result[row.MarketingDeliveryProductID] = row.ProjectFlockCategory
		}
	}

	for _, delivery := range deliveryProducts {
		if _, exists := result[delivery.Id]; exists {
			continue
		}
		if delivery.AttributedProjectFlockKandang != nil {
			result[delivery.Id] = delivery.AttributedProjectFlockKandang.ProjectFlock.Category
			continue
		}
		if delivery.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
			result[delivery.Id] = delivery.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
		}
	}

	return result
}

func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

	const (
		recordsPerWeek     = 7
		defaultStdBw       = 1951
		defaultBw          = 0
		defaultUniformText = "90% up"
	)
	defaultStartWoa := config.LayingWeekStart()

	if params.Limit <= 0 {
		params.Limit = 10
	}
	if params.Page <= 0 {
		params.Page = 1
	}

	weeksPerPage := params.Limit
	recordLimit := weeksPerPage * recordsPerWeek
	if recordLimit <= 0 {
		recordLimit = recordsPerWeek
	}
	recordOffset := (params.Page - 1) * recordLimit
	if recordOffset < 0 {
		recordOffset = 0
	}

	recordings, totalRecordings, err := s.ProductionResultRepo.GetRecordingsByProjectFlockKandang(ctx.Context(), params.ProjectFlockKandangID, recordOffset, recordLimit)
	if err != nil {
		return nil, 0, err
	}

	dailyResults := make([]dto.ProductionResultDTO, len(recordings))
	for i := range recordings {
		dailyResults[i] = mapRecordingToProductionResultDTO(recordings[i])
		if dailyResults[i].StdUniformity == "" {
			dailyResults[i].StdUniformity = defaultUniformText
		}
	}

	weeklyResults := summarizeProductionResults(dailyResults, recordsPerWeek)

	var productionStandardID uint
	if s.ProductionResultRepo != nil {
		standardID, err := s.ProductionResultRepo.GetProductionStandardIDByProjectFlockKandangID(ctx.Context(), params.ProjectFlockKandangID)
		if err != nil {
			if !errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, 0, err
			}
		} else {
			productionStandardID = standardID
		}
	}

	standardDetailCache := make(map[int]*entity.ProductionStandardDetail)
	growthDetailCache := make(map[int]*entity.StandardGrowthDetail)

	weeks := make([]int, len(weeklyResults))
	for i := range weeklyResults {
		weeks[i] = defaultStartWoa + i
	}
	uniformityMap, err := s.getUniformityByWeek(ctx.Context(), params.ProjectFlockKandangID, weeks)
	if err != nil {
		return nil, 0, err
	}

	var cumulativeButir int64
	var cumulativeKg float64
	for i := range weeklyResults {
		weeklyResults[i].Woa = float64(defaultStartWoa + i)
		weeklyResults[i].StdBw = defaultStdBw
		weeklyResults[i].Bw = defaultBw
		if weeklyResults[i].StdUniformity == "" {
			weeklyResults[i].StdUniformity = defaultUniformText
		}
		if uniformity, ok := uniformityMap[defaultStartWoa+i]; ok {
			weeklyResults[i].Uniformity = uniformity.Uniformity
			if uniformity.AvgWeight != nil {
				weeklyResults[i].Bw = *uniformity.AvgWeight
			}
		}

		cumulativeButir += weeklyResults[i].ButiranJumlah
		weeklyResults[i].TotalButir = cumulativeButir

		cumulativeKg += weeklyResults[i].KgJumlah
		weeklyResults[i].TotalKg = cumulativeKg

		if productionStandardID == 0 {
			continue
		}

		week := int(weeklyResults[i].Woa)
		if s.ProductionStandardDetailRepo != nil {
			detail, ok := standardDetailCache[week]
			if !ok {
				fetched, fetchErr := s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(ctx.Context(), productionStandardID, week)
				if fetchErr != nil {
					if !errors.Is(fetchErr, gorm.ErrRecordNotFound) {
						return nil, 0, fetchErr
					}
				} else {
					detail = fetched
				}
				standardDetailCache[week] = detail
			}

			if detail != nil {
				if detail.TargetHenDayProduction != nil {
					weeklyResults[i].HdStd = *detail.TargetHenDayProduction
				}
				if detail.TargetHenHouseProduction != nil {
					weeklyResults[i].HhStd = *detail.TargetHenHouseProduction
				}
				if detail.TargetEggWeight != nil {
					weeklyResults[i].EwStd = *detail.TargetEggWeight
				}
				if detail.TargetEggMass != nil {
					weeklyResults[i].EmStd = *detail.TargetEggMass
				}
				if detail.StandardFCR != nil {
					weeklyResults[i].FcrStd = *detail.StandardFCR
				}
			}
		}

		if s.StandardGrowthDetailRepo != nil {
			detail, ok := growthDetailCache[week]
			if !ok {
				fetched, fetchErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx.Context(), productionStandardID, week)
				if fetchErr != nil {
					if !errors.Is(fetchErr, gorm.ErrRecordNotFound) {
						return nil, 0, fetchErr
					}
				} else {
					detail = fetched
				}
				growthDetailCache[week] = detail
			}

			if detail != nil && detail.FeedIntake != nil {
				weeklyResults[i].FiStd = *detail.FeedIntake
			}
			if detail != nil && detail.TargetMeanBw != nil {
				weeklyResults[i].StdBw = *detail.TargetMeanBw
			}
			if detail != nil {
				weeklyResults[i].DepStd = valueOrZero(detail.MaxDepletion)
			}
		}
	}

	totalWeeks := int64(math.Ceil(float64(totalRecordings) / float64(recordsPerWeek)))

	return weeklyResults, totalWeeks, nil
}

func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

	locationScope, err := m.ResolveLocationScope(ctx, s.DB())
	if err != nil {
		return nil, 0, err
	}
	areaScope, err := m.ResolveAreaScope(ctx, s.DB())
	if err != nil {
		return nil, 0, err
	}

	restrictScope := locationScope.Restrict || areaScope.Restrict
	var allowedCustomerIDs []uint
	if restrictScope {
		if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) {
			return []dto.CustomerPaymentReportItem{}, 0, nil
		}
		allowedCustomerIDs, err = s.getCustomerIDsByScope(ctx.Context(), locationScope.IDs, areaScope.IDs)
		if err != nil {
			return nil, 0, err
		}
		if len(allowedCustomerIDs) == 0 {
			return []dto.CustomerPaymentReportItem{}, 0, nil
		}
	}

	var customerIDs []uint
	var totalCustomers int64

	if len(params.CustomerIDs) > 0 {
		customerIDs = params.CustomerIDs
		if restrictScope {
			customerIDs = intersectUint(customerIDs, allowedCustomerIDs)
		}
		totalCustomers = int64(len(customerIDs))

		if len(customerIDs) == 0 {
			return []dto.CustomerPaymentReportItem{}, 0, nil
		}
	} else {
		page := params.Page
		limit := params.Limit
		if page < 1 {
			page = 1
		}
		if limit < 1 {
			limit = 10
		}

		offset := (page - 1) * limit

		var err error
		customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset, allowedCustomerIDs)
		if err != nil {
			return nil, 0, err
		}

		if len(customerIDs) == 0 {
			return []dto.CustomerPaymentReportItem{}, 0, nil
		}
	}

	var result []dto.CustomerPaymentReportItem
	for _, customerID := range customerIDs {
		item, err := s.processCustomerPayment(ctx.Context(), customerID, params)
		if err != nil {
			return nil, 0, err
		}

		if len(item.Rows) > 0 {
			result = append(result, item)
		}
	}

	totalCustomers = int64(len(result))
	return result, totalCustomers, nil
}

func (s *repportService) getCustomerIDsByScope(ctx context.Context, locationIDs, areaIDs []uint) ([]uint, error) {
	if len(locationIDs) == 0 && len(areaIDs) == 0 {
		return []uint{}, nil
	}

	db := s.db.WithContext(ctx).
		Table("customers c").
		Select("DISTINCT c.id").
		Joins("JOIN marketings m ON m.customer_id = c.id").
		Joins("JOIN marketing_products mp ON mp.marketing_id = m.id").
		Joins("JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id").
		Joins("JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Where("mdp.delivery_date IS NOT NULL").
		Where("m.deleted_at IS NULL").
		Where("c.deleted_at IS NULL")

	if len(locationIDs) > 0 {
		db = db.Where("w.location_id IN ?", locationIDs)
	}
	if len(areaIDs) > 0 {
		db = db.Where("w.area_id IN ?", areaIDs)
	}

	var customerIDs []uint
	if err := db.Pluck("c.id", &customerIDs).Error; err != nil {
		return nil, err
	}
	return customerIDs, nil
}

func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) {

	customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil)
	if err != nil {
		return dto.CustomerPaymentReportItem{}, err
	}

	initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID)
	if err != nil {
		return dto.CustomerPaymentReportItem{}, err
	}

	cid := customerID
	transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid)
	if err != nil {
		return dto.CustomerPaymentReportItem{}, err
	}

	rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions))
	runningBalance := initialBalance

	for i, tx := range transactions {

		previousBalance := runningBalance

		row := dto.ToCustomerPaymentReportRow(tx)

		if tx.TransactionType == "SALES" {
			runningBalance -= tx.TotalPrice
			status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
			row.Status = status

			if status == "LUNAS" {
				if paymentDate != nil {
					days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
					row.AgingDay = &days
				} else {
					days := 0
					row.AgingDay = &days
				}
			} else {
				days := int(time.Since(tx.TransDate).Hours() / 24)
				row.AgingDay = &days
			}
		} else if tx.TransactionType == "PAYMENT" {
			runningBalance += tx.PaymentAmount
			row.Status = ""
			row.AgingDay = nil
		}

		row.AccountsReceivable = runningBalance
		rows = append(rows, row)
	}

	if params.StartDate != "" || params.EndDate != "" {
		filteredRows := make([]dto.CustomerPaymentReportRow, 0, len(rows))
		location, err := time.LoadLocation("Asia/Jakarta")
		if err != nil {
			return dto.CustomerPaymentReportItem{}, err
		}

		filterBy := strings.ToUpper(strings.TrimSpace(params.FilterBy))
		if filterBy == "" {
			filterBy = utils.CustomerPaymentFilterByTransDate
		}

		var startDate, endDate *time.Time
		if params.StartDate != "" {
			parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location)
			if err != nil {
				return dto.CustomerPaymentReportItem{}, err
			}
			startDate = &parsed
		}
		if params.EndDate != "" {
			parsed, err := time.ParseInLocation("2006-01-02", params.EndDate, location)
			if err != nil {
				return dto.CustomerPaymentReportItem{}, err
			}
			endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location)
			endDate = &endOfDay
		}

		for _, row := range rows {
			var compareDate time.Time
			if filterBy == utils.CustomerPaymentFilterByRealizationDate {
				if row.DeliveryDate == nil {
					continue
				}
				compareDate = row.DeliveryDate.In(location)
			} else {
				compareDate = row.TransDate.In(location)
			}

			if startDate != nil && compareDate.Before(*startDate) {
				continue
			}
			if endDate != nil && compareDate.After(*endDate) {
				continue
			}
			filteredRows = append(filteredRows, row)
		}

		rows = filteredRows
	}

	summary := dto.ToCustomerPaymentReportSummary(rows, initialBalance)

	return dto.ToCustomerPaymentReportItem(*customer, initialBalance, rows, summary), nil
}

func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
	currentSales := transactions[currentIndex]

	if previousBalance >= currentSales.TotalPrice {
		type paymentAllocation struct {
			date     time.Time
			amount   float64
			consumed float64
		}
		allocations := []paymentAllocation{}
		runningBalance := 0.0

		for i := 0; i < currentIndex; i++ {
			if transactions[i].TransactionType == "PAYMENT" {
				allocations = append(allocations, paymentAllocation{
					date:     transactions[i].TransDate,
					amount:   transactions[i].PaymentAmount,
					consumed: 0,
				})
				runningBalance += transactions[i].PaymentAmount
			} else if transactions[i].TransactionType == "SALES" {
				salesAmount := transactions[i].TotalPrice
				remainingToConsume := salesAmount

				for j := range allocations {
					if remainingToConsume <= 0 {
						break
					}
					available := allocations[j].amount - allocations[j].consumed
					if available > 0 {
						consume := available
						if consume > remainingToConsume {
							consume = remainingToConsume
						}
						allocations[j].consumed += consume
						remainingToConsume -= consume
					}
				}
				runningBalance -= salesAmount
			}
		}

		amountNeeded := currentSales.TotalPrice
		for _, alloc := range allocations {
			available := alloc.amount - alloc.consumed
			if available > 0 {
				if amountNeeded <= available {
					return "LUNAS", &alloc.date
				} else {
					amountNeeded -= available
				}
			}
		}

		if len(allocations) > 0 {
			return "LUNAS", &allocations[0].date
		}
		return "LUNAS", nil
	}

	hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice

	futureBalance := currentBalance
	hasPayment := false
	var paymentDateThatMadeItLunas *time.Time

	for i := currentIndex + 1; i < len(transactions); i++ {
		if transactions[i].TransactionType == "PAYMENT" {
			futureBalance += transactions[i].PaymentAmount
			hasPayment = true

			if futureBalance >= 0 {
				paymentDateThatMadeItLunas = &transactions[i].TransDate
				return "LUNAS", paymentDateThatMadeItLunas
			}
		} else if transactions[i].TransactionType == "SALES" {
			futureBalance -= transactions[i].TotalPrice
		}
	}

	if hasPayment || hasPartialPaymentFromBalance {
		return "DIBAYAR SEBAGIAN", nil
	}

	return "BELUM LUNAS", nil
}

func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
	result := dto.ProductionResultDTO{
		CreatedAt:     record.CreatedAt,
		UpdatedAt:     record.UpdatedAt,
		StdUniformity: "90% up",
		DepKum:        valueOrZero(record.CumDepletionRate),
		DepStd:        valueOrZero(record.TotalDepletionQty),
		Hd:            valueOrZero(record.HenDay),
		Fi:            valueOrZero(record.FeedIntake),
		Fcr:           valueOrZero(record.FcrValue),
		Hh:            valueOrZero(record.HenHouse),
		Em:            valueOrZero(record.EggMass),
		Ew:            valueOrZero(record.EggWeight),
	}

	if record.Day != nil {
		result.Woa = float64(*record.Day)
	}
	avgWeight := 1.0
	if avgWeight > 0 {
		result.Bw = avgWeight
	}

	eggSummary := summarizeEggs(record.Eggs)
	result.ButiranUtuh = eggSummary.Utuh
	result.ButiranPutih = eggSummary.Putih
	result.ButiranRetak = eggSummary.Retak
	result.ButiranPecah = eggSummary.Pecah
	result.ButiranJumlah = eggSummary.TotalQty
	result.TotalButir = eggSummary.TotalQty
	result.KgUtuh = eggSummary.KgUtuh
	result.KgPutih = eggSummary.KgPutih
	result.KgRetak = eggSummary.KgRetak
	result.KgPecah = eggSummary.KgPecah
	result.KgJumlah = eggSummary.TotalKg
	result.TotalKg = eggSummary.TotalKg

	if eggSummary.TotalQty > 0 {
		total := float64(eggSummary.TotalQty)
		result.PersenUtuh = roundFloat((float64(result.ButiranUtuh)/total)*100, 2)
		result.PersenPutih = roundFloat((float64(result.ButiranPutih)/total)*100, 2)
		result.PersenRetak = roundFloat((float64(result.ButiranRetak)/total)*100, 2)
		result.PersenPecah = roundFloat((float64(result.ButiranPecah)/total)*100, 2)
	}

	return result
}

// func calculateAverageBodyWeight(bodyWeights []entity.RecordingBW) float64 {
// 	var totalQty float64
// 	var totalWeight float64

// 	for _, bw := range bodyWeights {
// 		totalQty += bw.Qty
// 		if bw.TotalWeight > 0 {
// 			totalWeight += bw.TotalWeight
// 		} else {
// 			totalWeight += bw.AvgWeight * bw.Qty
// 		}
// 	}

// 	if totalQty == 0 {
// 		return 0
// 	}

// 	return totalWeight / totalQty
// }

type eggSummary struct {
	TotalQty int64
	TotalKg  float64

	Utuh  int64
	Putih int64
	Retak int64
	Pecah int64

	KgUtuh  float64
	KgPutih float64
	KgRetak float64
	KgPecah float64
}

func summarizeEggs(eggs []entity.RecordingEgg) eggSummary {
	var summary eggSummary

	for _, egg := range eggs {
		qty := int64(egg.Qty)
		weightKg := valueOrZero(egg.Weight)

		summary.TotalQty += qty
		summary.TotalKg += weightKg

		if flagType, ok := getEggFlagType(egg); ok {
			switch flagType {
			case utils.FlagTelurUtuh:
				summary.Utuh += qty
				summary.KgUtuh += weightKg
			case utils.FlagTelurPutih:
				summary.Putih += qty
				summary.KgPutih += weightKg
			case utils.FlagTelurRetak:
				summary.Retak += qty
				summary.KgRetak += weightKg
			case utils.FlagTelurPecah:
				summary.Pecah += qty
				summary.KgPecah += weightKg
			}
		}
	}

	return summary
}

func valueOrZero(value *float64) float64 {
	if value == nil {
		return 0
	}
	return *value
}

func roundFloat(val float64, precision int) float64 {
	if precision < 0 {
		return val
	}
	factor := math.Pow(10, float64(precision))
	return math.Round(val*factor) / factor
}

func getEggFlagType(egg entity.RecordingEgg) (utils.FlagType, bool) {
	if egg.ProductFlagName == nil || *egg.ProductFlagName == "" {
		return "", false
	}

	flagType := utils.FlagType(*egg.ProductFlagName)
	switch flagType {
	case utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah:
		return flagType, true
	}

	return "", false
}

type uniformityWeekData struct {
	Uniformity float64
	AvgWeight  *float64
}

type uniformityChartPayload struct {
	Statistics *uniformityChartStats `json:"statistics"`
}

type uniformityChartStats struct {
	AverageWeight *float64 `json:"average_weight"`
}

func (s *repportService) getUniformityByWeek(ctx context.Context, projectFlockKandangID uint, weeks []int) (map[int]uniformityWeekData, error) {
	result := make(map[int]uniformityWeekData, len(weeks))
	if projectFlockKandangID == 0 || len(weeks) == 0 {
		return result, nil
	}

	weekExpr := fmt.Sprintf(`CASE
		WHEN u.uniform_date IS NULL OR pc.chick_in_date IS NULL THEN 0
		WHEN u.uniform_date::date < pc.chick_in_date THEN 0
		WHEN UPPER(pf.category) = 'LAYING' THEN (((u.uniform_date::date - pc.chick_in_date)::int) / 7) + %d
		ELSE (((u.uniform_date::date - pc.chick_in_date)::int) / 7) + 1
	END`, config.LayingWeekStart())

	var rows []entity.ProjectFlockKandangUniformity
	if err := s.db.WithContext(ctx).
		Table("project_flock_kandang_uniformity AS u").
		Select(fmt.Sprintf("%s AS week, u.uniformity, u.uniform_date, u.id, u.chart_data", weekExpr)).
		Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = u.project_flock_kandang_id").
		Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
		Joins(`JOIN (
			SELECT project_flock_kandang_id, MIN(chick_in_date)::date AS chick_in_date
			FROM project_chickins
			WHERE deleted_at IS NULL
			GROUP BY project_flock_kandang_id
		) AS pc ON pc.project_flock_kandang_id = u.project_flock_kandang_id`).
		Where("u.project_flock_kandang_id = ?", projectFlockKandangID).
		Where(fmt.Sprintf("%s IN ?", weekExpr), weeks).
		Order("uniform_date DESC").
		Order("id DESC").
		Find(&rows).Error; err != nil {
		return nil, err
	}

	for _, row := range rows {
		if _, exists := result[row.Week]; exists {
			continue
		}
		result[row.Week] = uniformityWeekData{
			Uniformity: row.Uniformity,
			AvgWeight:  extractAverageWeight(row.ChartData, s.Log),
		}
	}

	return result, nil
}

func extractAverageWeight(raw json.RawMessage, log *logrus.Logger) *float64 {
	if len(raw) == 0 {
		return nil
	}

	var payload uniformityChartPayload
	if err := json.Unmarshal(raw, &payload); err != nil {
		if log != nil {
			log.WithError(err).Warn("uniformity chart_data decode failed")
		}
		return nil
	}
	if payload.Statistics == nil {
		return nil
	}
	return payload.Statistics.AverageWeight
}

func summarizeProductionResults(daily []dto.ProductionResultDTO, groupSize int) []dto.ProductionResultDTO {
	if groupSize <= 0 || len(daily) == 0 {
		return daily
	}

	result := make([]dto.ProductionResultDTO, 0, (len(daily)+groupSize-1)/groupSize)
	for i := 0; i < len(daily); i += groupSize {
		end := i + groupSize
		if end > len(daily) {
			end = len(daily)
		}
		result = append(result, aggregateProductionResultGroup(daily[i:end], groupSize))
	}

	return result
}

func aggregateProductionResultGroup(group []dto.ProductionResultDTO, groupSize int) dto.ProductionResultDTO {
	count := len(group)
	if count == 0 {
		return dto.ProductionResultDTO{}
	}

	agg := dto.ProductionResultDTO{
		CreatedAt:     group[0].CreatedAt,
		UpdatedAt:     group[0].UpdatedAt,
		StdUniformity: group[0].StdUniformity,
	}

	var sumBw, sumStdBw, sumUniformity float64
	var sumDepStd float64
	var sumKgUtuh, sumKgPutih, sumKgRetak, sumKgPecah float64
	var sumKgJumlah, sumTotalKg float64
	var sumPersenUtuh, sumPersenPutih, sumPersenRetak, sumPersenPecah float64
	var percentSamples int
	var sumHd, sumHdStd float64
	var sumFi, sumFiStd float64
	var sumEm, sumEmStd float64
	var sumEw, sumEwStd float64
	var sumFcr, sumFcrStd float64
	var sumHh, sumHhStd float64

	var sumButiranUtuh, sumButiranPutih int64
	var sumButiranRetak, sumButiranPecah int64
	var sumButiranJumlah, sumTotalButir int64

	for _, item := range group {
		sumBw += item.Bw
		sumStdBw += item.StdBw
		sumUniformity += item.Uniformity
		sumDepStd += item.DepStd
		sumKgUtuh += item.KgUtuh
		sumKgPutih += item.KgPutih
		sumKgRetak += item.KgRetak
		sumKgPecah += item.KgPecah
		sumKgJumlah += item.KgJumlah
		sumTotalKg += item.TotalKg
		if item.ButiranJumlah > 0 {
			sumPersenUtuh += item.PersenUtuh
			sumPersenPutih += item.PersenPutih
			sumPersenRetak += item.PersenRetak
			sumPersenPecah += item.PersenPecah
			percentSamples++
		}
		sumHd += item.Hd
		sumHdStd += item.HdStd
		sumFi += item.Fi
		sumFiStd += item.FiStd
		sumEm += item.Em
		sumEmStd += item.EmStd
		sumEw += item.Ew
		sumEwStd += item.EwStd
		sumFcr += item.Fcr
		sumFcrStd += item.FcrStd
		sumHh += item.Hh
		sumHhStd += item.HhStd

		sumButiranUtuh += item.ButiranUtuh
		sumButiranPutih += item.ButiranPutih
		sumButiranRetak += item.ButiranRetak
		sumButiranPecah += item.ButiranPecah
		sumButiranJumlah += item.ButiranJumlah
		sumTotalButir += item.TotalButir
	}

	divider := float64(count)
	if divider == 0 {
		divider = 1
	}
	weeklyDivider := float64(groupSize)
	if weeklyDivider == 0 {
		weeklyDivider = divider
	}

	agg.Bw = sumBw / divider
	agg.StdBw = sumStdBw / divider
	agg.Uniformity = sumUniformity / divider
	agg.DepKum = group[count-1].DepKum
	agg.DepStd = sumDepStd / divider
	agg.KgUtuh = sumKgUtuh
	agg.KgPutih = sumKgPutih
	agg.KgRetak = sumKgRetak
	agg.KgPecah = sumKgPecah
	agg.KgJumlah = sumKgJumlah
	agg.TotalKg = sumTotalKg

	agg.ButiranUtuh = sumButiranUtuh
	agg.ButiranPutih = sumButiranPutih
	agg.ButiranRetak = sumButiranRetak
	agg.ButiranPecah = sumButiranPecah
	agg.ButiranJumlah = sumButiranJumlah
	agg.TotalButir = sumTotalButir

	if percentSamples > 0 {
		percentDivider := float64(percentSamples)
		agg.PersenUtuh = roundFloat(sumPersenUtuh/percentDivider, 2)
		agg.PersenPutih = roundFloat(sumPersenPutih/percentDivider, 2)
		agg.PersenRetak = roundFloat(sumPersenRetak/percentDivider, 2)
		agg.PersenPecah = roundFloat(sumPersenPecah/percentDivider, 2)
	}

	agg.Hd = roundFloat(sumHd/weeklyDivider, 2)
	agg.HdStd = sumHdStd / divider
	agg.Fi = roundFloat(sumFi/weeklyDivider, 2)
	agg.FiStd = sumFiStd / divider
	agg.Em = group[count-1].Em
	agg.EmStd = sumEmStd / divider
	agg.Ew = group[count-1].Ew
	agg.EwStd = sumEwStd / divider
	agg.Fcr = roundFloat(sumFcr/weeklyDivider, 2)
	agg.FcrStd = sumFcrStd / divider
	agg.Hh = roundFloat(sumHh/weeklyDivider, 2)
	agg.HhStd = sumHhStd / divider

	return agg
}

func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

	offset := (params.Page - 1) * params.Limit
	if offset < 0 {
		offset = 0
	}

	suppliers, totalSuppliers, err := s.PurchaseSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params)
	if err != nil {
		return nil, 0, err
	}

	if totalSuppliers == 0 || len(suppliers) == 0 {
		return []dto.PurchaseSupplierDTO{}, totalSuppliers, nil
	}

	supplierMap := make(map[uint]entity.Supplier, len(suppliers))
	supplierIDs := make([]uint, 0, len(suppliers))
	for _, supplier := range suppliers {
		supplierMap[supplier.Id] = supplier
		supplierIDs = append(supplierIDs, supplier.Id)
	}

	items, err := s.PurchaseSupplierRepo.GetItemsBySuppliers(c.Context(), supplierIDs, params)
	if err != nil {
		return nil, 0, err
	}

	itemsBySupplier := make(map[uint][]entity.PurchaseItem)
	for _, item := range items {
		if item.Purchase == nil {
			continue
		}
		supplierID := item.Purchase.SupplierId
		itemsBySupplier[supplierID] = append(itemsBySupplier[supplierID], item)
	}

	result := make([]dto.PurchaseSupplierDTO, 0, len(supplierIDs))
	for _, supplierID := range supplierIDs {
		supplier, exists := supplierMap[supplierID]
		if !exists {
			continue
		}

		supplierItems := itemsBySupplier[supplierID]
		dtoItem := dto.ToPurchaseSupplierDTO(supplier, supplierItems)
		result = append(result, dtoItem)
	}

	return result, totalSuppliers, nil
}

func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
	if params.FilterBy == "" {
		params.FilterBy = "received_date"
	}

	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

	offset := (params.Page - 1) * params.Limit
	if offset < 0 {
		offset = 0
	}

	suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params)
	if err != nil {
		return nil, 0, err
	}
	if totalSuppliers == 0 || len(suppliers) == 0 {
		return []dto.DebtSupplierDTO{}, totalSuppliers, nil
	}

	supplierMap := make(map[uint]entity.Supplier, len(suppliers))
	supplierIDs := make([]uint, 0, len(suppliers))
	for _, supplier := range suppliers {
		supplierMap[supplier.Id] = supplier
		supplierIDs = append(supplierIDs, supplier.Id)
	}

	purchases, err := s.DebtSupplierRepo.GetPurchasesBySuppliers(c.Context(), supplierIDs, params)
	if err != nil {
		return nil, 0, err
	}

	payments, err := s.DebtSupplierRepo.GetPaymentsBySuppliers(c.Context(), supplierIDs, params)
	if err != nil {
		return nil, 0, err
	}

	purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
	for _, purchase := range purchases {
		purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase)
	}

	paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
	for _, payment := range payments {
		paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
	}

	initialPurchaseTotals, err := s.DebtSupplierRepo.GetPurchaseTotalsBeforeDate(c.Context(), supplierIDs, params)
	if err != nil {
		return nil, 0, err
	}

	initialPaymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsBeforeDate(c.Context(), supplierIDs, params)
	if err != nil {
		return nil, 0, err
	}

	initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs)
	if err != nil {
		return nil, 0, err
	}

	location, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
	}
	now := time.Now().In(location)

	result := make([]dto.DebtSupplierDTO, 0, len(supplierIDs))
	type debtSupplierRowItem struct {
		Row          dto.DebtSupplierRowDTO
		SortTime     time.Time
		Order        int
		DeltaBalance float64
		CountTotals  bool
	}
	type debtSupplierAllocation struct {
		RowIndex int
		SortTime time.Time
		Amount   float64
		Purchase entity.Purchase
	}
	type paymentAllocation struct {
		Date   time.Time
		Amount float64
	}

	for _, supplierID := range supplierIDs {
		supplier, exists := supplierMap[supplierID]
		if !exists {
			continue
		}

		initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
		items := purchasesBySupplier[supplierID]
		paymentItems := paymentsBySupplier[supplierID]
		total := dto.DebtSupplierTotalDTO{}

		combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
		purchaseAllocations := make([]debtSupplierAllocation, 0, len(items))
		for _, purchase := range items {
			row := buildDebtSupplierRow(purchase, now, location)
			sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
			rowIndex := len(combinedRows)
			combinedRows = append(combinedRows, debtSupplierRowItem{
				Row:          row,
				SortTime:     sortTime,
				Order:        0,
				DeltaBalance: -row.TotalPrice,
				CountTotals:  true,
			})
			purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
				RowIndex: rowIndex,
				SortTime: sortTime,
				Amount:   row.TotalPrice,
				Purchase: purchase,
			})
		}

		paymentAllocations := make([]paymentAllocation, 0, len(paymentItems)+1)
		initialAllocation := initialBalanceTotals[supplierID] + initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
		paymentCarry := 0.0
		if initialAllocation > 0 && len(purchaseAllocations) > 0 {
			paymentAllocations = append(paymentAllocations, paymentAllocation{
				Date:   purchaseAllocations[0].SortTime,
				Amount: initialAllocation,
			})
		} else if initialAllocation < 0 {
			paymentCarry = -initialAllocation
		}

		for _, payment := range paymentItems {
			row := buildDebtSupplierPaymentRow(payment, location)
			sortTime := payment.PaymentDate.In(location)
			combinedRows = append(combinedRows, debtSupplierRowItem{
				Row:          row,
				SortTime:     sortTime,
				Order:        1,
				DeltaBalance: payment.Nominal,
				CountTotals:  false,
			})
			paymentAllocations = append(paymentAllocations, paymentAllocation{
				Date:   sortTime,
				Amount: payment.Nominal,
			})
		}

		if len(purchaseAllocations) > 0 && len(paymentAllocations) > 0 {
			sort.SliceStable(purchaseAllocations, func(i, j int) bool {
				return purchaseAllocations[i].SortTime.Before(purchaseAllocations[j].SortTime)
			})
			sort.SliceStable(paymentAllocations, func(i, j int) bool {
				return paymentAllocations[i].Date.Before(paymentAllocations[j].Date)
			})
			remaining := make([]float64, len(purchaseAllocations))
			for i := range purchaseAllocations {
				remaining[i] = purchaseAllocations[i].Amount
			}
			purchaseIndex := 0
			for _, pay := range paymentAllocations {
				amount := pay.Amount
				if amount <= 0 {
					continue
				}
				if paymentCarry > 0 {
					used := math.Min(amount, paymentCarry)
					paymentCarry -= used
					amount -= used
				}
				for amount > 0 && purchaseIndex < len(remaining) {
					if remaining[purchaseIndex] <= 0 {
						purchaseIndex++
						continue
					}
					used := math.Min(amount, remaining[purchaseIndex])
					remaining[purchaseIndex] -= used
					amount -= used
					if remaining[purchaseIndex] <= 0.000001 {
						allocation := purchaseAllocations[purchaseIndex]
						combinedRows[allocation.RowIndex].Row.Status = "Lunas"
						combinedRows[allocation.RowIndex].Row.Aging = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location)
						purchaseIndex++
					}
				}
				if purchaseIndex >= len(remaining) {
					break
				}
			}
		}

		sort.SliceStable(combinedRows, func(i, j int) bool {
			if combinedRows[i].SortTime.Equal(combinedRows[j].SortTime) {
				return combinedRows[i].Order < combinedRows[j].Order
			}
			return combinedRows[i].SortTime.Before(combinedRows[j].SortTime)
		})

		balance := initialBalance
		for i := range combinedRows {
			balance += combinedRows[i].DeltaBalance
			combinedRows[i].Row.DebtPrice = balance
			combinedRows[i].Row.Balance = balance

			if combinedRows[i].CountTotals {
				row := combinedRows[i].Row
				if row.Aging > total.Aging {
					total.Aging = row.Aging
				}
				total.TotalPrice += row.TotalPrice
			} else {
				total.PaymentPrice += combinedRows[i].Row.PaymentPrice
			}
		}
		total.DebtPrice = balance

		rows := make([]dto.DebtSupplierRowDTO, 0, len(combinedRows))
		sortDesc := strings.EqualFold(params.SortOrder, "desc")
		if sortDesc {
			for i := len(combinedRows) - 1; i >= 0; i-- {
				rows = append(rows, combinedRows[i].Row)
			}
		} else {
			for i := range combinedRows {
				rows = append(rows, combinedRows[i].Row)
			}
		}

		var supplierDTORef *supplierDTO.SupplierRelationDTO
		if supplier.Id != 0 {
			mapped := supplierDTO.ToSupplierRelationDTO(supplier)
			supplierDTORef = &mapped
		}

		result = append(result, dto.DebtSupplierDTO{
			Supplier:       supplierDTORef,
			InitialBalance: initialBalance,
			Rows:           rows,
			Total:          total,
		})
	}

	return result, totalSuppliers, nil
}

func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO {
	prNumber := purchase.PrNumber
	poNumber := ""
	if purchase.PoNumber != nil {
		poNumber = *purchase.PoNumber
	}

	startDate := resolveDebtSupplierReceivedDate(purchase, loc)
	endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
	aging := 0
	if !startDate.IsZero() {
		aging = int(endDate.Sub(startDate).Hours() / 24)
	}

	totalPrice := 0.0
	travelNumber := "-"
	receivedDate := ""
	var area *areaDTO.AreaRelationDTO
	var warehouse *warehouseDTO.WarehouseRelationDTO

	if len(purchase.Items) > 0 {
		firstItem := purchase.Items[0]
		if firstItem.TravelNumber != nil && strings.TrimSpace(*firstItem.TravelNumber) != "" {
			travelNumber = *firstItem.TravelNumber
		}

		if firstItem.Warehouse != nil && firstItem.Warehouse.Id != 0 {
			mappedWarehouse := warehouseDTO.ToWarehouseRelationDTO(*firstItem.Warehouse)
			warehouse = &mappedWarehouse
			if firstItem.Warehouse.Area.Id != 0 {
				mappedArea := areaDTO.ToAreaRelationDTO(firstItem.Warehouse.Area)
				area = &mappedArea
			}
		}

		earliestReceived := time.Time{}
		for _, item := range purchase.Items {
			totalPrice += item.TotalPrice
			if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
				continue
			}
			received := item.ReceivedDate.In(loc)
			if earliestReceived.IsZero() || received.Before(earliestReceived) {
				earliestReceived = received
			}
		}
		if !earliestReceived.IsZero() {
			receivedDate = earliestReceived.Format("2006-01-02")
		}
	}

	dueDate := ""
	dueStatus := "-"
	if purchase.DueDate != nil && !purchase.DueDate.IsZero() {
		due := purchase.DueDate.In(loc)
		dueDate = due.Format("2006-01-02")
		if now.After(due) {
			dueStatus = "Sudah Jatuh Tempo"
		} else {
			dueStatus = "Mendekati Jatuh Tempo"
		}
	}

	status := "Belum Lunas"
	poDate := ""
	if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
		poDate = purchase.PoDate.In(loc).Format("2006-01-02")
	}

	return dto.DebtSupplierRowDTO{
		PrNumber:     prNumber,
		PoNumber:     poNumber,
		PoDate:       poDate,
		ReceivedDate: receivedDate,
		Aging:        aging,
		Area:         area,
		Warehouse:    warehouse,
		DueDate:      dueDate,
		DueStatus:    dueStatus,
		TotalPrice:   totalPrice,
		PaymentPrice: 0,
		DebtPrice:    0,
		Status:       status,
		TravelNumber: travelNumber,
		Balance:      0,
	}
}

func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto.DebtSupplierRowDTO {
	referenceNumber := ""
	if payment.ReferenceNumber != nil {
		referenceNumber = *payment.ReferenceNumber
	}

	prNumber := payment.PaymentCode
	if strings.TrimSpace(prNumber) == "" {
		prNumber = referenceNumber
	}

	return dto.DebtSupplierRowDTO{
		PrNumber:     prNumber,
		PoNumber:     referenceNumber,
		PoDate:       "-",
		ReceivedDate: payment.PaymentDate.In(loc).Format("2006-01-02"),
		Aging:        0,
		Area:         nil,
		Warehouse:    nil,
		DueDate:      "-",
		DueStatus:    "-",
		TotalPrice:   0,
		PaymentPrice: payment.Nominal,
		DebtPrice:    0,
		Status:       "Pembayaran",
		TravelNumber: "-",
		Balance:      0,
	}
}

func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
	if strings.EqualFold(strings.TrimSpace(filterBy), "po_date") {
		if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
			return purchase.PoDate.In(loc)
		}
	}

	earliest := time.Time{}
	for _, item := range purchase.Items {
		if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
			continue
		}
		received := item.ReceivedDate.In(loc)
		if earliest.IsZero() || received.Before(earliest) {
			earliest = received
		}
	}
	if !earliest.IsZero() {
		return earliest
	}

	return purchase.CreatedAt.In(loc)
}

func collectDebtSupplierReferences(purchases []entity.Purchase) []string {
	if len(purchases) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(purchases))
	result := make([]string, 0, len(purchases))
	for _, purchase := range purchases {
		ref := resolveDebtSupplierReference(purchase)
		if ref == "" {
			continue
		}
		if _, ok := seen[ref]; ok {
			continue
		}
		seen[ref] = struct{}{}
		result = append(result, ref)
	}
	return result
}

func resolveDebtSupplierReference(purchase entity.Purchase) string {
	if purchase.PoNumber != nil {
		if ref := strings.TrimSpace(*purchase.PoNumber); ref != "" {
			return ref
		}
	}
	if ref := strings.TrimSpace(purchase.PrNumber); ref != "" {
		return ref
	}
	return ""
}

func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool {
	if totalPrice <= 0 {
		return true
	}
	return paymentTotal >= totalPrice-0.000001
}

func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int {
	startDate := resolveDebtSupplierReceivedDate(purchase, loc)
	if startDate.IsZero() {
		return 0
	}
	stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
	if stopDate.Before(startDate) {
		return 0
	}
	return int(stopDate.Sub(startDate).Hours() / 24)
}

func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Location) time.Time {
	earliest := time.Time{}
	for _, item := range purchase.Items {
		if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
			continue
		}
		received := item.ReceivedDate.In(loc)
		if earliest.IsZero() || received.Before(earliest) {
			earliest = received
		}
	}
	if earliest.IsZero() {
		return time.Time{}
	}
	return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc)
}

func (s *repportService) GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	if s.HppV2Svc == nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "hpp v2 service is not configured")
	}

	periodDate, err := time.ParseInLocation("2006-01-02", params.Period, time.FixedZone("Asia/Jakarta", 7*60*60))
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD")
	}

	result, err := s.HppV2Svc.CalculateHppBreakdown(params.ProjectFlockKandangID, &periodDate)
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
	params, filters, err := s.parseHppPerKandangQuery(ctx)
	if err != nil {
		return nil, nil, err
	}

	if err := s.Validate.Struct(params); err != nil {
		return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}

	location, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
	}

	periodDate, err := time.ParseInLocation("2006-01-02", params.Period, location)
	if err != nil {
		return nil, nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD")
	}

	startOfDay := time.Date(periodDate.Year(), periodDate.Month(), periodDate.Day(), 0, 0, 0, 0, location)
	endOfDay := startOfDay.Add(24 * time.Hour)

	repoRows, err := s.HppPerKandangRepo.GetRowsByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs)
	if err != nil {
		return nil, nil, err
	}

	validPfkIDs := make([]uint, 0, len(repoRows))
	pfkIndex := make(map[uint]int, len(repoRows))
	for idx := range repoRows {
		row := repoRows[idx]
		pfkIndex[row.ProjectFlockKandangID] = idx
		if row.RecordingCount > 0 {
			validPfkIDs = append(validPfkIDs, row.ProjectFlockKandangID)
		}
	}

	costRows := make([]repportRepo.HppPerKandangCostRow, 0)
	supplierRows := make([]repportRepo.HppPerKandangSupplierRow, 0)
	if len(validPfkIDs) > 0 {
		costRows, supplierRows, err = s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
		if err != nil {
			return nil, nil, err
		}

		// eggMap, err := s.HppPerKandangRepo.GetWeightRemainingByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
		// if err != nil {
		// 	return nil, nil, err
		// }
		// for pfkID, egg := range eggMap {
		// 	if rowIdx, ok := pfkIndex[pfkID]; ok {
		// 		repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
		// 		repoRows[rowIdx].AverageWeightEggPerPiece = egg.AverageWeightEggPerPiece
		// 	}
		// }
	}

	costMap := make(map[uint]HppCostAggregate, len(costRows))
	for _, row := range costRows {
		costMap[row.ProjectFlockKandangID] = HppCostAggregate{
			// FeedCost:    row.FeedCost,
			// OvkCost:     row.OvkCost,
			DocCost: row.DocCost,
			DocQty:  row.DocQty,
			// BudgetCost:  row.BudgetCost,
			// ExpenseCost: row.ExpenseCost,
		}
	}

	docSupplierMap := make(map[uint][]dto.HppPerKandangSupplierDTO)
	feedSupplierMap := make(map[uint][]dto.HppPerKandangSupplierDTO)
	docSeen := make(map[uint]map[uint]bool)
	feedSeen := make(map[uint]map[uint]bool)

	for _, sup := range supplierRows {
		if sup.SupplierID == 0 {
			continue
		}

		targetMap := feedSupplierMap
		seen := feedSeen
		category := "FEED"
		if strings.EqualFold(sup.Category, "DOC") {
			targetMap = docSupplierMap
			seen = docSeen
			category = "DOC"
		}

		if seen[sup.ProjectFlockKandangID] == nil {
			seen[sup.ProjectFlockKandangID] = make(map[uint]bool)
		}
		if seen[sup.ProjectFlockKandangID][sup.SupplierID] {
			continue
		}
		seen[sup.ProjectFlockKandangID][sup.SupplierID] = true

		targetMap[sup.ProjectFlockKandangID] = append(targetMap[sup.ProjectFlockKandangID], dto.HppPerKandangSupplierDTO{
			ID:       int64(sup.SupplierID),
			Name:     sup.SupplierName,
			Alias:    sup.SupplierAlias,
			Category: category,
		})
	}

	type weightRangeKey struct {
		Min float64
		Max float64
	}
	type weightRangeAggregate struct {
		Summary           *dto.HppPerKandangSummaryWeightRangeDTO
		RemainingBirds    int64
		RemainingWeightKg float64
		AvgWeightSum      float64
		AvgWeightCount    int64
		EggHppSum         float64
		EggHppCount       int
		FeedSuppliers     map[int64]dto.HppPerKandangSupplierDTO
		DocSuppliers      map[int64]dto.HppPerKandangSupplierDTO
	}

	dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
	perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
	var totalBirds int64
	var totalEggPieces int64
	var totalEggKg float64
	var totalEggValueRp int64
	var totalHppCount int
	var totalDocPriceSum float64
	var totalDocPriceCount int
	var totalEggHppSum float64
	var totalEggHppCount int
	var totalAvgWeightSum float64
	var totalAvgWeightCount int64

	for _, row := range repoRows {
		if !params.ShowUnrecorded && row.RecordingCount == 0 {
			continue
		}

		var eggPiecesFloatRemaining float64
		var eggRemainingWeightFloatRemaining float64
		var eggTotalPiecesFloat float64
		var eggWeightFloat float64
		var avgWeight float64
		eggHpp := 0.0
		if s.HppV2Svc != nil {
			hppCost, err := s.HppV2Svc.CalculateHppCost(row.ProjectFlockKandangID, &periodDate)
			if err != nil {
				return nil, nil, err
			}
			if hppCost != nil {
				eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir
				eggHpp = hppCost.Estimation.HargaKg
				// eggHpp = hppCost.Real.HargaKg
				eggTotalPiecesFloat = hppCost.Estimation.Butir
				eggWeightFloat = hppCost.Estimation.Kg
				if eggTotalPiecesFloat > 0 {
					avgWeight = eggWeightFloat / eggTotalPiecesFloat
				}
				// eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining
				eggRemainingWeightFloatRemaining = hppCost.Estimation.Kg - hppCost.Real.Kg
			}
		}
		if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
			eggPiecesFloatRemaining = 0
		}
		if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) {
			eggTotalPiecesFloat = 0
		}
		if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
			eggRemainingWeightFloatRemaining = 0
		}
		if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
			eggWeightFloat = 0
		}
		if math.IsNaN(avgWeight) || math.IsInf(avgWeight, 0) {
			avgWeight = 0
		}

		if params.WeightMin != nil && avgWeight < *params.WeightMin {
			continue
		}
		if params.WeightMax != nil && avgWeight > *params.WeightMax {
			continue
		}

		weightMin := math.Floor(avgWeight*10) / 10
		if weightMin < 0 {
			weightMin = 0
		}
		weightMax := weightMin + 0.09
		rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}

		costEntry := costMap[row.ProjectFlockKandangID]

		rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
		rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
		avgDocPrice := int64(0)
		if costEntry.DocQty > 0 {
			avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
		}

		nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod)

		dataRows = append(dataRows, dto.HppPerKandangRowDTO{
			ID: int(row.ProjectFlockKandangID),
			Kandang: dto.HppPerKandangRowKandangDTO{
				ID:     int64(row.KandangID),
				Name:   row.KandangName,
				Status: row.KandangStatus,
				Location: dto.HppPerKandangLocationDTO{
					ID:   int64(row.LocationID),
					Name: row.LocationName,
				},
				Pic: dto.HppPerKandangPICDTO{
					ID:   int64(row.PicID),
					Name: row.PicName,
				},
			},
			WeightRange: dto.HppPerKandangWeightRangeDTO{
				WeightMin: weightMin,
				WeightMax: weightMax,
			},
			AvgWeightKg:         avgWeight,
			NameWithPeriode:     nameWithPeriod,
			DocSuppliers:        docSupplierMap[row.ProjectFlockKandangID],
			FeedSuppliers:       feedSupplierMap[row.ProjectFlockKandangID],
			EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
			EggProductionKg:     eggRemainingWeightFloatRemaining,
			AverageDocPriceRp:   avgDocPrice,
			EggHppRpPerKg:       eggHpp,
			EggValueRp:          rowEggValue,
		})

		totalEggPieces += rowEggPieces
		totalEggKg += eggRemainingWeightFloatRemaining
		totalEggValueRp += rowEggValue
		totalAvgWeightSum += avgWeight
		totalAvgWeightCount++
		if avgDocPrice > 0 {
			totalDocPriceSum += float64(avgDocPrice)
			totalDocPriceCount++
		}
		if eggWeightFloat > 0 {
			totalEggHppSum += eggHpp
			totalEggHppCount++
		}

		rangeAgg, exists := perRangeMap[rangeKey]
		if !exists {
			rangeAgg = &weightRangeAggregate{
				Summary: &dto.HppPerKandangSummaryWeightRangeDTO{
					WeightRange: dto.HppPerKandangWeightRangeDTO{
						WeightMin: weightMin,
						WeightMax: weightMax,
					},
					Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax),
				},
				FeedSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO),
				DocSuppliers:  make(map[int64]dto.HppPerKandangSupplierDTO),
			}
			perRangeMap[rangeKey] = rangeAgg
		}

		rangeSummary := rangeAgg.Summary
		rangeAgg.AvgWeightSum += avgWeight
		rangeAgg.AvgWeightCount++
		for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
			if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
				rangeAgg.FeedSuppliers[supplier.ID] = supplier
			}
		}
		for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] {
			if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
				rangeAgg.DocSuppliers[supplier.ID] = supplier
			}
		}
		rangeSummary.EggProductionPieces += rowEggPieces
		rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
		rangeSummary.EggValueRp += rowEggValue
		if eggWeightFloat > 0 {
			rangeAgg.EggHppSum += eggHpp
			rangeAgg.EggHppCount++
		}
	}

	rangeKeys := make([]weightRangeKey, 0, len(perRangeMap))
	for key := range perRangeMap {
		rangeKeys = append(rangeKeys, key)
	}
	sort.Slice(rangeKeys, func(i, j int) bool {
		if rangeKeys[i].Min == rangeKeys[j].Min {
			return rangeKeys[i].Max < rangeKeys[j].Max
		}
		return rangeKeys[i].Min < rangeKeys[j].Min
	})

	perRangeSummary := make([]dto.HppPerKandangSummaryWeightRangeDTO, 0, len(rangeKeys))
	for idx, key := range rangeKeys {
		agg := perRangeMap[key]
		entry := agg.Summary
		entry.ID = idx + 1
		if agg.AvgWeightCount > 0 {
			entry.AvgWeightKg = agg.AvgWeightSum / float64(agg.AvgWeightCount)
		}
		if agg.EggHppCount > 0 {
			entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount)
		}
		entry.FeedSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.FeedSuppliers))
		for _, supplier := range agg.FeedSuppliers {
			entry.FeedSuppliers = append(entry.FeedSuppliers, supplier)
		}
		entry.DocSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.DocSuppliers))
		for _, supplier := range agg.DocSuppliers {
			entry.DocSuppliers = append(entry.DocSuppliers, supplier)
		}
		perRangeSummary = append(perRangeSummary, *entry)
	}

	totalSummary := dto.HppPerKandangSummaryTotalDTO{
		TotalEggProductionPieces: totalEggPieces,
		TotalEggProductionKg:     totalEggKg,
		TotalEggValueRp:          totalEggValueRp,
	}
	if totalBirds > 0 {
	}
	if totalAvgWeightCount > 0 {
		totalSummary.AverageWeightKg = totalAvgWeightSum / float64(totalAvgWeightCount)
	}
	if totalEggHppCount > 0 {
		totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount)
	}
	if totalHppCount > 0 {
	}
	if totalDocPriceCount > 0 {
		totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount)
	}

	limit := params.Limit
	if limit <= 0 {
		limit = 10
	}
	totalCount := len(dataRows)
	offset := (params.Page - 1) * limit
	if offset < 0 {
		offset = 0
	}
	if offset > totalCount {
		offset = totalCount
	}
	end := offset + limit
	if end > totalCount {
		end = totalCount
	}
	pagedRows := dataRows[offset:end]

	data := dto.HppPerKandangResponseData{
		Period: params.Period,
		Rows:   pagedRows,
		Summary: dto.HppPerKandangSummaryDTO{
			PerWeightRange: perRangeSummary,
			Total:          totalSummary,
		},
	}

	totalResults := int64(totalCount)

	totalPages := int64(0)
	if totalResults > 0 {
		totalPages = int64(math.Ceil(float64(totalResults) / float64(limit)))
	}
	if totalPages == 0 {
		totalPages = 1
	}

	meta := &dto.HppPerKandangMetaDTO{
		Page:         params.Page,
		Limit:        limit,
		TotalPages:   totalPages,
		TotalResults: totalResults,
		Filters:      filters,
	}

	return &data, meta, nil
}

func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.HppPerKandangQuery, dto.HppPerKandangFiltersDTO, error) {
	page := ctx.QueryInt("page", 1)
	if page < 1 {
		page = 1
	}
	limit := ctx.QueryInt("limit", 10)
	if limit < 1 {
		limit = 10
	}

	rawArea := ctx.Query("area_id", "")
	rawLocation := ctx.Query("location_id", "")
	rawKandang := ctx.Query("kandang_id", "")
	rawWeightMin := ctx.Query("weight_min", "")
	rawWeightMax := ctx.Query("weight_max", "")
	period := ctx.Query("period", "")
	showUnrecorded := ctx.QueryBool("show_unrecorded", false)

	areaIDs, err := parseCommaSeparatedInt64s(rawArea)
	if err != nil {
		return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	locationIDs, err := parseCommaSeparatedInt64s(rawLocation)
	if err != nil {
		return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	kandangIDs, err := parseCommaSeparatedInt64s(rawKandang)
	if err != nil {
		return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}

	locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB())
	if err != nil {
		return nil, dto.HppPerKandangFiltersDTO{}, err
	}
	areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB())
	if err != nil {
		return nil, dto.HppPerKandangFiltersDTO{}, err
	}

	if locationScope.Restrict {
		allowed := toInt64Slice(locationScope.IDs)
		if len(allowed) == 0 {
			locationIDs = []int64{-1}
		} else if len(locationIDs) > 0 {
			locationIDs = intersectInt64(locationIDs, allowed)
		} else {
			locationIDs = allowed
		}
	}
	if areaScope.Restrict {
		allowed := toInt64Slice(areaScope.IDs)
		if len(allowed) == 0 {
			areaIDs = []int64{-1}
		} else if len(areaIDs) > 0 {
			areaIDs = intersectInt64(areaIDs, allowed)
		} else {
			areaIDs = allowed
		}
	}

	weightMin, err := parseOptionalFloat64(rawWeightMin)
	if err != nil {
		return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	weightMax, err := parseOptionalFloat64(rawWeightMax)
	if err != nil {
		return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	if weightMin != nil && weightMax != nil && *weightMin > *weightMax {
		return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "weight_min must be less than or equal to weight_max")
	}

	params := &validation.HppPerKandangQuery{
		Page:           page,
		Limit:          limit,
		Period:         period,
		ShowUnrecorded: showUnrecorded,
		AreaIDs:        areaIDs,
		LocationIDs:    locationIDs,
		KandangIDs:     kandangIDs,
		WeightMin:      weightMin,
		WeightMax:      weightMax,
	}

	showUnrecordedFilter := ""
	if showUnrecorded {
		showUnrecordedFilter = "true"
	}

	filters := dto.NewHppPerKandangFiltersDTO(
		rawArea,
		rawLocation,
		rawKandang,
		rawWeightMin,
		rawWeightMax,
		period,
		showUnrecordedFilter,
	)

	return params, filters, nil
}

func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validation.ExpenseDepreciationQuery, dto.ExpenseDepreciationFiltersDTO, error) {
	page := ctx.QueryInt("page", 1)
	if page < 1 {
		page = 1
	}
	limit := ctx.QueryInt("limit", 10)
	if limit < 1 {
		limit = 10
	}

	rawArea := ctx.Query("area_id", "")
	rawLocation := ctx.Query("location_id", "")
	rawProjectFlock := ctx.Query("project_flock_id", "")
	period := strings.TrimSpace(ctx.Query("period", ""))
	forceRecompute := ctx.QueryBool("force_recompute", false)

	areaIDs, err := parseCommaSeparatedInt64s(rawArea)
	if err != nil {
		return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	locationIDs, err := parseCommaSeparatedInt64s(rawLocation)
	if err != nil {
		return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	projectFlockIDs, err := parseCommaSeparatedInt64s(rawProjectFlock)
	if err != nil {
		return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}

	locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB())
	if err != nil {
		return nil, dto.ExpenseDepreciationFiltersDTO{}, err
	}
	areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB())
	if err != nil {
		return nil, dto.ExpenseDepreciationFiltersDTO{}, err
	}

	if locationScope.Restrict {
		allowed := toInt64Slice(locationScope.IDs)
		if len(allowed) == 0 {
			locationIDs = []int64{-1}
		} else if len(locationIDs) > 0 {
			locationIDs = intersectInt64(locationIDs, allowed)
		} else {
			locationIDs = allowed
		}
	}

	if areaScope.Restrict {
		allowed := toInt64Slice(areaScope.IDs)
		if len(allowed) == 0 {
			areaIDs = []int64{-1}
		} else if len(areaIDs) > 0 {
			areaIDs = intersectInt64(areaIDs, allowed)
		} else {
			areaIDs = allowed
		}
	}

	params := &validation.ExpenseDepreciationQuery{
		Page:            page,
		Limit:           limit,
		Period:          period,
		ForceRecompute:  forceRecompute,
		ProjectFlockIDs: projectFlockIDs,
		AreaIDs:         areaIDs,
		LocationIDs:     locationIDs,
	}

	filters := dto.NewExpenseDepreciationFiltersDTO(
		rawArea,
		rawLocation,
		rawProjectFlock,
		period,
	)

	return params, filters, nil
}

func parseCommaSeparatedInt64s(raw string) ([]int64, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}

	parts := strings.Split(raw, ",")
	result := make([]int64, 0, len(parts))
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}

		id, err := strconv.ParseInt(part, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("invalid integer value '%s'", part)
		}
		result = append(result, id)
	}

	return result, nil
}

func toInt64Slice(ids []uint) []int64 {
	if len(ids) == 0 {
		return nil
	}
	out := make([]int64, 0, len(ids))
	for _, id := range ids {
		out = append(out, int64(id))
	}
	return out
}

func intersectInt64(a, b []int64) []int64 {
	if len(a) == 0 || len(b) == 0 {
		return nil
	}
	set := make(map[int64]struct{}, len(b))
	for _, id := range b {
		set[id] = struct{}{}
	}
	out := make([]int64, 0, len(a))
	for _, id := range a {
		if _, ok := set[id]; ok {
			out = append(out, id)
		}
	}
	return out
}

func intersectUint(a, b []uint) []uint {
	if len(a) == 0 || len(b) == 0 {
		return nil
	}
	set := make(map[uint]struct{}, len(b))
	for _, id := range b {
		set[id] = struct{}{}
	}
	out := make([]uint, 0, len(a))
	for _, id := range a {
		if _, ok := set[id]; ok {
			out = append(out, id)
		}
	}
	return out
}

func parseOptionalFloat64(raw string) (*float64, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}

	value, err := strconv.ParseFloat(raw, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid float value '%s'", raw)
	}

	return &value, nil
}
