package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"mime/multipart"
	"net/http"
	"sort"
	"strings"
	"time"

	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	"gitlab.com/mbugroup/lti-api.git/internal/config"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
	rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
	utypes "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/types"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
	"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 UniformityService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
	GetSummary(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
	GetStandard(ctx *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error)
	MapStandards(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityStandard, error)
	MapCharts(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityChartData, error)
	MapDocuments(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error)
	CreateOne(ctx *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
	Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
	ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
	ComputeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error)
	GetDocumentInfo(ctx *fiber.Ctx, uniformityID uint) (*entity.Document, string, error)
	CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (utypes.UniformityCalculation, *entity.Document, string, error)
}

type uniformityService struct {
	Log                      *logrus.Logger
	Validate                 *validator.Validate
	Repository               repository.UniformityRepository
	DocumentSvc              commonSvc.DocumentService
	ApprovalRepo             commonRepo.ApprovalRepository
	ApprovalSvc              commonSvc.ApprovalService
	ProjectFlockKandangRepo  rProjectFlock.ProjectFlockKandangRepository
	ProductionStandardRepo   rProductionStandard.ProductionStandardRepository
	StandardGrowthDetailRepo rProductionStandard.StandardGrowthDetailRepository
}

func NewUniformityService(
	repo repository.UniformityRepository,
	documentSvc commonSvc.DocumentService,
	approvalRepo commonRepo.ApprovalRepository,
	approvalSvc commonSvc.ApprovalService,
	projectFlockKandangRepo rProjectFlock.ProjectFlockKandangRepository,
	productionStandardRepo rProductionStandard.ProductionStandardRepository,
	standardGrowthDetailRepo rProductionStandard.StandardGrowthDetailRepository,
	validate *validator.Validate,
) UniformityService {
	return &uniformityService{
		Log:                      utils.Log,
		Validate:                 validate,
		Repository:               repo,
		DocumentSvc:              documentSvc,
		ApprovalRepo:             approvalRepo,
		ApprovalSvc:              approvalSvc,
		ProjectFlockKandangRepo:  projectFlockKandangRepo,
		ProductionStandardRepo:   productionStandardRepo,
		StandardGrowthDetailRepo: standardGrowthDetailRepo,
	}
}

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

	var scopeErr error

	offset := (params.Page - 1) * params.Limit
	uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params, func(db *gorm.DB) *gorm.DB {
		db = db.
			Joins("JOIN project_flock_kandangs pfk ON pfk.id = project_flock_kandang_uniformity.project_flock_kandang_id").
			Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id")
		db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id")
		return db
	})

	if scopeErr != nil {
		return nil, 0, scopeErr
	}
	if err != nil {
		s.Log.Errorf("Failed to get uniformitys: %+v", err)
		return nil, 0, err
	}
	s.normalizeUniformityWeeks(uniformitys)
	if err := s.attachLatestApprovals(c.Context(), uniformitys); err != nil {
		return nil, 0, err
	}
	return uniformitys, total, nil
}

func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
	if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil {
		return nil, err
	}

	uniformity, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get uniformity by id: %+v", err)
		return nil, err
	}
	s.normalizeUniformityWeek(uniformity)
	if err := s.attachLatestApproval(c.Context(), uniformity); err != nil {
		return nil, err
	}
	return uniformity, nil
}

func (s uniformityService) GetSummary(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
	return s.GetOne(c, id)
}

func (s *uniformityService) normalizeUniformityWeeks(items []entity.ProjectFlockKandangUniformity) {
	for i := range items {
		s.normalizeUniformityWeek(&items[i])
	}
}

func (s *uniformityService) normalizeUniformityWeek(item *entity.ProjectFlockKandangUniformity) {
	if item == nil || item.UniformDate == nil {
		return
	}
	computedWeek, err := s.computeUniformityWeekForPFK(&item.ProjectFlockKandang, *item.UniformDate)
	if err != nil || computedWeek <= 0 {
		return
	}
	item.Week = computedWeek
}

