package service

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

	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	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"
	productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
	flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
	kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
	nonstockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
	warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
	projectBudgetRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
	recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
	transferLayingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
	uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
	purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
	utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
	approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"

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

type ProjectflockService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error)
	CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
	GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
	GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
	GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
	GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
	GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error)
	GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error)
	GetProjectFlockKandangTransferStateAtDate(ctx *fiber.Ctx, projectFlockKandangID uint, referenceDate *time.Time) (bool, bool, error)
	GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
	GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
	Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
	Resubmit(ctx *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error)
	EnsureProjectFlockApproved(ctx context.Context, projectFlockID uint) error
}

type projectflockService struct {
	Log                  *logrus.Logger
	Validate             *validator.Validate
	Repository           repository.ProjectflockRepository
	FlockRepo            flockRepository.FlockRepository
	KandangRepo          kandangRepository.KandangRepository
	NonstockRepo         nonstockRepository.NonstockRepository
	WarehouseRepo        warehouseRepository.WarehouseRepository
	ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
	ProjectBudgetRepo    projectBudgetRepository.ProjectBudgetRepository
	PivotRepo            repository.ProjectFlockKandangRepository
	PopulationRepo       repository.ProjectFlockPopulationRepository
	RecordingRepo        recordingRepo.RecordingRepository
	TransferLayingRepo   transferLayingRepo.TransferLayingRepository
	ApprovalSvc          commonSvc.ApprovalService
	approvalWorkflow     approvalutils.ApprovalWorkflowKey
}

type KandangPeriodSummary struct {
	Id     uint
	Name   string
	Period int
}

func NewProjectflockService(
	repo repository.ProjectflockRepository,
	flockRepo flockRepository.FlockRepository,
	kandangRepo kandangRepository.KandangRepository,
	pivotRepo repository.ProjectFlockKandangRepository,
	warehouseRepo warehouseRepository.WarehouseRepository,
	productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
	projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
	nonstockRepo nonstockRepository.NonstockRepository,
	populationRepo repository.ProjectFlockPopulationRepository,
	recordingRepo recordingRepo.RecordingRepository,
	transferLayingRepo transferLayingRepo.TransferLayingRepository,
	approvalSvc commonSvc.ApprovalService,
	validate *validator.Validate,

) ProjectflockService {
	return &projectflockService{
		Log:                  utils.Log,
		Validate:             validate,
		Repository:           repo,
		FlockRepo:            flockRepo,
		KandangRepo:          kandangRepo,
		NonstockRepo:         nonstockRepo,
		WarehouseRepo:        warehouseRepo,
		ProductWarehouseRepo: productWarehouseRepo,
		ProjectBudgetRepo:    projectBudgetRepo,
		PivotRepo:            pivotRepo,
		PopulationRepo:       populationRepo,
		RecordingRepo:        recordingRepo,
		TransferLayingRepo:   transferLayingRepo,
		ApprovalSvc:          approvalSvc,
		approvalWorkflow:     utils.ApprovalWorkflowProjectFlock,
	}
}

func (s projectflockService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	}
}

