package service

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

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/sirupsen/logrus"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
	rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
	rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gorm.io/gorm"
)

type ProjectFlockKandangService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
	GetAllNameWithPeriode(ctx *fiber.Ctx, params *validation.Query) ([]ProjectFlockKandangNameWithPeriode, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error)
	CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error)
	Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error)
	GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error)
}

type projectFlockKandangService struct {
	Log                  *logrus.Logger
	Validate             *validator.Validate
	Repository           repository.ProjectFlockKandangRepository
	ApprovalSvc          commonSvc.ApprovalService
	FifoStockV2Svc       commonSvc.FifoStockV2Service
	ExpenseRepo          expenseRepo.ExpenseRepository
	WarehouseRepo        rWarehouse.WarehouseRepository
	ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
	PopulationRepo       repository.ProjectFlockPopulationRepository
	KandangRepo          kandangRepo.KandangRepository
}

type ClosingCheckResult struct {
	UnfinishedExpenses int64                  `json:"unfinished_expenses"`
	StockRemaining     []StockRemainingDetail `json:"stock_remaining"`
	Expenses           []ExpenseSummary       `json:"expenses"`
}

type ProjectFlockKandangNameWithPeriode struct {
	Id          uint
	KandangName string
	Period      int
}

type StockRemainingDetail struct {
	FlagName           string  `json:"flag_name"`
	ProductWarehouseId uint    `json:"product_warehouse_id"`
	ProductId          uint    `json:"product_id"`
	ProductName        string  `json:"product_name"`
	ProductCategory    string  `json:"product_category"`
	Uom                string  `json:"uom"`
	Quantity           float64 `json:"quantity"`
}

type ExpenseSummary struct {
	Id        uint64  `json:"id"`
	PoNumber  string  `json:"po_number"`
	Category  string  `json:"category"`
	Total     float64 `json:"total"`
	Status    string  `json:"status"`
	StepName  string  `json:"step_name"`
	Step      uint16  `json:"step"`
	Reference string  `json:"reference_number"`
}

func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
	return &projectFlockKandangService{
		Log:                  utils.Log,
		Validate:             validate,
		Repository:           repo,
		ApprovalSvc:          approvalSvc,
		FifoStockV2Svc:       fifoStockV2Svc,
		ExpenseRepo:          expenseRepo,
		WarehouseRepo:        warehouseRepo,
		ProductWarehouseRepo: productWarehouseRepo,
		PopulationRepo:       populationRepo,
		KandangRepo:          kandangRepo,
	}
}

func resolveWarehouseForProjectFlockKandang(ctx context.Context, warehouseRepo rWarehouse.WarehouseRepository, pfk *entity.ProjectFlockKandang) (*entity.Warehouse, error) {
	if warehouseRepo == nil || pfk == nil {
		return nil, gorm.ErrRecordNotFound
	}
	if pfk.KandangId == 0 {
		return nil, gorm.ErrRecordNotFound
	}
	if pfk.ProjectFlock.LocationId != 0 {
		warehouse, err := warehouseRepo.GetByKandangIDAndLocationID(ctx, pfk.KandangId, pfk.ProjectFlock.LocationId)
		if err == nil {
			return warehouse, nil
		}
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}
	return warehouseRepo.GetByKandangID(ctx, pfk.KandangId)
}

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

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

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

	projectFlockKandangs, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict)

	if err != nil {
		s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err)
		return nil, 0, err
	}

	if s.ApprovalSvc != nil {
		s.fetchProjectFlockApprovals(c, projectFlockKandangs)
		s.fetchChickinApprovals(c, projectFlockKandangs)
	}

	return projectFlockKandangs, total, nil
}

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

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

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

	rows, total, err := s.Repository.GetAllNameWithPeriodeScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict)
	if err != nil {
		s.Log.Errorf("Failed to get projectFlockKandangs name_with_periode: %+v", err)
		return nil, 0, err
	}

	results := make([]ProjectFlockKandangNameWithPeriode, 0, len(rows))
	for _, row := range rows {
		results = append(results, ProjectFlockKandangNameWithPeriode{
			Id:          row.Id,
			KandangName: row.KandangName,
			Period:      row.Period,
		})
	}

	return results, total, nil
}

