package service

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

	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"
	flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
	kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
	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, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
	CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
	GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error)
	GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
	Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
}

type projectflockService struct {
	Log              *logrus.Logger
	Validate         *validator.Validate
	Repository       repository.ProjectflockRepository
	FlockRepo        flockRepository.FlockRepository
	KandangRepo      kandangRepository.KandangRepository
	PivotRepo        repository.ProjectFlockKandangRepository
	ApprovalSvc      commonSvc.ApprovalService
	approvalWorkflow approvalutils.ApprovalWorkflowKey
}

type FlockPeriodSummary struct {
	Flock      entity.Flock
	NextPeriod int
}

func NewProjectflockService(
	repo repository.ProjectflockRepository,
	flockRepo flockRepository.FlockRepository,
	kandangRepo kandangRepository.KandangRepository,
	pivotRepo repository.ProjectFlockKandangRepository,
	approvalSvc commonSvc.ApprovalService,
	validate *validator.Validate,
) ProjectflockService {
	return &projectflockService{
		Log:              utils.Log,
		Validate:         validate,
		Repository:       repo,
		FlockRepo:        flockRepo,
		KandangRepo:      kandangRepo,
		PivotRepo:        pivotRepo,
		ApprovalSvc:      approvalSvc,
		approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
	}
}

func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
	return db.
		Preload("CreatedUser").
		Preload("Flock").
		Preload("Area").
		Preload("Fcr").
		Preload("Location").
		Preload("Kandangs")
}

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

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

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

	projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
		db = s.withRelations(db)

		if params.AreaId > 0 {
			db = db.Where("project_flocks.area_id = ?", params.AreaId)
		}
		if params.LocationId > 0 {
			db = db.Where("project_flocks.location_id = ?", params.LocationId)
		}
		if params.Period > 0 {
			db = db.Where("project_flocks.period = ?", params.Period)
		}
		if len(params.KandangIds) > 0 {
			db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
		}

		if params.Search != "" {
			normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
			if normalizedSearch == "" {
				for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
					db = db.Order(expr)
				}
				return db
			}
			likeQuery := "%" + normalizedSearch + "%"
			db = db.
				Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
				Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
				Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
				Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
				Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
				Where(`
					LOWER(flocks.name) LIKE ?
					OR LOWER(areas.name) LIKE ?
					OR LOWER(project_flocks.category) LIKE ?
					OR LOWER(fcrs.name) LIKE ?
					OR LOWER(locations.name) LIKE ?
					OR LOWER(locations.address) LIKE ?
					OR LOWER(created_users.name) LIKE ?
					OR LOWER(created_users.email) LIKE ?
					OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
					OR EXISTS (
						SELECT 1 FROM kandangs
						WHERE kandangs.project_flock_id = project_flocks.id
						AND LOWER(kandangs.name) LIKE ?
					)
				`,
					likeQuery,
					likeQuery,
					likeQuery,
					likeQuery,
					likeQuery,
					likeQuery,
					likeQuery,
					likeQuery,
					likeQuery,
					likeQuery,
				)
		}
		for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
			db = db.Order(expr)
		}
		return db
	})

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

	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, func(db *gorm.DB) *gorm.DB {
			return db.Preload("ActionUser")
		})
		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
				}
			}
		}
	}

	return projectflocks, total, nil
}