func (s projectflockService) EnsureProjectFlockApproved(ctx context.Context, projectFlockID uint) error {
	if projectFlockID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
	}

	approvalSvc := s.ApprovalSvc
	if approvalSvc == nil {
		approvalSvc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
	}

	latest, err := approvalSvc.LatestByTarget(ctx, s.approvalWorkflow, projectFlockID, nil)
	if err != nil {
		s.Log.Errorf("Failed to check project flock %d approval status: %+v", projectFlockID, err)
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa status project flock")
	}

	if latest == nil {
		return fiber.NewError(fiber.StatusBadRequest, "Project flock masih dalam status pengajuan sehingga belum dapat membuat recording")
	}
	if latest.StepNumber != uint16(utils.ProjectFlockStepAktif) || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
		return fiber.NewError(fiber.StatusBadRequest, "Project flock masih dalam status pengajuan sehingga belum dapat membuat recording")
	}

	return nil
}

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

	scope, err := m.ResolveLocationScope(c, s.Repository.DB())
	if err != nil {
		return nil, 0, nil, err
	}
	if params.TransferContext == utils.TransferContextTransferToLaying {
		if m.HasPermission(c, m.P_TransferToLaying_CreateOne) || m.HasPermission(c, m.P_TransferToLaying_UpdateOne) {
			scope.Restrict = false
			scope.IDs = nil
		}
	}

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

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

	if err != nil {
		s.Log.Errorf("Failed to get projectflocks: %+v", err)
		return nil, 0, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks")
	}

	if s.ApprovalSvc != nil && len(projectflocks) > 0 {
		ids := make([]uint, len(projectflocks))
		for i, item := range projectflocks {
			ids[i] = item.Id
		}

		latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, s.approvalQueryModifier())
		if err != nil {
			s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err)
		} else if len(latestMap) > 0 {
			for i := range projectflocks {
				if approval, ok := latestMap[projectflocks[i].Id]; ok {
					projectflocks[i].LatestApproval = approval
				}
			}
		}
	}

	flockMap := make(map[uint]*flockDTO.FlockRelationDTO)
	for i := range projectflocks {
		if projectflocks[i].FlockName != "" {
			baseName := pfutils.DeriveBaseName(projectflocks[i].FlockName)
			if baseName != "" {
				flock, err := s.FlockRepo.GetByName(c.Context(), baseName)
				if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
					s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err)
				} else if flock != nil {
					flockMap[projectflocks[i].Id] = &flockDTO.FlockRelationDTO{
						Id:   flock.Id,
						Name: flock.Name,
					}
				}
			}
		}
	}

	return projectflocks, total, flockMap, nil
}

func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
	projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get projectflock by id: %+v", err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
	}

	if s.ApprovalSvc != nil {
		approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
		if err != nil {
			s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
		} else if len(approvals) > 0 {
			if projectflock.LatestApproval == nil {
				latest := approvals[len(approvals)-1]
				projectflock.LatestApproval = &latest
			}
		} else {
			projectflock.LatestApproval = nil
		}
	}

	return projectflock, nil
}

func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) {
	scope, err := m.ResolveLocationScope(c, s.Repository.DB())
	if err != nil {
		return nil, nil, err
	}

	projectflock, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
		db = s.Repository.WithDefaultRelations()(db)
		db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id")
		return db
	})
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get projectflock by id: %+v", err)
		return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
	}

	if s.ApprovalSvc != nil {
		approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
		if err != nil {
			s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
		} else if len(approvals) > 0 {
			if projectflock.LatestApproval == nil {
				latest := approvals[len(approvals)-1]
				projectflock.LatestApproval = &latest
			}
		} else {
			projectflock.LatestApproval = nil
		}
	}

	// Fetch Flock master data for this ProjectFlock
	var flockResult *flockDTO.FlockRelationDTO
	if projectflock.FlockName != "" {
		baseName := pfutils.DeriveBaseName(projectflock.FlockName)
		if baseName != "" {
			flock, err := s.FlockRepo.GetByName(c.Context(), baseName)
			if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
				s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err)
			} else if flock != nil {
				flockResult = &flockDTO.FlockRelationDTO{
					Id:   flock.Id,
					Name: flock.Name,
				}
			}
		}
	}

	return projectflock, flockResult, nil
}