func (s uniformityService) GetStandard(c *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error) {
	if uniformity == nil {
		return nil, nil
	}
	return s.resolveUniformityStandard(c.Context(), *uniformity)
}

func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityStandard, error) {
	if len(items) == 0 {
		return nil, nil
	}
	if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil {
		return nil, nil
	}

	categoryStandard := make(map[string]*entity.ProductionStandard)
	detailCache := make(map[uint]map[int]entity.StandardGrowthDetail)
	result := make(map[uint]utypes.UniformityStandard, len(items))

	for _, item := range items {
		if item.Id == 0 {
			continue
		}
		standard, err := s.resolveCategoryStandard(c.Context(), item.ProjectFlockKandang.ProjectFlock.Category, categoryStandard)
		if err != nil {
			return nil, err
		}
		if standard == nil {
			continue
		}

		weekMap, ok := detailCache[standard.Id]
		if !ok {
			details, err := s.StandardGrowthDetailRepo.GetByProductionStandardID(c.Context(), standard.Id)
			if err != nil {
				return nil, err
			}
			weekMap = make(map[int]entity.StandardGrowthDetail, len(details))
			for _, detail := range details {
				weekMap[detail.Week] = detail
			}
			detailCache[standard.Id] = weekMap
		}

		detail, ok := weekMap[item.Week]
		if !ok {
			continue
		}
		standardDTO := utypes.UniformityStandard{
			MeanWeight: cloneFloat64(detail.TargetMeanBw),
			Uniformity: float64Ptr(detail.MinUniformity),
		}
		result[item.Id] = standardDTO
	}

	return result, nil
}

func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityChartData, error) {
	if len(items) == 0 {
		return nil, nil
	}

	grouped := make(map[uint][]entity.ProjectFlockKandangUniformity)
	for _, item := range items {
		if item.ProjectFlockKandangId == 0 {
			continue
		}
		grouped[item.ProjectFlockKandangId] = append(grouped[item.ProjectFlockKandangId], item)
	}

	if len(grouped) == 0 {
		return nil, nil
	}

	result := make(map[uint]utypes.UniformityChartData, len(items))

	for _, group := range grouped {
		allWeeks := make(map[int]utypes.UniformityChartWeek)
		weekOrder := make([]int, 0, len(group))
		weeksWithData := 0
		gaugeWeeks := make([]utypes.UniformityChartGaugeWeek, 0, len(group))
		latestByWeek := make(map[int]entity.ProjectFlockKandangUniformity)

		for _, item := range group {
			if item.Week == 0 {
				continue
			}
			if existing, ok := latestByWeek[item.Week]; !ok || isUniformityNewer(item, existing) {
				latestByWeek[item.Week] = item
			}
		}

		for week := range latestByWeek {
			weekOrder = append(weekOrder, week)
		}
		sort.Ints(weekOrder)

		for _, week := range weekOrder {
			item := latestByWeek[week]
			var weekSummary utypes.UniformityChartWeek
			if len(item.ChartData) > 0 {
				if err := json.Unmarshal(item.ChartData, &weekSummary); err != nil {
					return nil, err
				}
			}
			if weekSummary.WeightDistribution == nil {
				weekSummary.WeightDistribution = []utypes.UniformityChartRange{}
			}
			if !weekSummary.HasData && item.ChickQtyOfWeight > 0 {
				weekSummary.HasData = true
			}
			if weekSummary.HasData {
				weeksWithData++
			}
			allWeeks[week] = weekSummary

			hasData := item.ChickQtyOfWeight > 0
			gaugeWeeks = append(gaugeWeeks, utypes.UniformityChartGaugeWeek{
				Week:              week,
				UniformityPercent: item.Uniformity,
				IdealCount:        item.UniformQty,
				OutsideIdealCount: item.NotUniformQty,
				TotalCount:        item.ChickQtyOfWeight,
				HasData:           hasData,
			})
		}

		weekIndex := make(map[int]int, len(weekOrder))
		for idx, week := range weekOrder {
			weekIndex[week] = idx
		}

		totalWeeks := len(weekOrder)
		for _, item := range group {
			if item.Id == 0 || item.Week == 0 {
				continue
			}
			currentIndex := weekIndex[item.Week]
			chart := utypes.UniformityChartData{
				BarChart: utypes.UniformityChartBar{
					CurrentWeek: item.Week,
					AllWeeks:    allWeeks,
				},
				GaugeChart: utypes.UniformityChartGauge{
					CurrentWeek:    item.Week,
					AvailableWeeks: gaugeWeeks,
					WeekInfo: utypes.UniformityChartWeekInfo{
						TotalWeeks:       totalWeeks,
						WeeksWithData:    weeksWithData,
						CurrentWeekIndex: currentIndex,
						HasPrevWeek:      currentIndex > 0,
						HasNextWeek:      currentIndex < totalWeeks-1,
					},
				},
			}
			result[item.Id] = chart
		}
	}

	return result, nil
}