func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) {
	scope, err := m.ResolveLocationScope(c, s.Repository.DB())
	if err != nil {
		return nil, nil, nil, err
	}
	if scope.Restrict {
		if len(scope.IDs) == 0 {
			return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
		}
		var count int64
		if err := s.Repository.DB().WithContext(c.Context()).
			Table("project_flock_kandangs").
			Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
			Where("project_flock_kandangs.id = ?", id).
			Where("project_flocks.location_id IN ?", scope.IDs).
			Count(&count).Error; err != nil {
			return nil, nil, nil, err
		}
		if count == 0 {
			return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
		}
	}

	projectFlockKandang, err := s.Repository.GetByID(c.Context(), id)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
	}
	if err != nil {
		return nil, nil, nil, err
	}

	if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil {
		s.fetchProjectFlockApproval(c, projectFlockKandang)
		s.fetchChickinApproval(c, projectFlockKandang)
	}

	availableQtyMap, err := s.getAvailableQuantities(c, projectFlockKandang)
	if err != nil {
		s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err)
		availableQtyMap = nil
	}

	var productWarehouses []entity.ProductWarehouse
	if len(availableQtyMap) > 0 {
		pwIds := make([]uint, 0, len(availableQtyMap))
		for pwId := range availableQtyMap {
			pwIds = append(pwIds, pwId)
		}
		productWarehouses, err = s.ProductWarehouseRepo.GetByIDs(c.Context(), pwIds, func(db *gorm.DB) *gorm.DB {
			return db.Preload("Product").Preload("Warehouse")
		})
		if err != nil {
			s.Log.Warnf("Failed to fetch product warehouses: %+v", err)
			productWarehouses = []entity.ProductWarehouse{}
		}
	}

	return projectFlockKandang, availableQtyMap, productWarehouses, nil
}

func (s projectFlockKandangService) fetchProjectFlockApprovals(c *fiber.Ctx, projectFlockKandangs []entity.ProjectFlockKandang) {
	projectFlockKandangIDs := make([]uint, len(projectFlockKandangs))
	for i, pfk := range projectFlockKandangs {
		projectFlockKandangIDs[i] = pfk.Id
	}

	approvalMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandangIDs, func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	})
	if err != nil {
		s.Log.Warnf("Failed to fetch approvals for projectFlockKandangs: %+v", err)
	} else {
		for i := range projectFlockKandangs {
			if approval, ok := approvalMap[projectFlockKandangs[i].Id]; ok {
				projectFlockKandangs[i].LatestProjectFlockApproval = approval
			}
		}
	}
}

func (s projectFlockKandangService) fetchChickinApprovals(c *fiber.Ctx, projectFlockKandangs []entity.ProjectFlockKandang) {
	projectFlockKandangIDs := make([]uint, len(projectFlockKandangs))
	for i, pfk := range projectFlockKandangs {
		projectFlockKandangIDs[i] = pfk.Id
	}

	chickinApprovalMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandangIDs, func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	})
	if err != nil {
		s.Log.Warnf("Failed to fetch chickin approvals for projectFlockKandangs: %+v", err)
	} else {
		for i := range projectFlockKandangs {
			if approval, ok := chickinApprovalMap[projectFlockKandangs[i].Id]; ok {
				projectFlockKandangs[i].LatestChickinApproval = approval
			}
		}
	}
}

func (s projectFlockKandangService) fetchProjectFlockApproval(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) {
	latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
	if err != nil {
		s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
	}

	if latest != nil {
		projectFlockKandang.LatestProjectFlockApproval = latest
	}
}

func (s projectFlockKandangService) fetchChickinApproval(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) {
	latestChickin, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
	if err != nil {
		s.Log.Errorf("Failed to fetch latest chickin approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
	}

	if latestChickin != nil {
		projectFlockKandang.LatestChickinApproval = latestChickin
	}
}

func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) {
	if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil {
		return nil, nil
	}

	warehouse, err := resolveWarehouseForProjectFlockKandang(c.Context(), s.WarehouseRepo, projectFlockKandang)
	if err != nil || warehouse == nil {
		return nil, nil
	}

	var productCategoryCode string
	if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
		productCategoryCode = "DOC"
	} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
		productCategoryCode = "PULLET"
	} else {
		return nil, nil
	}

	products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
	if err != nil || len(products) == 0 {
		return nil, nil
	}

	result := make(map[uint]float64)
	for _, pw := range products {

		if pw.ProjectFlockKandangId == nil || *pw.ProjectFlockKandangId == projectFlockKandang.Id {
			availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
			if err != nil {
				s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
			}

			if availableQty > 0 {
				result[pw.Id] = availableQty
			}
		}
	}

	return result, nil
}