func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

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

	cat := strings.ToUpper(req.Category)
	if !utils.IsValidProjectFlockCategory(cat) {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
	}

	if len(req.KandangIds) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
	}

	baseName := strings.TrimSpace(req.FlockName)
	if baseName == "" {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
	}

	if err := commonSvc.EnsureRelations(c.Context(),
		commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
		commonSvc.RelationCheck{Name: "Production Standard", ID: &req.ProductionStandardId, Exists: s.Repository.ProductionStandardExists},
		commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
	); err != nil {
		return nil, err
	}

	var location entity.Location
	if err := s.Repository.DB().WithContext(c.Context()).
		Where("id = ? AND area_id = ?", req.LocationId, req.AreaId).
		First(&location).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Lokasi tidak berada pada area yang diminta")
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi area-lokasi")
	}

	canonicalBase := baseName
	if s.FlockRepo != nil {
		baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
		if err != nil {
			return nil, err
		}
		canonicalBase = baseFlock.Name
	}

	kandangIDs := uniqueUintSlice(req.KandangIds)
	kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
	}
	if len(kandangs) != len(kandangIDs) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
	}
	for _, kandang := range kandangs {
		if kandang.LocationId != req.LocationId {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
		}
	}
	// larang kalau ada yg sudah terikat ke project lain
	if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
	} else if linked {
		return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
	}

	createBody := &entity.ProjectFlock{
		AreaId:               req.AreaId,
		Category:             cat,
		ProductionStandardId: req.ProductionStandardId,
		LocationId:           req.LocationId,
		CreatedBy:            actorID,
	}

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		projectRepo := repository.NewProjectflockRepository(dbTransaction)

		generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil)
		if err != nil {
			return err
		}
		createBody.FlockName = generatedName

		if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
			return err
		}

		periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs)
		if err != nil {
			return err
		}
		if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, periods); err != nil {
			return err
		}

		if err := s.UpsertProjectBudget(c.Context(), dbTransaction, createBody.Id, req.ProjectBudgets); err != nil {
			return err
		}

		action := entity.ApprovalActionCreated
		approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
		_, err = approvalSvcTx.CreateApproval(
			c.Context(),
			utils.ApprovalWorkflowProjectFlock,
			createBody.Id,
			utils.ProjectFlockStepPengajuan,
			&action,
			actorID,
			nil,
		)
		return err
	})

	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		if errors.Is(err, gorm.ErrDuplicatedKey) {
			return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
		}
		s.Log.Errorf("Failed to create projectflock: %+v", err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock")
	}

	return s.getOneEntityOnly(c, createBody.Id)
}

func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
	existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
	}
	if err != nil {
		s.Log.Errorf("Failed to fetch projectflock %d before delete: %+v", id, err)
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
	}

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		if len(existing.Kandangs) > 0 {
			ids := make([]uint, len(existing.Kandangs))
			for i, k := range existing.Kandangs {
				ids[i] = k.Id
			}
			if err := s.detachKandangs(c.Context(), dbTransaction, id, ids, true); err != nil {
				return err
			}
		}

		if err := repository.NewProjectflockRepository(dbTransaction).DeleteOne(c.Context(), id); err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
			}
			return err
		}

		return nil
	})
	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return fiberErr
		}
		s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err)
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete project flock")
	}

	return nil
}

func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) {

	pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
		}
		s.Log.Errorf("Failed to fetch project_flock_kandang by project %d and kandang %d: %+v", projectFlockID, kandangID, err)
		return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
	}

	availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
	if err != nil {
		return nil, 0, err
	}

	return pfk, availableQuantity, nil
}

func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) {
	if s.PopulationRepo == nil {
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
	}
	if projectFlockKandangID == 0 {
		return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
	}

	total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
	if err != nil {
		s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err)
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
	}
	if total > 0 {
		return total, nil
	}

	if s.RecordingRepo != nil {
		latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
		if err != nil {
			s.Log.Errorf("Failed to fetch latest recording for project flock kandang %d: %+v", projectFlockKandangID, err)
			return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
		}
		if latest != nil && latest.TotalChickQty != nil && *latest.TotalChickQty > 0 {
			return *latest.TotalChickQty, nil
		}
	}

	return total, nil
}