func isUniformityNewer(a, b entity.ProjectFlockKandangUniformity) bool {
	var aDate, bDate time.Time
	if a.UniformDate != nil {
		aDate = *a.UniformDate
	}
	if b.UniformDate != nil {
		bDate = *b.UniformDate
	}
	if !aDate.IsZero() || !bDate.IsZero() {
		if aDate.Equal(bDate) {
			return a.Id > b.Id
		}
		return aDate.After(bDate)
	}
	return a.Id > b.Id
}

func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) {
	if s.DocumentSvc == nil || len(items) == 0 {
		return map[uint]string{}, nil
	}

	result := make(map[uint]string, len(items))
	for _, item := range items {
		if item.Id == 0 {
			continue
		}
		documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(item.Id))
		if err != nil {
			return nil, err
		}
		if len(documents) == 0 {
			continue
		}
		result[item.Id] = documents[len(documents)-1].Name
	}

	return result, nil
}

func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}
	if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, req.ProjectFlockKandangId); err != nil {
		return nil, err
	}
	if s.ProjectFlockKandangRepo == nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available")
	}

	if file == nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
	}

	uniformDate, err := time.Parse("2006-01-02", req.Date)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format")
	}

	if err := commonSvc.EnsureRelations(
		c.Context(),
		commonSvc.RelationCheck{Name: "Project Flock Kandang", ID: &req.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
	); err != nil {
		return nil, err
	}

	pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
		}
		return nil, err
	}
	computedWeek, err := s.computeUniformityWeekForPFK(pfk, uniformDate)
	if err != nil {
		return nil, err
	}
	isGrowingCategory := strings.EqualFold(strings.TrimSpace(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryGrowing))
	if req.Week > 0 && req.Week != computedWeek {
		s.Log.WithFields(logrus.Fields{
			"project_flock_kandang_id": req.ProjectFlockKandangId,
			"uniform_date":             uniformDate.Format("2006-01-02"),
			"requested_week":           req.Week,
			"computed_week":            computedWeek,
		}).Warn("Uniformity week mismatch detected; using computed week")
	}

	var latestWeek int
	if err := s.Repository.DB().WithContext(c.Context()).
		Model(&entity.ProjectFlockKandangUniformity{}).
		Where("project_flock_kandang_id = ?", req.ProjectFlockKandangId).
		Select("COALESCE(MAX(week), 0)").
		Scan(&latestWeek).Error; err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence")
	}
	if latestWeek > 0 && computedWeek > latestWeek+1 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping")
	}
	// if latestWeek > 0 && req.Week > latestWeek+1 {
	// 	return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping")
	// }

	if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, computedWeek); err != nil {
		return nil, err
	}

	if len(rows) == 0 {
		parsedRows, err := s.ParseBodyWeightExcel(c, file)
		if err != nil {
			return nil, err
		}
		rows = parsedRows
	}

	calculation, err := s.ComputeUniformity(rows)
	if err != nil {
		return nil, err
	}
	chartSummary := buildChartWeekSummary(extractWeights(rows))
	chartJSON, err := json.Marshal(chartSummary)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "failed to build chart data")
	}
	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return nil, err
	}

	createBody := &entity.ProjectFlockKandangUniformity{
		Uniformity:            calculation.Uniformity,
		Week:                  computedWeek,
		Cv:                    calculation.Cv,
		ChickQtyOfWeight:      calculation.ChickQtyOfWeight,
		MeanUp:                calculation.MeanUp,
		MeanDown:              calculation.MeanDown,
		ProjectFlockKandangId: req.ProjectFlockKandangId,
		UniformQty:            calculation.UniformQty,
		NotUniformQty:         calculation.OutsideQty,
		ChartData:             chartJSON,
		UniformDate:           &uniformDate,
		CreatedBy:             actorID,
	}

	if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
		repoTx := s.Repository.WithTx(tx)
		if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
			return err
		}
		if err := s.createUniformityApproval(
			c.Context(),
			tx,
			createBody.Id,
			utils.UniformityStepPengajuan,
			entity.ApprovalActionCreated,
			actorID,
			nil,
		); err != nil {
			return err
		}
		if isGrowingCategory {
			if err := s.updateGrowingFcrForWeek(tx, createBody.ProjectFlockKandangId, createBody.Week, calculation.MeanUp); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		s.Log.Errorf("Failed to create uniformity: %+v", err)
		return nil, err
	}

	if s.DocumentSvc != nil {
		actorIDCopy := actorID
		_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
			DocumentableType: "UNIFORMITY",
			DocumentableID:   uint64(createBody.Id),
			CreatedBy:        &actorIDCopy,
			Files: []commonSvc.DocumentFile{
				{
					File: file,
					Type: "UNIFORMITY",
				},
			},
		})
		if err != nil {
			if errDelete := s.ApprovalRepo.DeleteByTarget(c.Context(), utils.ApprovalWorkflowUniformity.String(), createBody.Id); errDelete != nil {
				s.Log.WithError(errDelete).Warnf("Failed to rollback uniformity approvals for %d", createBody.Id)
			}
			if errDelete := s.Repository.DeleteOne(c.Context(), createBody.Id); errDelete != nil {
				s.Log.WithError(errDelete).Warnf("Failed to rollback uniformity %d", createBody.Id)
			}
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
		}
	}

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