func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
	projectflock, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
	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, err
	}

	if s.ApprovalSvc != nil {
		approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB {
			return db.Preload("ActionUser")
		})
		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) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) {
	if err := s.Validate.Struct(req); 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")
	}

	if err := commonSvc.EnsureRelations(c.Context(),
		commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())},
		commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())},
		commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())},
		commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
	); err != nil {
		return nil, err
	}

	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.ProjectFlockId != nil {
			return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name))
		}
	}

	createBody := &entity.ProjectFlock{
		FlockId:    req.FlockId,
		AreaId:     req.AreaId,
		Category:   cat,
		FcrId:      req.FcrId,
		LocationId: req.LocationId,
		CreatedBy:  1,
	}

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

		period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId)
		if err != nil {
			return err
		}
		createBody.Period = period

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

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

		actorID := uint(1) //TODO: Change From Auth
		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 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, err
	}

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

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

	existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
	}
	if err != nil {
		s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
	}
	updateBody := make(map[string]any)
	hasBodyChanges := false
	var relationChecks []commonSvc.RelationCheck

	if req.FlockId != nil {
		updateBody["flock_id"] = *req.FlockId
		hasBodyChanges = true
		relationChecks = append(relationChecks, commonSvc.RelationCheck{
			Name:   "Flock",
			ID:     req.FlockId,
			Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
		})
	}
	if req.AreaId != nil {
		updateBody["area_id"] = *req.AreaId
		hasBodyChanges = true
		relationChecks = append(relationChecks, commonSvc.RelationCheck{
			Name:   "Area",
			ID:     req.AreaId,
			Exists: relationExistsChecker[entity.Area](s.Repository.DB()),
		})
	}
	if req.Category != nil {
		cat := strings.ToUpper(*req.Category)
		if !utils.IsValidProjectFlockCategory(cat) {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
		}

		updateBody["category"] = cat
	}
	if req.FcrId != nil {
		updateBody["fcr_id"] = *req.FcrId
		hasBodyChanges = true
		relationChecks = append(relationChecks, commonSvc.RelationCheck{
			Name:   "FCR",
			ID:     req.FcrId,
			Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()),
		})
	}
	if req.LocationId != nil {
		updateBody["location_id"] = *req.LocationId
		hasBodyChanges = true
		relationChecks = append(relationChecks, commonSvc.RelationCheck{
			Name:   "Location",
			ID:     req.LocationId,
			Exists: relationExistsChecker[entity.Location](s.Repository.DB()),
		})
	}

	if len(relationChecks) > 0 {
		if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil {
			return nil, err
		}
	}

	var newKandangIDs []uint
	hasKandangChanges := false
	if req.KandangIds != nil {
		hasKandangChanges = true
		if len(req.KandangIds) == 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty")
		}
		newKandangIDs = uniqueUintSlice(req.KandangIds)
		kandangs, err := s.KandangRepo.GetByIDs(c.Context(), newKandangIDs, 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(newKandangIDs) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
		}
		for _, k := range kandangs {
			if k.ProjectFlockId != nil && *k.ProjectFlockId != id {
				return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name))
			}
		}
	}

	hasChanges := hasBodyChanges || hasKandangChanges
	if !hasChanges {
		return s.GetOne(c, id)
	}

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

		if len(updateBody) > 0 {
			if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
				return err
			}
		} else {
			if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil {
				return err
			}
		}

		if req.KandangIds != nil {
			existingIDs := make(map[uint]struct{}, len(existing.Kandangs))
			for _, k := range existing.Kandangs {
				existingIDs[k.Id] = struct{}{}
			}
			newSet := make(map[uint]struct{}, len(newKandangIDs))
			for _, kid := range newKandangIDs {
				newSet[kid] = struct{}{}
			}

			var toDetach []uint
			for kid := range existingIDs {
				if _, ok := newSet[kid]; !ok {
					toDetach = append(toDetach, kid)
				}
			}

			var toAttach []uint
			for kid := range newSet {
				if _, ok := existingIDs[kid]; !ok {
					toAttach = append(toAttach, kid)
				}
			}

			if len(toDetach) > 0 {
				if err := s.detachKandangs(c.Context(), dbTransaction, id, toDetach, true); err != nil {
					return err
				}
			}

			if len(toAttach) > 0 {
				if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil {
					return err
				}
			}
		}

		if hasChanges {
			actorID := uint(1) //TODO: Change From Auth
			approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
			if approvalSvc != nil {
				latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
				if err != nil {
					return err
				}
				shouldRecordUpdate := latestBeforeReset == nil ||
					latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) ||
					latestBeforeReset.Action == nil ||
					(latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated)

				if shouldRecordUpdate {
					action := entity.ApprovalActionUpdated
					if _, err := approvalSvc.CreateApproval(
						c.Context(),
						utils.ApprovalWorkflowProjectFlock,
						id,
						utils.ProjectFlockStepPengajuan,
						&action,
						actorID,
						nil,
					); 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 update projectflock %d: %+v", id, err)
		return nil, err
	}

	return s.GetOne(c, id)
}

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 := uint(1) // TODO: change from auth context
	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)

		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
				}
			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.GetOne(c, approvableID)
		if err != nil {
			return nil, err
		}
		updated = append(updated, *project)
	}

	return updated, nil
}

func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
	existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
	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 err
	}

	return nil
}

func (s projectflockService) GetProjectFlockKandang(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, error) {
	// keep for backward compatibility; delegate to new consolidated method
	return s.GetProjectFlockKandangByParams(ctx, fmt.Sprintf("%d", id), "", "")
}

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

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

func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, 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, 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, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
			}
			return nil, err
		}
		return pfk, nil
	}

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

func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) {
	flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB {
		return db.Preload("CreatedUser")
	})
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get flock %d for period summary: %+v", flockID, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
	}

	maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID)
	if err != nil {
		s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period")
	}

	return &FlockPeriodSummary{
		Flock:      *flock,
		NextPeriod: maxPeriod + 1,
	}, 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 relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool, error) {
	return func(ctx context.Context, id uint) (bool, error) {
		return commonRepo.Exists[T](ctx, db, id)
	}
}

func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []string {
	direction := "ASC"
	if strings.ToLower(sortOrder) == "desc" {
		direction = "DESC"
	}

	switch sortBy {
	case "area":
		return []string{
			fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
			fmt.Sprintf("project_flocks.id %s", direction),
		}
	case "location":
		return []string{
			fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
			fmt.Sprintf("project_flocks.id %s", direction),
		}
	case "kandangs":
		return []string{
			fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction),
			fmt.Sprintf("project_flocks.id %s", direction),
		}
	case "period":
		return []string{
			fmt.Sprintf("project_flocks.period %s", direction),
			fmt.Sprintf("project_flocks.id %s", direction),
		}
	default:
		return []string{
			"project_flocks.created_at DESC",
			"project_flocks.updated_at DESC",
		}
	}
}

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

	if err := dbTransaction.Model(&entity.Kandang{}).
		Where("id IN ?", kandangIDs).
		Updates(map[string]any{
			"project_flock_id": projectFlockID,
			"status":           string(utils.KandangStatusPengajuan),
		}).Error; err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
	}

	pivotRepo := s.pivotRepoWithTx(dbTransaction)
	records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
	for i, id := range kandangIDs {
		records[i] = &entity.ProjectFlockKandang{
			ProjectFlockId: projectFlockID,
			KandangId:      id,
		}
	}
	if err := pivotRepo.CreateMany(ctx, records); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
	}
	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
	}

	updates := map[string]any{"project_flock_id": nil}
	if resetStatus {
		updates["status"] = string(utils.KandangStatusNonActive)
	}

	if err := dbTransaction.Model(&entity.Kandang{}).
		Where("id IN ?", kandangIDs).
		Updates(updates).Error; err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
	}

	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 s.PivotRepo == nil {
		return repository.NewProjectFlockKandangRepository(dbTransaction)
	}
	return s.PivotRepo.WithTx(dbTransaction)
}