func (s projectflockService) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error) {
	if s.PopulationRepo == nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
	}
	if projectFlockKandangID == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
	}

	populations, err := s.PopulationRepo.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
	if err != nil {
		s.Log.Errorf("Failed to fetch populations for project flock kandang %d: %+v", projectFlockKandangID, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang chick in date")
	}

	var earliest *time.Time
	for _, pop := range populations {
		if pop.ProjectChickin == nil || pop.ProjectChickin.ChickInDate.IsZero() {
			continue
		}
		chickinDate := pop.ProjectChickin.ChickInDate
		if earliest == nil || chickinDate.Before(*earliest) {
			copy := chickinDate
			earliest = &copy
		}
	}

	return earliest, nil
}

func (s projectflockService) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error) {
	return s.GetProjectFlockKandangTransferStateAtDate(ctx, projectFlockKandangID, nil)
}

func (s projectflockService) GetProjectFlockKandangTransferStateAtDate(ctx *fiber.Ctx, projectFlockKandangID uint, referenceDate *time.Time) (bool, bool, error) {
	if projectFlockKandangID == 0 || s.TransferLayingRepo == nil || s.PivotRepo == nil {
		return false, false, nil
	}

	pfk, err := s.PivotRepo.GetByIDLight(ctx.Context(), projectFlockKandangID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, false, nil
		}
		s.Log.Errorf("Failed to resolve project flock kandang %d for transfer state: %+v", projectFlockKandangID, err)
		return false, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer state")
	}

	category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
	var transfer *entity.LayingTransfer
	switch category {
	case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
		transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx.Context(), projectFlockKandangID)
	case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
		transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx.Context(), projectFlockKandangID)
	default:
		return false, false, nil
	}
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, false, nil
		}
		s.Log.Errorf("Failed to resolve transfer state for project flock kandang %d: %+v", projectFlockKandangID, err)
		return false, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer state")
	}
	if transfer == nil {
		return false, false, nil
	}

	physicalMoveDate := normalizeDateOnlyUTC(transfer.TransferDate)
	if physicalMoveDate.IsZero() {
		return false, false, nil
	}

	economicCutoffDate := physicalMoveDate
	if transfer.EconomicCutoffDate != nil && !transfer.EconomicCutoffDate.IsZero() {
		economicCutoffDate = normalizeDateOnlyUTC(*transfer.EconomicCutoffDate)
	} else if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() {
		economicCutoffDate = normalizeDateOnlyUTC(*transfer.EffectiveMoveDate)
	}
	if economicCutoffDate.Before(physicalMoveDate) {
		economicCutoffDate = physicalMoveDate
	}

	reference := normalizeDateOnlyUTC(time.Now().UTC())
	if referenceDate != nil && !referenceDate.IsZero() {
		reference = normalizeDateOnlyUTC(referenceDate.UTC())
	}
	isTransition := !reference.Before(physicalMoveDate) && reference.Before(economicCutoffDate)
	isLaying := !reference.Before(economicCutoffDate)

	return isTransition, isLaying, nil
}

func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
	idStr = strings.TrimSpace(idStr)
	projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
	kandangIdStr = strings.TrimSpace(kandangIdStr)

	if idStr != "" {
		id, err := strconv.Atoi(idStr)
		if err != nil || id <= 0 {
			return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
		}
		pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id))
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
			}
			s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", id, err)
			return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
		}

		availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
		if err != nil {
			return nil, 0, err
		}

		return pfk, availableQuantity, nil
	}

	if projectFlockIdStr == "" || kandangIdStr == "" {
		return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
	}
	pfid, err := strconv.Atoi(projectFlockIdStr)
	if err != nil || pfid <= 0 {
		return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
	}
	kid, err := strconv.Atoi(kandangIdStr)
	if err != nil || kid <= 0 {
		return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
	}
	return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
}

func normalizeDateOnlyUTC(value time.Time) time.Time {
	return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
}