func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}
	if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil {
		return nil, err
	}

	updateBody := make(map[string]any)
	var uniformDate *time.Time

	if req.Date != nil {
		parsed, err := time.Parse("2006-01-02", *req.Date)
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format")
		}
		updateBody["uniform_date"] = parsed
		uniformDate = &parsed
	}
	if req.ProjectFlockKandangId != nil {
		if s.ProjectFlockKandangRepo == nil {
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available")
		}
		if err := commonSvc.EnsureRelations(
			c.Context(),
			commonSvc.RelationCheck{Name: "Project Flock Kandang", ID: req.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
		); err != nil {
			return nil, err
		}
		updateBody["project_flock_kandang_id"] = *req.ProjectFlockKandangId
	}

	if req.Date != nil || req.ProjectFlockKandangId != nil || req.Week != nil {
		current, err := s.Repository.GetByID(c.Context(), id, nil)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
			}
			return nil, err
		}
		targetDate := uniformDate
		if targetDate == nil {
			targetDate = current.UniformDate
		}
		targetPFKID := current.ProjectFlockKandangId
		if req.ProjectFlockKandangId != nil {
			targetPFKID = *req.ProjectFlockKandangId
		}
		if targetPFKID != 0 && targetDate != nil {
			pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetPFKID)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
				}
				return nil, err
			}
			computedWeek, err := s.computeUniformityWeekForPFK(pfk, *targetDate)
			if err != nil {
				return nil, err
			}
			if req.Week != nil && *req.Week != computedWeek {
				s.Log.WithFields(logrus.Fields{
					"uniformity_id":            id,
					"project_flock_kandang_id": targetPFKID,
					"uniform_date":             targetDate.Format("2006-01-02"),
					"requested_week":           *req.Week,
					"computed_week":            computedWeek,
				}).Warn("Uniformity week mismatch detected on update; using computed week")
			}
			updateBody["week"] = computedWeek
			if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, computedWeek); err != nil {
				return nil, err
			}
		}
	}

	if file != nil {
		if s.DocumentSvc == nil {
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
		}

		if len(rows) == 0 {
			parsedRows, err := s.ParseBodyWeightExcel(c, file)
			if err != nil {
				return nil, err
			}
			rows = parsedRows
		}

		calculation, err := s.ComputeUniformity(rows)
		if err != nil {
			return nil, err
		}
		chartSummary := buildChartWeekSummary(extractWeights(rows))
		chartJSON, err := json.Marshal(chartSummary)
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, "failed to build chart data")
		}

		updateBody["uniformity"] = calculation.Uniformity
		updateBody["cv"] = calculation.Cv
		updateBody["chick_qty_of_weight"] = calculation.ChickQtyOfWeight
		updateBody["mean_up"] = calculation.MeanUp
		updateBody["mean_down"] = calculation.MeanDown
		updateBody["uniform_qty"] = calculation.UniformQty
		updateBody["not_uniform_qty"] = calculation.OutsideQty
		updateBody["chart_data"] = chartJSON
	}

	if len(updateBody) == 0 {
		return s.GetOne(c, id)
	}

	if file == nil {
		if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
			}
			s.Log.Errorf("Failed to update uniformity: %+v", err)
			return nil, err
		}

		if err := s.updateGrowingFcrFromUniformity(c.Context(), id); err != nil {
			return nil, err
		}
		return s.GetOne(c, id)
	}

	existingDocs, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(id))
	if err != nil {
		return nil, err
	}

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

	actorIDCopy := actorID
	uploadResults, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
		DocumentableType: "UNIFORMITY",
		DocumentableID:   uint64(id),
		CreatedBy:        &actorIDCopy,
		Files: []commonSvc.DocumentFile{
			{
				File: file,
				Type: "UNIFORMITY",
			},
		},
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
	}

	if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
		if len(uploadResults) > 0 {
			ids := make([]uint, 0, len(uploadResults))
			for _, result := range uploadResults {
				if result.Document.Id != 0 {
					ids = append(ids, result.Document.Id)
				}
			}
			if len(ids) > 0 {
				_ = s.DocumentSvc.DeleteDocuments(c.Context(), ids, true)
			}
		}

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
		}
		s.Log.Errorf("Failed to update uniformity: %+v", err)
		return nil, err
	}

	if len(existingDocs) > 0 {
		oldIDs := make([]uint, 0, len(existingDocs))
		for _, doc := range existingDocs {
			if doc.Id != 0 {
				oldIDs = append(oldIDs, doc.Id)
			}
		}
		if len(oldIDs) > 0 {
			_ = s.DocumentSvc.DeleteDocuments(c.Context(), oldIDs, true)
		}
	}

	if err := s.updateGrowingFcrFromUniformity(c.Context(), id); err != nil {
		return nil, err
	}

	return s.GetOne(c, id)
}

