package recording

import (
	"context"
	"fmt"
	"strings"

	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"
	rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gorm.io/gorm"
)

type warnLogger interface {
	Warnf(format string, args ...any)
}

type productWarehouseExistsRepo interface {
	ExistsByID(ctx context.Context, id uint) (bool, error)
}

type recordingValidationRepo interface {
	ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error)
}

func EnsureProductWarehousesExist(ctx context.Context, repo productWarehouseExistsRepo, ids []uint) error {
	if repo == nil || len(ids) == 0 {
		return nil
	}
	for _, id := range ids {
		ok, err := repo.ExistsByID(ctx, id)
		if err != nil {
			return err
		}
		if !ok {
			return fmt.Errorf("product warehouse %d not found", id)
		}
	}
	return nil
}

func EnsureProductWarehousesByFlags(ctx context.Context, repo recordingValidationRepo, ids []uint, flags []string, label string) error {
	if repo == nil || len(ids) == 0 {
		return nil
	}
	invalidID, err := repo.ValidateProductWarehousesByFlags(ctx, ids, flags)
	if err != nil {
		return err
	}
	if invalidID != 0 {
		return fmt.Errorf("product warehouse %d is not a %s warehouse", invalidID, label)
	}
	return nil
}

type idGetter[T any] func(T) uint

func CollectWarehouseIDs[T any](items []T, getID idGetter[T]) []uint {
	if len(items) == 0 {
		return nil
	}
	ids := make([]uint, 0, len(items))
	for _, item := range items {
		if id := getID(item); id != 0 {
			ids = append(ids, id)
		}
	}
	return ids
}

func EnsureProductWarehousesByFlagsForItems[T any](
	ctx context.Context,
	repo recordingValidationRepo,
	items []T,
	getID idGetter[T],
	flags []string,
	label string,
) error {
	ids := CollectWarehouseIDs(items, getID)
	return EnsureProductWarehousesByFlags(ctx, repo, ids, flags, label)
}

func ComputeDepletionRate(prevRecording *entity.Recording, currentDepletion float64, totalChick int64) float64 {
	base := 0.0
	if prevRecording != nil && prevRecording.TotalChickQty != nil && *prevRecording.TotalChickQty > 0 {
		base = *prevRecording.TotalChickQty
	} else if totalChick > 0 {
		base = float64(totalChick) + currentDepletion
	}
	if base <= 0 {
		return 0
	}
	return (currentDepletion / base) * 100
}

func AttachLatestApprovals(ctx context.Context, items []entity.Recording, approvalSvc commonSvc.ApprovalService, logger warnLogger) error {
	if len(items) == 0 || 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 := approvalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowRecording, ids, func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	})
	if err != nil {
		if logger != nil {
			logger.Warnf("Unable to load latest approvals for recordings: %+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 AttachLatestApproval(ctx context.Context, item *entity.Recording, approvalSvc commonSvc.ApprovalService, logger warnLogger) error {
	if item == nil || item.Id == 0 || approvalSvc == nil {
		return nil
	}

	latest, err := approvalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, item.Id, func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	})
	if err != nil {
		if logger != nil {
			logger.Warnf("Unable to load approvals for recording %d: %+v", item.Id, err)
		}
		return nil
	}
	item.LatestApproval = latest
	return nil
}

type productionStandardValues struct {
	HenDay       *float64
	HenHouse     *float64
	FeedIntake   *float64
	MaxDepletion *float64
	EggMass      *float64
	EggWeight    *float64
}