func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*ClosingCheckResult, error) {
	pfk, err := s.Repository.GetByID(c.Context(), id)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
		}
		return nil, err
	}

	var unfinished int64
	if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
		count, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
			return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
		})
		if err != nil {
			return nil, err
		}
		unfinished = count
	}

	stockRemain := make([]StockRemainingDetail, 0)
	if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
		warehouse, werr := resolveWarehouseForProjectFlockKandang(c.Context(), s.WarehouseRepo, pfk)
		if werr != nil {
			return nil, werr
		}

		for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
			productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
			if pwErr != nil {
				return nil, pwErr
			}

			for _, pw := range productWarehouses {
				category := ""
				if pw.Product.ProductCategory.Id != 0 {
					category = pw.Product.ProductCategory.Name
				}
				uomName := ""
				if pw.Product.Uom.Id != 0 {
					uomName = pw.Product.Uom.Name
				}
				stockRemain = append(stockRemain, StockRemainingDetail{
					FlagName:           string(flagName),
					ProductWarehouseId: pw.Id,
					ProductId:          pw.ProductId,
					ProductName:        pw.Product.Name,
					ProductCategory:    category,
					Uom:                uomName,
					Quantity:           pw.Quantity,
				})
			}
		}
	}

	expenseSummaries := make([]ExpenseSummary, 0)
	if s.ExpenseRepo != nil {
		var expenses []entity.Expense
		if err := s.ExpenseRepo.DB().WithContext(c.Context()).
			Scopes(s.ExpenseRepo.WithProjectFlockKandangFilter(pfk.Id, pfk.KandangId)).
			Preload("Nonstocks").
			Find(&expenses).Error; err != nil {
			return nil, err
		}

		if len(expenses) > 0 && s.ApprovalSvc != nil {
			ids := make([]uint, 0, len(expenses))
			for _, e := range expenses {
				ids = append(ids, uint(e.Id))
			}
			latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, ids, nil)
			if err == nil {
				for i := range expenses {
					if latest, ok := latestMap[uint(expenses[i].Id)]; ok {
						expenses[i].LatestApproval = latest
					}
				}
			}
		}

		for _, exp := range expenses {
			total := 0.0
			for _, ns := range exp.Nonstocks {
				total += ns.Qty * ns.Price
			}

			status := "Pending"
			stepName := ""
			stepNum := uint16(0)
			if exp.LatestApproval != nil {
				stepName = exp.LatestApproval.StepName
				stepNum = exp.LatestApproval.StepNumber
				if exp.LatestApproval.Action != nil {
					status = string(*exp.LatestApproval.Action)
				} else if stepName != "" {
					status = stepName
				}
			}

			expenseSummaries = append(expenseSummaries, ExpenseSummary{
				Id:        exp.Id,
				PoNumber:  exp.PoNumber,
				Category:  exp.Category,
				Total:     total,
				Status:    status,
				StepName:  stepName,
				Step:      stepNum,
				Reference: exp.ReferenceNumber,
			})
		}
	}

	return &ClosingCheckResult{
		UnfinishedExpenses: unfinished,
		StockRemaining:     stockRemain,
		Expenses:           expenseSummaries,
	}, nil
}

// getProjectFlockKandangClosingDate mengembalikan tanggal closing PFK jika sudah di-close.
func (s projectFlockKandangService) GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error) {
	if id == 0 {
		return nil, nil
	}
	pfk, err := s.Repository.GetByID(c.Context(), id)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return pfk.ClosedAt, nil
}