func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int) error {
	if projectFlockKandangID == 0 || week == 0 {
		return nil
	}

	query := s.Repository.DB().WithContext(ctx).
		Model(&entity.ProjectFlockKandangUniformity{}).
		Where("project_flock_kandang_id = ? AND week = ?", projectFlockKandangID, week)
	if id != 0 {
		query = query.Where("id <> ?", id)
	}

	var count int64
	if err := query.Count(&count).Error; err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity uniqueness")
	}
	if count > 0 {
		return fiber.NewError(fiber.StatusConflict, "Uniformity already exists for the same project flock kandang and week")
	}
	return nil
}

func (s *uniformityService) computeUniformityWeekForPFK(pfk *entity.ProjectFlockKandang, uniformDate time.Time) (int, error) {
	if pfk == nil || pfk.Id == 0 {
		return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
	}

	chickInDate, ok := earliestUniformityChickInDate(pfk.Chickins)
	if !ok {
		return 0, fiber.NewError(fiber.StatusBadRequest, "Tanggal chick in tidak ditemukan")
	}

	chickInDay := normalizeUniformityDateOnlyUTC(chickInDate)
	uniformDay := normalizeUniformityDateOnlyUTC(uniformDate)
	if uniformDay.Before(chickInDay) {
		return 0, fiber.NewError(fiber.StatusBadRequest, "Uniformity date tidak boleh sebelum tanggal chick in")
	}

	diff := int(uniformDay.Sub(chickInDay).Hours() / 24)
	weekBase := 1
	if strings.EqualFold(strings.TrimSpace(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying)) {
		weekBase = config.LayingWeekStart()
	}

	return (diff / 7) + weekBase, nil
}