func AttachProductionStandards(ctx context.Context, db *gorm.DB, warnOnly bool, logger warnLogger, items ...*entity.Recording) error {
	if len(items) == 0 {
		return nil
	}

	type standardKey struct {
		standardID uint
		week       int
	}
	type standardCacheEntry struct {
		values productionStandardValues
		fcr    *float64
	}

	if db == nil {
		return nil
	}

	standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
	growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
	cache := make(map[standardKey]standardCacheEntry, len(items))

	standardIDs := make(map[uint]struct{}, len(items))
	for _, item := range items {
		if item == nil || item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 {
			continue
		}
		if item.ProjectFlockKandang.ProjectFlock.ProductionStandardId > 0 {
			standardIDs[item.ProjectFlockKandang.ProjectFlock.ProductionStandardId] = struct{}{}
		}
	}

	standardDetailByStd := make(map[uint]map[int]*entity.ProductionStandardDetail, len(standardIDs))
	growthDetailByStd := make(map[uint]map[int]*entity.StandardGrowthDetail, len(standardIDs))

	for standardID := range standardIDs {
		details, err := standardDetailRepo.GetByProductionStandardID(ctx, standardID)
		if err != nil {
			if warnOnly {
				if logger != nil {
					logger.Warnf("Unable to preload production standard detail for standard %d: %+v", standardID, err)
				}
			} else {
				return err
			}
			continue
		}
		detailMap := make(map[int]*entity.ProductionStandardDetail, len(details))
		for i := range details {
			detail := details[i]
			detailMap[detail.Week] = &detail
		}
		standardDetailByStd[standardID] = detailMap

		growths, err := growthDetailRepo.GetByProductionStandardID(ctx, standardID)
		if err != nil {
			if warnOnly {
				if logger != nil {
					logger.Warnf("Unable to preload standard growth detail for standard %d: %+v", standardID, err)
				}
			} else {
				return err
			}
			continue
		}
		growthMap := make(map[int]*entity.StandardGrowthDetail, len(growths))
		for i := range growths {
			growth := growths[i]
			growthMap[growth.Week] = &growth
		}
		growthDetailByStd[standardID] = growthMap
	}

	for _, item := range items {
		if item == nil || item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 {
			continue
		}
		standardID := item.ProjectFlockKandang.ProjectFlock.ProductionStandardId
		if standardID == 0 {
			continue
		}
		week := RecordingWeekValue(*item)
		cacheKey := standardKey{standardID: standardID, week: week}
		if cached, ok := cache[cacheKey]; ok {
			applyProductionStandardValues(item, cached.values, cached.fcr)
			continue
		}
		values := productionStandardValues{}
		var fcr *float64
		if detailMap, ok := standardDetailByStd[standardID]; ok {
			if detail, ok := detailMap[week]; ok {
				values.HenDay = detail.TargetHenDayProduction
				values.HenHouse = detail.TargetHenHouseProduction
				values.EggMass = detail.TargetEggMass
				values.EggWeight = detail.TargetEggWeight
				fcr = detail.StandardFCR
			}
		}
		if growthMap, ok := growthDetailByStd[standardID]; ok {
			if growth, ok := growthMap[week]; ok {
				values.FeedIntake = growth.FeedIntake
				values.MaxDepletion = growth.MaxDepletion
			}
		}
		cache[cacheKey] = standardCacheEntry{values: values, fcr: fcr}
		applyProductionStandardValues(item, values, fcr)
	}

	return nil
}

func applyProductionStandardValues(item *entity.Recording, values productionStandardValues, fcr *float64) {
	item.StandardHenDay = values.HenDay
	item.StandardHenHouse = values.HenHouse
	item.StandardFeedIntake = values.FeedIntake
	item.StandardMaxDepletion = values.MaxDepletion
	item.StandardEggMass = values.EggMass
	item.StandardEggWeight = values.EggWeight
	item.StandardFcr = fcr
}

func RecordingWeekValue(e entity.Recording) int {
	day := intValue(e.Day)
	if day <= 0 {
		return 0
	}
	weekBase := 1
	if IsLayingRecording(e) {
		weekBase = config.LayingWeekStart()
	}
	return ((day - 1) / 7) + weekBase
}

func IsLayingRecording(e entity.Recording) bool {
	if e.ProjectFlockKandang == nil {
		return false
	}
	return strings.EqualFold(e.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying))
}

func intValue(value *int) int {
	if value == nil {
		return 0
	}
	return *value
}