func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
	if s.PopulationRepo == nil {
		return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
	}

	pfk, err := s.PivotRepo.GetActiveByKandangID(ctx.Context(), kandangID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
		}
		return 0, err
	}

	total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), pfk.Id)
	if err != nil {
		return 0, err
	}
	return total, nil
}

func (s projectflockService) GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error) {
	if kandangID == 0 || s.WarehouseRepo == nil {
		return nil, nil
	}

	var warehouse entity.Warehouse
	err := s.WarehouseRepo.DB().WithContext(ctx.Context()).
		Preload("Area").
		Preload("Location").
		Preload("Kandang").
		Where("kandang_id = ?", kandangID).
		Where("deleted_at IS NULL").
		Order("id DESC").
		First(&warehouse).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		s.Log.Errorf("Failed to fetch warehouse for kandang %d: %+v", kandangID, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouse")
	}

	return &warehouse, nil
}

func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
	if len(projectIDs) == 0 {
		return map[uint]int{}, nil
	}
	return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
}

func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

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

	var action entity.ApprovalAction
	switch strings.ToUpper(strings.TrimSpace(req.Action)) {
	case string(entity.ApprovalActionRejected):
		action = entity.ApprovalActionRejected
	case string(entity.ApprovalActionApproved):
		action = entity.ApprovalActionApproved
	default:
		return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
	}

	approvableIDs := uniqueUintSlice(req.ApprovableIds)
	if len(approvableIDs) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
	}

	step := utils.ProjectFlockStepPengajuan
	if action == entity.ApprovalActionApproved {
		step = utils.ProjectFlockStepAktif
	}

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
		kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
		projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
		projectFlockKandangRepoTx := repository.NewProjectFlockKandangRepository(dbTransaction)

		for _, approvableID := range approvableIDs {
			if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
				}
				return err
			}

			if _, err := approvalSvc.CreateApproval(
				c.Context(),
				utils.ApprovalWorkflowProjectFlock,
				approvableID,
				step,
				&action,
				actorID,
				req.Notes,
			); err != nil {
				return err
			}

			switch action {
			case entity.ApprovalActionApproved:
				if err := kandangRepoTx.UpdateStatusByProjectFlockID(
					c.Context(),
					approvableID,
					utils.KandangStatusActive,
				); err != nil {
					return err
				}

				pfks, err := projectFlockKandangRepoTx.GetByProjectFlockID(c.Context(), approvableID)
				if err != nil {
					return err
				}
				for _, pfk := range pfks {
					latest, lerr := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, pfk.Id, nil)
					if lerr != nil {
						return lerr
					}
					if latest != nil && latest.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) {
						continue
					}
					if _, aerr := approvalSvc.CreateApproval(
						c.Context(),
						utils.ApprovalWorkflowProjectFlockKandang,
						pfk.Id,
						utils.ProjectFlockKandangStepDisetujui,
						&action,
						actorID,
						req.Notes,
					); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
						return aerr
					}
				}
			case entity.ApprovalActionRejected:
				if err := kandangRepoTx.UpdateStatusByProjectFlockID(
					c.Context(),
					approvableID,
					utils.KandangStatusNonActive,
				); err != nil {
					return err
				}
			}
		}

		return nil
	})

	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
		}
		s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
	}

	updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
	for _, approvableID := range approvableIDs {
		project, err := s.getOneEntityOnly(c, approvableID)
		if err != nil {
			return nil, err
		}
		updated = append(updated, *project)
	}

	return updated, nil
}

func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
	if locationID == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
	}

	exists, err := s.Repository.LocationExists(c.Context(), locationID)
	if err != nil {
		s.Log.Errorf("Failed to validate location %d: %+v", locationID, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate location")
	}
	if !exists {
		return nil, fiber.NewError(fiber.StatusNotFound, "Location not found")
	}

	rows, err := s.Repository.GetKandangPeriodSummaryRows(c.Context(), locationID)
	if err != nil {
		s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary")
	}

	summaries := make([]KandangPeriodSummary, 0, len(rows))
	for _, row := range rows {
		nextPeriod := 1
		if row.LatestPeriod > 0 {
			nextPeriod = row.LatestPeriod + 1
		}

		summaries = append(summaries, KandangPeriodSummary{
			Id:     row.Id,
			Name:   row.Name,
			Period: nextPeriod,
		})
	}

	return summaries, nil
}