func earliestUniformityChickInDate(chickins []entity.ProjectChickin) (time.Time, bool) {
	var earliest time.Time
	for _, chickin := range chickins {
		if chickin.ChickInDate.IsZero() {
			continue
		}
		if earliest.IsZero() || chickin.ChickInDate.Before(earliest) {
			earliest = chickin.ChickInDate
		}
	}
	if earliest.IsZero() {
		return time.Time{}, false
	}
	return earliest, true
}

func normalizeUniformityDateOnlyUTC(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 uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
	if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil {
		return err
	}

	type uniformityContext struct {
		ID                    uint
		Week                  int
		ProjectFlockKandangId uint
		Category              string
	}
	var ctxRow uniformityContext
	if err := s.Repository.DB().WithContext(c.Context()).
		Table("project_flock_kandang_uniformity u").
		Select("u.id, u.week, u.project_flock_kandang_id, pf.category").
		Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id").
		Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
		Where("u.id = ?", id).
		Scan(&ctxRow).Error; err != nil {
		return err
	}

	if ctxRow.ID == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
	}

	if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
		repoTx := s.Repository.WithTx(tx)
		if err := repoTx.DeleteOne(c.Context(), id); err != nil {
			return err
		}

		if strings.EqualFold(ctxRow.Category, string(utils.ProjectFlockCategoryGrowing)) {
			startDay := (ctxRow.Week-1)*7 + 1
			endDay := ctxRow.Week * 7
			if ctxRow.Week > 0 {
				if err := tx.Model(&entity.Recording{}).
					Where("project_flock_kandangs_id = ?", ctxRow.ProjectFlockKandangId).
					Where("day BETWEEN ? AND ?", startDay, endDay).
					Update("fcr_value", 0).Error; err != nil {
					return err
				}
			}
		}

		return nil
	}); err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
		}
		s.Log.Errorf("Failed to delete uniformity: %+v", err)
		return err
	}
	return nil
}

func (s *uniformityService) updateGrowingFcrFromUniformity(ctx context.Context, uniformityID uint) error {
	if uniformityID == 0 {
		return nil
	}

	type uniformityRow struct {
		ID                    uint
		Week                  int
		MeanUp                float64
		ProjectFlockKandangId uint
		Category              string
	}
	var row uniformityRow
	if err := s.Repository.DB().WithContext(ctx).
		Table("project_flock_kandang_uniformity u").
		Select("u.id, u.week, u.mean_up, u.project_flock_kandang_id, pf.category").
		Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id").
		Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
		Where("u.id = ?", uniformityID).
		Scan(&row).Error; err != nil {
		return err
	}
	if row.ID == 0 {
		return nil
	}
	if !strings.EqualFold(row.Category, string(utils.ProjectFlockCategoryGrowing)) {
		return nil
	}

	return s.updateGrowingFcrForWeek(s.Repository.DB().WithContext(ctx), row.ProjectFlockKandangId, row.Week, row.MeanUp)
}