func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

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

	pfk, err := s.Repository.GetByID(c.Context(), id)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
		}
		return nil, err
	}

	if s.ApprovalSvc != nil {
		latest, aerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
		if aerr != nil {
			return nil, aerr
		}
		if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
		}
		if latest.StepNumber != uint16(utils.ProjectFlockStepAktif) && latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
		}
	}

	action := strings.ToLower(strings.TrimSpace(req.Action))
	now := time.Now()

	switch action {
	case "close":
		if pfk.ClosedAt != nil {
			return nil, fiber.NewError(fiber.StatusConflict, "Kandang sudah closed")
		}
		if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
			unfinished, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
				return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
			})
			if err != nil {
				return nil, err
			}
			if unfinished > 0 {
				return nil, fiber.NewError(fiber.StatusBadRequest, "Masih ada expense belum selesai untuk kandang ini")
			}
		}

		if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
			warehouse, werr := resolveWarehouseForProjectFlockKandang(c.Context(), s.WarehouseRepo, pfk)
			if werr != nil {
				return nil, werr
			}

			for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
				productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
				if pwErr != nil {
					return nil, pwErr
				}

				for _, pw := range productWarehouses {
					if pw.Quantity > 0 {
						return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok %s masih tersedia (product warehouse %d: %.2f)", flagName, pw.Id, pw.Quantity))
					}
				}
			}
		}

		closeTime := now
		if req.ClosedDate != nil {
			parsed, perr := utils.ParseDateString(strings.TrimSpace(*req.ClosedDate))
			if perr != nil {
				return nil, fiber.NewError(fiber.StatusBadRequest, "closed_date tidak valid, gunakan format YYYY-MM-DD")
			}
			closeTime = parsed
		}
		if err := s.Repository.UpdateClosedAt(c.Context(), id, &closeTime); err != nil {
			return nil, err
		}
		if s.KandangRepo != nil {
			if err := s.KandangRepo.UpdateStatusByIDs(
				c.Context(),
				[]uint{pfk.KandangId},
				utils.KandangStatusNonActive,
			); err != nil {
				return nil, err
			}
		}
		if s.ApprovalSvc != nil {
			closeAction := entity.ApprovalActionApproved
			// Hindari duplikasi jika approval terakhir sudah Closed + Approved
			latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
			if lerr != nil {
				return nil, lerr
			}
			shouldCreate := true
			if latestPFK != nil &&
				latestPFK.StepNumber == uint16(utils.ProjectFlockKandangStepClosed) &&
				latestPFK.Action != nil && *latestPFK.Action == closeAction {
				shouldCreate = false
			}

			if shouldCreate {
				if _, aerr := s.ApprovalSvc.CreateApproval(
					c.Context(),
					utils.ApprovalWorkflowProjectFlockKandang,
					id,
					utils.ProjectFlockKandangStepClosed,
					&closeAction,
					actorID,
					nil,
				); aerr != nil {
					return nil, aerr
				}
			}

			// Jika semua kandang dalam project sudah ditutup, set approval project flock ke SELESAI.
			pfks, ferr := s.Repository.GetByProjectFlockID(c.Context(), pfk.ProjectFlockId)
			if ferr != nil {
				return nil, ferr
			}
			allClosed := true
			for _, item := range pfks {
				if item.ClosedAt == nil {
					allClosed = false
					break
				}
			}
			if allClosed {
				latestPF, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
				if lerr != nil {
					return nil, lerr
				}
				if latestPF == nil || latestPF.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
					if _, aerr := s.ApprovalSvc.CreateApproval(
						c.Context(),
						utils.ApprovalWorkflowProjectFlock,
						pfk.ProjectFlockId,
						utils.ProjectFlockStepSelesai,
						&closeAction,
						actorID,
						nil,
					); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
						return nil, aerr
					}
				}
			}
		}
	case "unclose":
		if pfk.ClosedAt == nil {
			return nil, fiber.NewError(fiber.StatusConflict, "Kandang belum closed")
		}
		openNewer, err := s.Repository.HasOpenNewerPeriod(c.Context(), pfk.KandangId, pfk.Period, &pfk.Id)
		if err != nil {
			return nil, err
		}
		if openNewer {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Tidak dapat un-close: ada periode yang sedang berjalan")
		}
		if err := s.Repository.UpdateClosedAt(c.Context(), id, nil); err != nil {
			return nil, err
		}
		if s.KandangRepo != nil {
			if err := s.KandangRepo.UpdateStatusByIDs(
				c.Context(),
				[]uint{pfk.KandangId},
				utils.KandangStatusActive,
			); err != nil {
				return nil, err
			}
		}
		if s.ApprovalSvc != nil {
			reopenAction := entity.ApprovalActionApproved
			// Hindari duplikasi jika approval terakhir sudah Disetujui + Updated
			latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
			if lerr != nil {
				return nil, lerr
			}
			shouldCreate := true
			if latestPFK != nil &&
				latestPFK.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) &&
				latestPFK.Action != nil && *latestPFK.Action == reopenAction {
				shouldCreate = false
			}

			if shouldCreate {
				if _, aerr := s.ApprovalSvc.CreateApproval(
					c.Context(),
					utils.ApprovalWorkflowProjectFlockKandang,
					id,
					utils.ProjectFlockKandangStepDisetujui,
					&reopenAction,
					actorID,
					nil,
				); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
					return nil, aerr
				}
			}

			// Pastikan approval project flock kembali ke Aktif
			latestPF, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
			if lerr != nil {
				return nil, lerr
			}
			shouldCreatePF := true
			if latestPF != nil &&
				latestPF.StepNumber == uint16(utils.ProjectFlockStepAktif) &&
				latestPF.Action != nil && *latestPF.Action == reopenAction {
				shouldCreatePF = false
			}
			if shouldCreatePF {
				if _, aerr := s.ApprovalSvc.CreateApproval(
					c.Context(),
					utils.ApprovalWorkflowProjectFlock,
					pfk.ProjectFlockId,
					utils.ProjectFlockStepAktif,
					&reopenAction,
					actorID,
					nil,
				); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
					return nil, aerr
				}
			}
		}
	default:
		return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
	}

	return s.Repository.GetByID(c.Context(), id)
}