func uniqueUintSlice(values []uint) []uint {
	seen := make(map[uint]struct{}, len(values))
	result := make([]uint, 0, len(values))
	for _, v := range values {
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		result = append(result, v)
	}
	return result
}

func (s projectflockService) generateSequentialFlockName(ctx context.Context, repo repository.ProjectflockRepository, baseName string, startNumber int, excludeID *uint) (string, int, error) {
	name := strings.TrimSpace(baseName)
	if name == "" {
		return "", 0, fiber.NewError(fiber.StatusBadRequest, "Base flock name cannot be empty")
	}

	number := startNumber
	if number <= 0 {
		number = 1
	}

	attempts := 0
	for {
		candidate := fmt.Sprintf("%s %03d", name, number)
		exists, err := repo.ExistsByFlockName(ctx, candidate, excludeID)
		if err != nil {
			s.Log.Errorf("Failed checking project flock name uniqueness for %q: %+v", candidate, err)
			return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate flock name")
		}
		if !exists {
			return candidate, number, nil
		}
		number++
		attempts++
		if attempts > 9999 {
			return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Unable to generate unique flock name")
		}
	}
}

func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint, name string) (*entity.Flock, error) {
	trimmed := strings.TrimSpace(name)
	if trimmed == "" {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
	}

	flock, err := s.FlockRepo.GetByName(ctx, trimmed)
	if err == nil {
		return flock, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		s.Log.Errorf("Failed to fetch flock by name %q: %+v", trimmed, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
	}

	newFlock := &entity.Flock{
		Name:      trimmed,
		CreatedBy: actorID,
	}
	if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
		if errors.Is(err, gorm.ErrDuplicatedKey) {
			return s.FlockRepo.GetByName(ctx, trimmed)
		}
		s.Log.Errorf("Failed to create flock %q: %+v", trimmed, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
	}

	return newFlock, nil
}

func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, periods map[uint]int) error {
	if len(kandangIDs) == 0 {
		return nil
	}

	if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
	}

	already, err := s.pivotRepoWithTx(dbTransaction).ListExistingKandangIDs(ctx, projectFlockID, kandangIDs)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
	}
	exists := make(map[uint]struct{}, len(already))
	for _, id := range already {
		exists[id] = struct{}{}
	}

	var toAttach []uint
	seen := make(map[uint]struct{}, len(kandangIDs))
	for _, id := range kandangIDs {
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		if _, ok := exists[id]; !ok {
			toAttach = append(toAttach, id)
		}
	}
	if len(toAttach) == 0 {
		return nil
	}

	records := make([]*entity.ProjectFlockKandang, 0, len(toAttach))
	for _, id := range toAttach {
		period := periods[id]
		if period <= 0 {
			period = 1
		}
		records = append(records, &entity.ProjectFlockKandang{
			ProjectFlockId: projectFlockID,
			KandangId:      id,
			Period:         period,
		})
	}
	if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
		if errors.Is(err, gorm.ErrDuplicatedKey) {
			return fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terhubung dengan project flock ini")
		}
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
	}
	if err := s.ensureProjectFlockKandangProductWarehouses(ctx, dbTransaction, records); err != nil {
		return err
	}
	return nil
}