func (s *uniformityService) updateGrowingFcrForWeek(tx *gorm.DB, projectFlockKandangID uint, week int, meanUp float64) error {
	if tx == nil || projectFlockKandangID == 0 || week <= 0 {
		return nil
	}
	startDay := (week-1)*7 + 1
	endDay := week * 7
	meanBw := meanUp / 1.10
	if meanBw <= 0 {
		return tx.Model(&entity.Recording{}).
			Where("project_flock_kandangs_id = ?", projectFlockKandangID).
			Where("day BETWEEN ? AND ?", startDay, endDay).
			Update("fcr_value", 0).Error
	}

	return tx.Model(&entity.Recording{}).
		Where("project_flock_kandangs_id = ?", projectFlockKandangID).
		Where("day BETWEEN ? AND ?", startDay, endDay).
		Update("fcr_value", gorm.Expr("CASE WHEN feed_intake IS NULL OR feed_intake = 0 THEN 0 ELSE feed_intake / ? END", meanBw)).Error
}

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

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

	ids := uniqueUintSlice(req.ApprovableIds)
	if len(ids) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
	}
	for _, id := range ids {
		if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil {
			return nil, err
		}
	}

	step := utils.UniformityStepPengajuan
	if action == entity.ApprovalActionApproved {
		step = utils.UniformityStepDisetujui
	}

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

	ctx := c.Context()
	transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		repoTx := s.Repository.WithTx(tx)
		approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))

		for _, id := range ids {
			if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Uniformity %d not found", id))
				}
				return err
			}

			if _, err := approvalSvc.CreateApproval(
				ctx,
				utils.ApprovalWorkflowUniformity,
				id,
				step,
				&action,
				actorID,
				req.Notes,
			); err != nil {
				return err
			}
		}

		return nil
	})
	if transactionErr != nil {
		if fiberErr, ok := transactionErr.(*fiber.Error); ok {
			return nil, fiberErr
		}
		s.Log.Errorf("Failed to record approvals for uniformities %+v: %+v", ids, transactionErr)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to submit uniformity approval")
	}

	results := make([]entity.ProjectFlockKandangUniformity, 0, len(ids))
	for _, id := range ids {
		loaded, err := s.GetOne(c, id)
		if err != nil {
			return nil, err
		}
		results = append(results, *loaded)
	}

	return results, nil
}

func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error) {
	return computeUniformity(rows)
}

func (s uniformityService) GetDocumentInfo(c *fiber.Ctx, uniformityID uint) (*entity.Document, string, error) {
	return s.fetchUniformityDocument(c.Context(), uniformityID, true)
}

func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (utypes.UniformityCalculation, *entity.Document, string, error) {
	document, url, err := s.fetchUniformityDocument(c.Context(), uniformityID, false)
	if err != nil {
		return utypes.UniformityCalculation{}, nil, "", err
	}
	if document == nil || url == "" {
		return utypes.UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
	}

	req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
	if err != nil {
		return utypes.UniformityCalculation{}, nil, "", err
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return utypes.UniformityCalculation{}, nil, "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return utypes.UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document")
	}

	rows, err := parseBodyWeightExcelReader(resp.Body)
	if err != nil {
		return utypes.UniformityCalculation{}, nil, "", err
	}

	calculation, err := computeUniformity(rows)
	if err != nil {
		return utypes.UniformityCalculation{}, nil, "", err
	}

	return calculation, document, url, nil
}

func (s uniformityService) fetchUniformityDocument(ctx context.Context, uniformityID uint, allowMissing bool) (*entity.Document, string, error) {
	if s.DocumentSvc == nil {
		return nil, "", fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
	}

	documents, err := s.DocumentSvc.ListByTarget(ctx, "UNIFORMITY", uint64(uniformityID))
	if err != nil {
		return nil, "", err
	}
	if len(documents) == 0 {
		if allowMissing {
			return nil, "", nil
		}
		return nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
	}

	document := documents[len(documents)-1]
	url, err := s.DocumentSvc.PresignURL(ctx, document, 15*time.Minute)
	if err != nil {
		return nil, "", err
	}
	if url == "" {
		return nil, "", fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
	}

	return &document, url, nil
}