func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {

	availableQty := productWarehouse.Quantity

	if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {

		var totalPendingQty float64

		for _, chickin := range projectFlockKandang.Chickins {
			if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
				totalPendingQty += chickin.PendingUsageQty
			}
		}

		totalPopulationQty := 0.0
		if s.PopulationRepo != nil {
			popQty, err := s.PopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), productWarehouse.Id)
			if err != nil {
				s.Log.Errorf("Failed to get population qty for PW %d: %+v", productWarehouse.Id, err)
			} else {
				totalPopulationQty = popQty
			}
		}

		availableQty = productWarehouse.Quantity - totalPendingQty - totalPopulationQty
		if availableQty < 0 {
			availableQty = 0
		}
	} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {

		var totalPendingQty float64

		for _, chickin := range projectFlockKandang.Chickins {
			if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
				totalPendingQty += chickin.PendingUsageQty
			}
		}

		totalPopulationQty := 0.0
		if s.PopulationRepo != nil {
			popQty, err := s.PopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), productWarehouse.Id)
			if err != nil {
				s.Log.Errorf("Failed to get population qty for PW %d: %+v", productWarehouse.Id, err)
			} else {
				totalPopulationQty = popQty
			}
		}

		availableQty = productWarehouse.Quantity - totalPendingQty - totalPopulationQty
		if availableQty < 0 {
			availableQty = 0
		}

		sourceAvailable, err := s.resolveLayingSourceAvailableQty(c.Context(), nil, productWarehouse.Id, nil)
		if err != nil {
			return 0, err
		}
		if sourceAvailable < availableQty {
			availableQty = sourceAvailable
		}
	}

	return availableQty, nil
}

func (s projectFlockKandangService) resolveLayingSourceAvailableQty(ctx context.Context, tx *gorm.DB, productWarehouseID uint, asOf *time.Time) (float64, error) {
	if productWarehouseID == 0 || s.FifoStockV2Svc == nil {
		return 0, nil
	}

	flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
	if err != nil {
		return 0, err
	}
	if strings.TrimSpace(flagGroupCode) == "" {
		return 0, nil
	}

	gatherRows, err := s.FifoStockV2Svc.Gather(ctx, commonSvc.FifoStockV2GatherRequest{
		FlagGroupCode:      flagGroupCode,
		Lane:               commonSvc.FifoStockV2Lane("STOCKABLE"),
		AllocationPurpose:  entity.StockAllocationPurposeConsume,
		ProductWarehouseID: productWarehouseID,
		AsOf:               asOf,
		Limit:              10000,
		Tx:                 tx,
	})
	if err != nil {
		return 0, err
	}

	total := 0.0
	for _, row := range gatherRows {
		if row.AvailableQuantity <= 0 {
			continue
		}
		total += row.AvailableQuantity
	}
	return math.Max(total, 0), nil
}

func (s projectFlockKandangService) resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
	type row struct {
		FlagGroupCode string `gorm:"column:flag_group_code"`
	}
	selected := row{}

	db := s.Repository.DB()
	if tx != nil {
		db = tx
	}

	err := db.WithContext(ctx).
		Table("fifo_stock_v2_route_rules rr").
		Select("rr.flag_group_code").
		Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
		Where("rr.is_active = TRUE").
		Where("rr.lane = 'STOCKABLE'").
		Where(`
			EXISTS (
				SELECT 1
				FROM product_warehouses pw
				JOIN flags f ON f.flagable_id = pw.product_id
				JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
				WHERE pw.id = ?
				  AND f.flagable_type = ?
				  AND fm.flag_group_code = rr.flag_group_code
			)
		`, productWarehouseID, entity.FlagableTypeProduct).
		Order("fg.priority ASC, rr.id ASC").
		Limit(1).
		Take(&selected).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return "", nil
		}
		return "", err
	}
	return strings.TrimSpace(selected.FlagGroupCode), nil
}