func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error {
	if len(kandangIDs) == 0 {
		return nil
	}

	// NOTE: Recording constraints are enforced via FK cascade; allow detachment even if recordings exist.

	pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandang ids")
	}

	if len(pfkIDs) > 0 {
		uniformityRepo := uniformityRepository.NewUniformityRepository(s.Repository.DB())
		if dbTransaction != nil {
			uniformityRepo = uniformityRepository.NewUniformityRepository(dbTransaction)
		}
		if err := uniformityRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang")
		}

		db := s.Repository.DB()
		if dbTransaction != nil {
			db = dbTransaction
		}
		purchaseRepo := purchaseRepository.NewPurchaseRepository(db)
		if err := purchaseRepo.SoftDeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to soft delete purchases for project flock kandang")
		}
		pwRepo := s.ProductWarehouseRepo
		if dbTransaction != nil {
			pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
		} else if pwRepo == nil {
			pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
		}
		if err := pwRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove product warehouses for project flock kandang")
		}
	}

	if resetStatus {
		if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
		}
	}

	if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
	}
	return nil
}

func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
	if dbTransaction == nil {
		return s.pivotRepo()
	}
	return s.pivotRepo().WithTx(dbTransaction)
}

func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository {
	if s.PivotRepo != nil {
		return s.PivotRepo
	}
	return repository.NewProjectFlockKandangRepository(s.Repository.DB())
}

func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.KandangRepository {
	if tx != nil {
		return kandangRepository.NewKandangRepository(tx)
	}
	if s.KandangRepo != nil {
		return s.KandangRepo
	}
	return kandangRepository.NewKandangRepository(s.Repository.DB())
}

func resolveWarehouseByKandangAndLocation(ctx context.Context, warehouseRepo warehouseRepository.WarehouseRepository, kandangID uint, locationID uint) (*entity.Warehouse, error) {
	if warehouseRepo == nil {
		return nil, gorm.ErrRecordNotFound
	}
	if kandangID == 0 {
		return nil, gorm.ErrRecordNotFound
	}
	if locationID != 0 {
		warehouse, err := warehouseRepo.GetByKandangIDAndLocationID(ctx, kandangID, locationID)
		if err == nil {
			return warehouse, nil
		}
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}
	return warehouseRepo.GetByKandangID(ctx, kandangID)
}