func (s *uniformityService) createUniformityApproval(
	ctx context.Context,
	db *gorm.DB,
	uniformityID uint,
	step approvalutils.ApprovalStep,
	action entity.ApprovalAction,
	actorID uint,
	notes *string,
) error {
	if uniformityID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Uniformity tidak valid untuk approval")
	}
	if actorID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
	}

	var svc commonSvc.ApprovalService
	if db != nil {
		svc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
	} else if s.ApprovalSvc != nil {
		svc = s.ApprovalSvc
	} else {
		svc = commonSvc.NewApprovalService(s.ApprovalRepo)
	}

	_, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowUniformity, uniformityID, step, &action, actorID, notes)
	return err
}

func (s *uniformityService) attachLatestApprovals(ctx context.Context, items []entity.ProjectFlockKandangUniformity) error {
	if len(items) == 0 || s.ApprovalSvc == nil {
		return nil
	}

	ids := make([]uint, 0, len(items))
	visited := make(map[uint]struct{}, len(items))
	for _, item := range items {
		if item.Id == 0 {
			continue
		}
		if _, ok := visited[item.Id]; ok {
			continue
		}
		visited[item.Id] = struct{}{}
		ids = append(ids, item.Id)
	}

	if len(ids) == 0 {
		return nil
	}

	latestMap, err := s.ApprovalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowUniformity, ids, func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	})
	if err != nil {
		s.Log.Warnf("Unable to load latest approvals for uniformities: %+v", err)
		return nil
	}

	if len(latestMap) == 0 {
		return nil
	}

	for i := range items {
		if items[i].Id == 0 {
			continue
		}
		if approval, ok := latestMap[items[i].Id]; ok {
			items[i].LatestApproval = approval
		}
	}

	return nil
}

func (s *uniformityService) attachLatestApproval(ctx context.Context, item *entity.ProjectFlockKandangUniformity) error {
	if item == nil || item.Id == 0 || s.ApprovalSvc == nil {
		return nil
	}

	approvals, err := s.ApprovalSvc.ListByTarget(ctx, utils.ApprovalWorkflowUniformity, item.Id, func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	})
	if err != nil {
		s.Log.Warnf("Unable to load approvals for uniformity %d: %+v", item.Id, err)
		return nil
	}

	if len(approvals) == 0 {
		item.LatestApproval = nil
		return nil
	}

	latest := approvals[len(approvals)-1]
	item.LatestApproval = &latest
	return nil
}

func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error) {
	if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil {
		return nil, nil
	}

	standard, err := s.resolveCategoryStandard(ctx, item.ProjectFlockKandang.ProjectFlock.Category, nil)
	if err != nil || standard == nil {
		return nil, err
	}

	detail, err := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx, standard.Id, item.Week)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}

	return &utypes.UniformityStandard{
		MeanWeight: cloneFloat64(detail.TargetMeanBw),
		Uniformity: float64Ptr(detail.MinUniformity),
	}, nil
}

func (s *uniformityService) resolveCategoryStandard(
	ctx context.Context,
	category string,
	cache map[string]*entity.ProductionStandard,
) (*entity.ProductionStandard, error) {
	category = strings.TrimSpace(category)
	if category == "" {
		return nil, nil
	}
	if cache != nil {
		if cached, ok := cache[category]; ok {
			return cached, nil
		}
	}

	var standard entity.ProductionStandard
	err := s.ProductionStandardRepo.DB().WithContext(ctx).
		Where("project_category = ?", category).
		Where("deleted_at IS NULL").
		Order("created_at DESC").
		First(&standard).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			if cache != nil {
				cache[category] = nil
			}
			return nil, nil
		}
		return nil, err
	}

	standardCopy := standard
	if cache != nil {
		cache[category] = &standardCopy
	}
	return &standardCopy, nil
}

func cloneFloat64(value *float64) *float64 {
	if value == nil {
		return nil
	}
	copy := *value
	return &copy
}

func float64Ptr(value float64) *float64 {
	copy := value
	return &copy
}

func uniqueUintSlice(values []uint) []uint {
	if len(values) == 0 {
		return nil
	}

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