func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx context.Context, dbTransaction *gorm.DB, records []*entity.ProjectFlockKandang) error {
	if len(records) == 0 {
		return nil
	}

	projectFlockID := records[0].ProjectFlockId
	if projectFlockID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Project flock id tidak ditemukan")
	}

	pwRepo := s.ProductWarehouseRepo
	if dbTransaction != nil {
		pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
	} else if pwRepo == nil {
		pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
	}

	warehouseRepo := s.WarehouseRepo
	if dbTransaction != nil {
		warehouseRepo = warehouseRepository.NewWarehouseRepository(dbTransaction)
	} else if warehouseRepo == nil {
		warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB())
	}

	db := s.Repository.DB()
	if dbTransaction != nil {
		db = dbTransaction
	}
	type projectFlockMeta struct {
		Category   string `gorm:"column:category"`
		LocationId uint   `gorm:"column:location_id"`
	}
	var flockMeta projectFlockMeta
	if err := db.WithContext(ctx).
		Model(&entity.ProjectFlock{}).
		Select("category, location_id").
		Where("id = ?", projectFlockID).
		Scan(&flockMeta).Error; err != nil {
		return err
	}
	if strings.TrimSpace(flockMeta.Category) == "" {
		return fiber.NewError(fiber.StatusBadRequest, "Project flock category tidak ditemukan")
	}

	prefixes := []string{"AYAM-"}
	if strings.EqualFold(flockMeta.Category, string(utils.ProjectFlockCategoryLaying)) {
		prefixes = append(prefixes, "TELUR")
	}

	invisibleOnly := false
	productIDs, err := pwRepo.ListProductIDsByFlagPrefixes(ctx, prefixes, &invisibleOnly)
	if err != nil {
		return err
	}
	if len(productIDs) == 0 {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product dengan flag %s tidak ditemukan", strings.Join(prefixes, ", ")))
	}

	for _, record := range records {
		if record == nil || record.Id == 0 {
			continue
		}

		warehouse, err := resolveWarehouseByKandangAndLocation(ctx, warehouseRepo, record.KandangId, flockMeta.LocationId)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse untuk kandang %d belum tersedia", record.KandangId))
			}
			return err
		}

		for _, productID := range productIDs {
			if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil {
				continue
			} else if !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}

			newPW := entity.ProductWarehouse{
				ProductId:             productID,
				WarehouseId:           warehouse.Id,
				ProjectFlockKandangId: &record.Id,
				Quantity:              0,
			}
			if err := pwRepo.CreateOne(ctx, &newPW, nil); err != nil {
				return err
			}
		}
	}

	return nil
}

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

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

	existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
	}
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
	}

	kandangIDs := uniqueUintSlice(req.KandangIds)
	kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data kandang")
	}
	if len(kandangs) != len(kandangIDs) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
	}

	for _, pb := range req.ProjectBudgets {
		if err := commonSvc.EnsureRelations(c.Context(),
			commonSvc.RelationCheck{Name: "Nonstock", ID: &pb.NonstockId, Exists: s.NonstockRepo.IdExists},
		); err != nil {
			return nil, err
		}
	}

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {

		approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))

		var period int = 1
		if len(existing.KandangHistory) > 0 {
			period = existing.KandangHistory[0].Period
		}

		periods := make(map[uint]int, len(kandangIDs))
		for _, kandangID := range kandangIDs {
			periods[kandangID] = period
		}

		if err := s.attachKandangs(c.Context(), dbTransaction, existing.Id, kandangIDs, periods); err != nil {
			return err
		}
		if err := s.UpsertProjectBudget(c.Context(), dbTransaction, existing.Id, req.ProjectBudgets); err != nil {
			return err
		}

		action := entity.ApprovalActionUpdated
		_, err = approvalSvc.CreateApproval(
			c.Context(),
			utils.ApprovalWorkflowProjectFlock,
			existing.Id,
			utils.ProjectFlockStepPengajuan,
			&action,
			actorID,
			nil,
		)
		return err
	})

	if err != nil {
		if fiberErr, ok := err.(*fiber.Error); ok {
			return nil, fiberErr
		}
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
		}

		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengajukan ulang project flock")
	}

	return s.getOneEntityOnly(c, id)
}

func (s projectflockService) UpsertProjectBudget(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, budgets []validation.ProjectBudget) error {

	if len(budgets) == 0 {
		return nil
	}
	budgetRepo := projectBudgetRepository.NewProjectBudgetRepository(dbTransaction)

	nonstockMap := make(map[uint]bool)
	relationChecks := make([]commonSvc.RelationCheck, 0, len(budgets))
	for _, b := range budgets {
		if nonstockMap[b.NonstockId] {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate nonstock_id: %d", b.NonstockId))
		}
		nonstockMap[b.NonstockId] = true
		nonstockID := b.NonstockId
		relationChecks = append(relationChecks, commonSvc.RelationCheck{
			Name:   "Nonstock",
			ID:     &nonstockID,
			Exists: s.NonstockRepo.IdExists,
		})
	}

	if err := commonSvc.EnsureRelations(ctx, relationChecks...); err != nil {
		return err
	}

	if err := budgetRepo.DeleteMany(ctx, func(q *gorm.DB) *gorm.DB {
		return q.Where("project_flock_id = ?", projectFlockID)
	}); err != nil && err != gorm.ErrRecordNotFound {
		return err
	}

	records := make([]*entity.ProjectBudget, 0, len(budgets))
	for _, b := range budgets {
		records = append(records, &entity.ProjectBudget{
			ProjectFlockId: projectFlockID,
			NonstockId:     b.NonstockId,
			Price:          b.Price,
			Qty:            b.Qty,
		})
	}

	if err := budgetRepo.CreateMany(ctx, records, nil); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to save project budgets")
	}

	return nil

}
