package service

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

	commonService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	"gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"

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

type DashboardService interface {
	GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error)
	DB() *gorm.DB
}

type dashboardService struct {
	Log        *logrus.Logger
	Validate   *validator.Validate
	Repository repository.DashboardRepository
	HppSvc     commonService.HppV2Service
}

func NewDashboardService(repo repository.DashboardRepository, validate *validator.Validate, hppSvc commonService.HppV2Service) DashboardService {
	return &dashboardService{
		Log:        utils.Log,
		Validate:   validate,
		Repository: repo,
		HppSvc:     hppSvc,
	}
}

func (s dashboardService) DB() *gorm.DB {
	return s.Repository.DB()
}

func (s dashboardService) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return dto.DashboardPerformanceOverviewDTO{}, 0, err
	}

	filter := &validation.DashboardFilter{
		LokasiIds:  params.LokasiIds,
		FlockIds:   params.FlockIds,
		KandangIds: params.KandangIds,
	}

	statistics, err := s.buildPerformanceStatistics(ctx, params, filter)
	if err != nil {
		return dto.DashboardPerformanceOverviewDTO{}, 0, err
	}

	charts, err := s.buildPerformanceCharts(ctx, params, filter)
	if err != nil {
		return dto.DashboardPerformanceOverviewDTO{}, 0, err
	}

	response := dto.DashboardPerformanceOverviewDTO{
		StatisticsData: statistics,
		Charts:         charts,
	}

	if len(params.Include) > 0 {
		include := map[string]bool{}
		for _, item := range params.Include {
			include[item] = true
		}
		if !include["statistics"] {
			response.StatisticsData = []dto.DashboardStatisticsDTO{}
		}
		if !include["charts"] {
			response.Charts = map[string]dto.DashboardChartDTO{}
		}
	}
	if response.StatisticsData == nil {
		response.StatisticsData = []dto.DashboardStatisticsDTO{}
	}
	if response.Charts == nil {
		response.Charts = map[string]dto.DashboardChartDTO{}
	}

	return response, 1, nil
}

func (s dashboardService) buildPerformanceStatistics(ctx context.Context, params *validation.Query, filter *validation.DashboardFilter) ([]dto.DashboardStatisticsDTO, error) {
	location, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		return nil, fmt.Errorf("failed to load timezone configuration: %w", err)
	}

	if params.PeriodStart.IsZero() || params.PeriodEnd.IsZero() || params.PeriodEndExclusive.IsZero() {
		return nil, errors.New("period dates are not initialized")
	}
	startDate := params.PeriodStart
	endDate := params.PeriodEnd
	endExclusive := params.PeriodEndExclusive

	globalStartDate, globalEndDate, globalEndExclusive := currentPeriodDates(location)

	hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, globalStartDate, globalEndExclusive, globalEndDate, location)
	if err != nil {
		return nil, err
	}

	sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, globalEndDate, location)
	if err != nil {
		return nil, err
	}

	hasFilter := filter != nil && (len(filter.LokasiIds) > 0 || len(filter.FlockIds) > 0 || len(filter.KandangIds) > 0)
	fcrCurrent := 0.0
	fcrLast := 0.0
	mortalityCurrent := 0.0
	mortalityLast := 0.0
	if hasFilter {
		fcrCurrent, fcrLast, err = s.calculateFcr(ctx, filter, startDate, endExclusive, endDate, location)
		if err != nil {
			return nil, err
		}
		mortalityCurrent, mortalityLast, err = s.calculateMortality(ctx, filter, startDate, endExclusive, endDate, location)
		if err != nil {
			return nil, err
		}
	}

	hppPercent := percentDelta(hppCurrent, hppLast)
	sellingPercent := percentDelta(sellingCurrent, sellingLast)

	stats := []dto.DashboardStatisticsDTO{
		{
			Label:            "HPP Global",
			Value:            roundTo(hppCurrent, 0),
			PercentLastMonth: roundTo(hppPercent*100, 2),
		},
		{
			Label:            "Avg. Selling Price",
			Value:            roundTo(sellingCurrent, 0),
			PercentLastMonth: roundTo(sellingPercent*100, 2),
		},
	}

	if hasFilter {
		fcrPercent := percentDelta(fcrCurrent, fcrLast)
		mortalityPercent := percentDelta(mortalityCurrent, mortalityLast)
		stats = append(stats,
			dto.DashboardStatisticsDTO{
				Label:            "FCR",
				Value:            roundTo(fcrCurrent, 2),
				PercentLastMonth: roundTo(fcrPercent*100, 2),
			},
			dto.DashboardStatisticsDTO{
				Label:            "Mortality",
				Value:            roundTo(mortalityCurrent, 2),
				PercentLastMonth: roundTo(mortalityPercent*100, 2),
			},
		)
	}

	return stats, nil
}

func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *validation.Query, filter *validation.DashboardFilter) (map[string]dto.DashboardChartDTO, error) {
	if params.AnalysisMode == validation.AnalysisModeComparison {
		return s.buildComparisonCharts(ctx, params, filter)
	}

	if params.PeriodStart.IsZero() || params.PeriodEndExclusive.IsZero() {
		return nil, errors.New("period dates are not initialized")
	}

	if filter == nil || (len(filter.LokasiIds) == 0 && len(filter.FlockIds) == 0 && len(filter.KandangIds) == 0) {
		return map[string]dto.DashboardChartDTO{}, nil
	}

	startDate := params.PeriodStart
	endExclusive := params.PeriodEndExclusive

	recordings, err := s.Repository.GetRecordingWeeklyMetrics(ctx, startDate, endExclusive, filter)
	if err != nil {
		return nil, err
	}

	uniformities, err := s.Repository.GetUniformityWeeklyMetrics(ctx, startDate, endExclusive, filter)
	if err != nil {
		return nil, err
	}

	weekSet := map[int]struct{}{}
	for _, row := range recordings {
		if row.Week > 0 {
			weekSet[row.Week] = struct{}{}
		}
	}
	for _, row := range uniformities {
		if row.Week > 0 {
			weekSet[row.Week] = struct{}{}
		}
	}

	weeks := make([]int, 0, len(weekSet))
	for week := range weekSet {
		weeks = append(weeks, week)
	}
	sort.Ints(weeks)

	standards, err := s.Repository.GetStandardWeeklyMetrics(ctx, weeks, filter)
	if err != nil {
		return nil, err
	}

	standardFcr, err := s.Repository.GetStandardFcrWeekly(ctx, weeks, filter)
	if err != nil {
		return nil, err
	}

	recordingMap := map[int]repository.RecordingWeeklyMetric{}
	for _, row := range recordings {
		recordingMap[row.Week] = row
	}

	uniformityMap := map[int]repository.UniformityWeeklyMetric{}
	for _, row := range uniformities {
		uniformityMap[row.Week] = row
	}

	standardMap := map[int]repository.StandardWeeklyMetric{}
	for _, row := range standards {
		standardMap[row.Week] = row
	}

	standardFcrMap := map[int]float64{}
	for _, row := range standardFcr {
		standardFcrMap[row.Week] = row.StdFcr
	}

	weeklyEggs, err := s.Repository.GetEggWeightWeeklyGrams(ctx, startDate, endExclusive, filter)
	if err != nil {
		return nil, err
	}
	weeklyFeedRows, err := s.Repository.GetFeedUsageWeeklyByUom(ctx, startDate, endExclusive, filter)
	if err != nil {
		return nil, err
	}

	weeklyEggMap := map[int]float64{}
	for _, row := range weeklyEggs {
		weeklyEggMap[row.Week] = row.EggWeightGrams
	}

	weeklyFeedMap := map[int]float64{}
	for _, row := range weeklyFeedRows {
		weeklyFeedMap[row.Week] += feedUsageRowToGrams(row.TotalQty, row.UomName)
	}

	bodyWeightDataset := make([]map[string]interface{}, 0, len(weeks))
	bodyWeightDatasetIndexByWeek := make(map[int]int, len(weeks))
	performanceDataset := make([]map[string]interface{}, 0, len(weeks))
	fcrDataset := make([]map[string]interface{}, 0, len(weeks))
	deplesiDataset := make([]map[string]interface{}, 0, len(weeks))
	qualityDataset := make([]map[string]interface{}, 0, len(weeks))

	cumEgg := 0.0
	cumFeed := 0.0

	for _, week := range weeks {
		rec, hasRec := recordingMap[week]
		uni, hasUni := uniformityMap[week]
		std, hasStd := standardMap[week]
		stdFcr, hasStdFcr := standardFcrMap[week]
		weekEgg := weeklyEggMap[week]
		weekFeed := weeklyFeedMap[week]

		actFcr := 0.0
		if weekEgg > 0 {
			actFcr = weekFeed / weekEgg
		}

		cumEgg += weekEgg
		cumFeed += weekFeed
		actFcrCum := 0.0
		if cumEgg > 0 {
			actFcrCum = cumFeed / cumEgg
		}

		bodyWeightRow := map[string]interface{}{
			"week": week,
		}
		if hasUni {
			bodyWeightRow["body_weight"] = roundTo(uni.AverageWeight, 2)
		}
		if hasStd {
			bodyWeightRow["std_body_weight"] = roundTo(std.StdBodyWeight, 2)
		}
		if len(bodyWeightRow) > 1 {
			bodyWeightDataset = append(bodyWeightDataset, bodyWeightRow)
		}

		performanceRow := map[string]interface{}{
			"week": week,
		}
		if hasRec {
			performanceRow["act_laying"] = roundTo(rec.HenDay, 2)
			performanceRow["act_egg_weight"] = roundTo(rec.EggWeight, 2)
			performanceRow["act_feed_intake"] = roundTo(rec.FeedIntake, 2)
		}
		if hasUni {
			performanceRow["act_uniformity"] = roundTo(uni.Uniformity, 2)
		}
		if hasStd {
			performanceRow["std_laying"] = roundTo(std.StdLaying, 2)
			performanceRow["std_egg_weight"] = roundTo(std.StdEggWeight, 2)
			performanceRow["std_feed_intake"] = roundTo(std.StdFeedIntake, 2)
			performanceRow["std_uniformity"] = roundTo(std.StdUniformity, 2)
		}
		if len(performanceRow) > 1 {
			performanceDataset = append(performanceDataset, performanceRow)
		}

		fcrRow := map[string]interface{}{
			"week": week,
		}
		if weekEgg > 0 && weekFeed > 0 {
			fcrRow["act_fcr"] = roundTo(actFcr, 2)
		}
		if cumEgg > 0 && cumFeed > 0 {
			fcrRow["act_fcr_cum"] = roundTo(actFcrCum, 2)
		}
		if hasStdFcr {
			fcrRow["std_fcr"] = roundTo(stdFcr, 2)
			fcrRow["std_fcr_cum"] = roundTo(stdFcr, 2)
		}
		if len(fcrRow) > 1 {
			fcrDataset = append(fcrDataset, fcrRow)
		}

		deplesiRow := map[string]interface{}{
			"week": week,
		}
		if hasRec {
			deplesiRow["act_deplesi"] = roundTo(rec.CumDepletionRate, 2)
		}
		if hasStd {
			deplesiRow["std_deplesi"] = roundTo(std.StdDepletion, 2)
		}
		if len(deplesiRow) > 1 {
			deplesiDataset = append(deplesiDataset, deplesiRow)
		}
	}

	bodyWeightDataset = extendBodyWeightDatasetUntilEndDate(
		bodyWeightDataset,
		bodyWeightDatasetIndexByWeek,
		uniformities,
		uniformityMap,
		standardMap,
		params.PeriodEnd,
	)

	qualityRows, err := s.Repository.GetEggQualityWeeklyMetrics(ctx, startDate, endExclusive, filter)
	if err != nil {
		return nil, err
	}

	for _, row := range qualityRows {
		normalPercent := 0.0
		abnormalPercent := 0.0
		if row.TotalQty > 0 {
			normalPercent = (row.NormalQty / row.TotalQty) * 100
			abnormalPercent = (row.AbnormalQty / row.TotalQty) * 100
		}
		qualityDataset = append(qualityDataset, map[string]interface{}{
			"week":     row.Week,
			"normal":   roundTo(normalPercent, 2),
			"abnormal": roundTo(abnormalPercent, 2),
		})
	}

	charts := map[string]dto.DashboardChartDTO{
		"body_weight": {
			Series: []dto.DashboardChartSeriesDTO{
				{Id: "body_weight", Label: "Body Weight", Unit: "g"},
				{Id: "std_body_weight", Label: "STD. Body Weight", Unit: "g"},
			},
			Dataset: bodyWeightDataset,
		},
		"performance": {
			Series: []dto.DashboardChartSeriesDTO{
				{Id: "act_laying", Label: "Act. % Laying", Unit: "%"},
				{Id: "std_laying", Label: "STD. % Laying", Unit: "%"},
				{Id: "act_egg_weight", Label: "Act. Egg Weight", Unit: "%"},
				{Id: "std_egg_weight", Label: "STD. Egg Weight", Unit: "%"},
				{Id: "act_feed_intake", Label: "Act. Feed Intake", Unit: "%"},
				{Id: "std_feed_intake", Label: "STD. Feed Intake", Unit: "%"},
				{Id: "act_uniformity", Label: "Act. Uniformity", Unit: "%"},
				{Id: "std_uniformity", Label: "STD. Uniformity", Unit: "%"},
			},
			Dataset: performanceDataset,
		},
		"fcr": {
			Series: []dto.DashboardChartSeriesDTO{
				{Id: "act_fcr", Label: "Act. FCR", Unit: "kg/kg"},
				{Id: "std_fcr", Label: "STD. FCR", Unit: "kg/kg"},
				{Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "kg/kg"},
				{Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "kg/kg"},
			},
			Dataset: fcrDataset,
		},
		"deplesi": {
			Series: []dto.DashboardChartSeriesDTO{
				{Id: "act_deplesi", Label: "Act. Deplesi", Unit: "%"},
				{Id: "std_deplesi", Label: "STD. Deplesi", Unit: "%"},
			},
			Dataset: deplesiDataset,
		},
		"quality_control": {
			Series: []dto.DashboardChartSeriesDTO{
				{Id: "normal", Label: "Normal", Unit: "%"},
				{Id: "abnormal", Label: "Abnormal", Unit: "%"},
			},
			Dataset: qualityDataset,
		},
	}

	return charts, nil
}

func (s dashboardService) buildComparisonCharts(ctx context.Context, params *validation.Query, filter *validation.DashboardFilter) (map[string]dto.DashboardChartDTO, error) {
	if params.PeriodStart.IsZero() || params.PeriodEndExclusive.IsZero() {
		return nil, errors.New("period dates are not initialized")
	}

	startDate := params.PeriodStart
	endExclusive := params.PeriodEndExclusive

	metric := strings.ToLower(strings.TrimSpace(params.Metric))
	if metric == "" {
		return s.buildComparisonChartsAll(ctx, startDate, endExclusive, params, filter)
	}

	seriesRows, err := s.Repository.GetComparisonSeries(ctx, startDate, endExclusive, filter, params.ComparisonType)
	if err != nil {
		return nil, err
	}

	metricRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, metric)
	if err != nil {
		return nil, err
	}

	weeks, actualMap := mapComparisonWeeklyMetricRows(metricRows)
	if len(weeks) == 0 {
		return map[string]dto.DashboardChartDTO{}, nil
	}

	standardMap, err := s.standardComparisonMap(ctx, weeks, metric, filter)
	if err != nil {
		return nil, err
	}

	chart := buildComparisonPercentChart(seriesRows, weeks, actualMap, standardMap)
	return map[string]dto.DashboardChartDTO{
		strings.ToLower(params.ComparisonType): chart,
	}, nil
}

func (s dashboardService) buildComparisonChartsAll(ctx context.Context, startDate, endExclusive time.Time, params *validation.Query, filter *validation.DashboardFilter) (map[string]dto.DashboardChartDTO, error) {
	seriesRows, err := s.Repository.GetComparisonSeries(ctx, startDate, endExclusive, filter, params.ComparisonType)
	if err != nil {
		return nil, err
	}

	layingRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricLaying)
	if err != nil {
		return nil, err
	}
	eggWeightRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricEggWeight)
	if err != nil {
		return nil, err
	}
	feedIntakeRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricFeedIntake)
	if err != nil {
		return nil, err
	}
	fcrRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricFcr)
	if err != nil {
		return nil, err
	}
	deplesiRows, err := s.Repository.GetComparisonWeeklyMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType, validation.MetricMortality)
	if err != nil {
		return nil, err
	}
	uniformityRows, err := s.Repository.GetComparisonWeeklyUniformityMetrics(ctx, startDate, endExclusive, filter, params.ComparisonType)
	if err != nil {
		return nil, err
	}

	weeks := mergeComparisonWeeks(
		layingRows,
		eggWeightRows,
		feedIntakeRows,
		fcrRows,
		deplesiRows,
		uniformityRows,
	)
	if len(weeks) == 0 {
		return map[string]dto.DashboardChartDTO{}, nil
	}

	standards, err := s.Repository.GetStandardWeeklyMetrics(ctx, weeks, filter)
	if err != nil {
		return nil, err
	}
	standardFcr, err := s.Repository.GetStandardFcrWeekly(ctx, weeks, filter)
	if err != nil {
		return nil, err
	}

	stdBodyWeight := map[int]float64{}
	stdLaying := map[int]float64{}
	stdEggWeight := map[int]float64{}
	stdFeedIntake := map[int]float64{}
	stdUniformity := map[int]float64{}
	stdDeplesi := map[int]float64{}
	for _, row := range standards {
		stdBodyWeight[row.Week] = row.StdBodyWeight
		stdLaying[row.Week] = row.StdLaying
		stdEggWeight[row.Week] = row.StdEggWeight
		stdFeedIntake[row.Week] = row.StdFeedIntake
		stdUniformity[row.Week] = row.StdUniformity
		stdDeplesi[row.Week] = row.StdDepletion
	}

	stdFcr := map[int]float64{}
	for _, row := range standardFcr {
		stdFcr[row.Week] = row.StdFcr
	}

	_, layingActual := mapComparisonWeeklyMetricRows(layingRows)
	_, eggWeightActual := mapComparisonWeeklyMetricRows(eggWeightRows)
	_, feedActual := mapComparisonWeeklyMetricRows(feedIntakeRows)
	_, fcrActual := mapComparisonWeeklyMetricRows(fcrRows)
	_, deplesiActual := mapComparisonWeeklyMetricRows(deplesiRows)
	_, bodyWeightActual, _, uniformityActual := mapComparisonUniformityRows(uniformityRows)

	aggregateActual := buildAggregateComparisonPercent(weeks, seriesRows, aggregateComparisonInput{
		BodyWeightActual: bodyWeightActual,
		LayingActual:     layingActual,
		EggWeightActual:  eggWeightActual,
		FeedIntakeActual: feedActual,
		UniformityActual: uniformityActual,
		FcrActual:        fcrActual,
		DeplesiActual:    deplesiActual,
		StdBodyWeight:    stdBodyWeight,
		StdLaying:        stdLaying,
		StdEggWeight:     stdEggWeight,
		StdFeedIntake:    stdFeedIntake,
		StdUniformity:    stdUniformity,
		StdFcr:           stdFcr,
		StdDeplesi:       stdDeplesi,
	})

	if len(aggregateActual) == 0 {
		return map[string]dto.DashboardChartDTO{}, nil
	}

	chartKey := strings.ToLower(params.ComparisonType)
	return map[string]dto.DashboardChartDTO{
		chartKey: buildComparisonAggregateChart(seriesRows, weeks, aggregateActual),
	}, nil
}

type aggregateComparisonInput struct {
	BodyWeightActual map[int]map[uint]float64
	LayingActual     map[int]map[uint]float64
	EggWeightActual  map[int]map[uint]float64
	FeedIntakeActual map[int]map[uint]float64
	UniformityActual map[int]map[uint]float64
	FcrActual        map[int]map[uint]float64
	DeplesiActual    map[int]map[uint]float64
	StdBodyWeight    map[int]float64
	StdLaying        map[int]float64
	StdEggWeight     map[int]float64
	StdFeedIntake    map[int]float64
	StdUniformity    map[int]float64
	StdFcr           map[int]float64
	StdDeplesi       map[int]float64
}

func buildAggregateComparisonPercent(weeks []int, seriesRows []repository.ComparisonSeries, input aggregateComparisonInput) map[int]map[uint]float64 {
	result := map[int]map[uint]float64{}

	for _, week := range weeks {
		stdBodyWeight := input.StdBodyWeight[week]
		stdLaying := input.StdLaying[week]
		stdEggWeight := input.StdEggWeight[week]
		stdFeedIntake := input.StdFeedIntake[week]
		stdUniformity := input.StdUniformity[week]
		stdFcr := input.StdFcr[week]
		stdDeplesi := input.StdDeplesi[week]

		for _, series := range seriesRows {
			sum := 0.0
			count := 0.0

			if percent, ok := higherIsBetterPercent(input.LayingActual, week, series.Id, stdLaying); ok {
				sum += percent
				count++
			}
			if percent, ok := higherIsBetterPercent(input.EggWeightActual, week, series.Id, stdEggWeight); ok {
				sum += percent
				count++
			}
			if percent, ok := higherIsBetterPercent(input.UniformityActual, week, series.Id, stdUniformity); ok {
				sum += percent
				count++
			}
			if percent, ok := lowerIsBetterPercent(input.FcrActual, week, series.Id, stdFcr); ok {
				sum += percent
				count++
			}
			if percent, ok := lowerIsBetterPercent(input.DeplesiActual, week, series.Id, stdDeplesi); ok {
				sum += percent
				count++
			}
			if percent, ok := higherIsBetterPercent(input.BodyWeightActual, week, series.Id, stdBodyWeight); ok {
				sum += percent
				count++
			}
			if percent, ok := lowerIsBetterPercent(input.FeedIntakeActual, week, series.Id, stdFeedIntake); ok {
				sum += percent
				count++
			}

			if result[week] == nil {
				result[week] = map[uint]float64{}
			}
			if count == 0 {
				result[week][series.Id] = 0
				continue
			}
			result[week][series.Id] = sum / count
		}
	}

	return result
}

func higherIsBetterPercent(actual map[int]map[uint]float64, week int, seriesId uint, standard float64) (float64, bool) {
	if standard <= 0 {
		return 0, false
	}
	val, ok := metricValue(actual, week, seriesId)
	if !ok || val <= 0 {
		return 0, false
	}
	return clampPercent((val / standard) * 100), true
}

func lowerIsBetterPercent(actual map[int]map[uint]float64, week int, seriesId uint, standard float64) (float64, bool) {
	if standard <= 0 {
		return 0, false
	}
	val, ok := metricValue(actual, week, seriesId)
	if !ok || val <= 0 {
		return 0, false
	}
	return clampPercent((standard / val) * 100), true
}

func metricValue(actual map[int]map[uint]float64, week int, seriesId uint) (float64, bool) {
	weekRows, ok := actual[week]
	if !ok {
		return 0, false
	}
	val, ok := weekRows[seriesId]
	return val, ok
}

func clampPercent(value float64) float64 {
	if value < 0 {
		return 0
	}
	if value > 200 {
		return 200
	}
	return value
}

func buildComparisonAggregateChart(seriesRows []repository.ComparisonSeries, weeks []int, actual map[int]map[uint]float64) dto.DashboardChartDTO {
	series := make([]dto.DashboardChartSeriesDTO, 0, len(seriesRows))
	for _, sRow := range seriesRows {
		series = append(series, dto.DashboardChartSeriesDTO{
			Id:    strconv.FormatUint(uint64(sRow.Id), 10),
			Label: sRow.Label,
			Unit:  "%",
		})
	}

	dataset := make([]map[string]interface{}, 0, len(weeks))
	for _, week := range weeks {
		row := map[string]interface{}{
			"week": week,
		}
		values, ok := actual[week]
		if !ok {
			continue
		}
		for _, sRow := range seriesRows {
			if val, exists := values[sRow.Id]; exists {
				row[strconv.FormatUint(uint64(sRow.Id), 10)] = roundTo(val, 2)
			}
		}
		if len(row) > 1 {
			dataset = append(dataset, row)
		}
	}

	return dto.DashboardChartDTO{
		Series:  series,
		Dataset: dataset,
	}
}

func (s dashboardService) standardComparisonMap(ctx context.Context, weeks []int, metric string, filter *validation.DashboardFilter) (map[int]float64, error) {
	switch metric {
	case validation.MetricFcr:
		rows, err := s.Repository.GetStandardFcrWeekly(ctx, weeks, filter)
		if err != nil {
			return nil, err
		}
		result := map[int]float64{}
		for _, row := range rows {
			result[row.Week] = row.StdFcr
		}
		return result, nil
	case validation.MetricLaying, validation.MetricEggWeight, validation.MetricFeedIntake, validation.MetricMortality:
		rows, err := s.Repository.GetStandardWeeklyMetrics(ctx, weeks, filter)
		if err != nil {
			return nil, err
		}
		result := map[int]float64{}
		for _, row := range rows {
			switch metric {
			case validation.MetricLaying:
				result[row.Week] = row.StdLaying
			case validation.MetricEggWeight:
				result[row.Week] = row.StdEggWeight
			case validation.MetricFeedIntake:
				result[row.Week] = row.StdFeedIntake
			case validation.MetricMortality:
				result[row.Week] = row.StdDepletion
			}
		}
		return result, nil
	default:
		return map[int]float64{}, nil
	}
}

func mapComparisonWeeklyMetricRows(rows []repository.ComparisonWeeklyMetric) ([]int, map[int]map[uint]float64) {
	weekSet := map[int]struct{}{}
	values := map[int]map[uint]float64{}
	for _, row := range rows {
		if row.Week <= 0 {
			continue
		}
		weekSet[row.Week] = struct{}{}
		if values[row.Week] == nil {
			values[row.Week] = map[uint]float64{}
		}
		values[row.Week][row.SeriesId] = row.Value
	}

	weeks := make([]int, 0, len(weekSet))
	for week := range weekSet {
		weeks = append(weeks, week)
	}
	sort.Ints(weeks)
	return weeks, values
}

func mapComparisonUniformityRows(rows []repository.ComparisonUniformityMetric) ([]int, map[int]map[uint]float64, []int, map[int]map[uint]float64) {
	bodyWeightSet := map[int]struct{}{}
	bodyWeightValues := map[int]map[uint]float64{}
	uniformitySet := map[int]struct{}{}
	uniformityValues := map[int]map[uint]float64{}

	for _, row := range rows {
		if row.Week <= 0 {
			continue
		}
		bodyWeightSet[row.Week] = struct{}{}
		uniformitySet[row.Week] = struct{}{}
		if bodyWeightValues[row.Week] == nil {
			bodyWeightValues[row.Week] = map[uint]float64{}
		}
		if uniformityValues[row.Week] == nil {
			uniformityValues[row.Week] = map[uint]float64{}
		}
		bodyWeightValues[row.Week][row.SeriesId] = row.AverageWeight
		uniformityValues[row.Week][row.SeriesId] = row.Uniformity
	}

	bodyWeightWeeks := make([]int, 0, len(bodyWeightSet))
	for week := range bodyWeightSet {
		bodyWeightWeeks = append(bodyWeightWeeks, week)
	}
	sort.Ints(bodyWeightWeeks)

	uniformityWeeks := make([]int, 0, len(uniformitySet))
	for week := range uniformitySet {
		uniformityWeeks = append(uniformityWeeks, week)
	}
	sort.Ints(uniformityWeeks)

	return bodyWeightWeeks, bodyWeightValues, uniformityWeeks, uniformityValues
}

func mergeComparisonWeeks(rows ...interface{}) []int {
	weekSet := map[int]struct{}{}
	for _, row := range rows {
		switch typed := row.(type) {
		case []repository.ComparisonWeeklyMetric:
			for _, item := range typed {
				if item.Week > 0 {
					weekSet[item.Week] = struct{}{}
				}
			}
		case []repository.ComparisonUniformityMetric:
			for _, item := range typed {
				if item.Week > 0 {
					weekSet[item.Week] = struct{}{}
				}
			}
		}
	}

	weeks := make([]int, 0, len(weekSet))
	for week := range weekSet {
		weeks = append(weeks, week)
	}
	sort.Ints(weeks)
	return weeks
}

func buildComparisonPercentChart(seriesRows []repository.ComparisonSeries, weeks []int, actual map[int]map[uint]float64, standard map[int]float64) dto.DashboardChartDTO {
	series := make([]dto.DashboardChartSeriesDTO, 0, len(seriesRows))
	for _, row := range seriesRows {
		series = append(series, dto.DashboardChartSeriesDTO{
			Id:    strconv.FormatUint(uint64(row.Id), 10),
			Label: row.Label,
			Unit:  "%",
		})
	}

	dataset := make([]map[string]interface{}, 0, len(weeks))
	for _, week := range weeks {
		row := map[string]interface{}{
			"week": week,
		}
		std := standard[week]
		for _, sRow := range seriesRows {
			key := strconv.FormatUint(uint64(sRow.Id), 10)
			actualVal := actual[week][sRow.Id]
			percent := 0.0
			if std > 0 {
				percent = (actualVal / std) * 100
			}
			row[key] = roundTo(percent, 2)
		}
		dataset = append(dataset, row)
	}

	return dto.DashboardChartDTO{
		Series:  series,
		Dataset: dataset,
	}
}

func percentDelta(current, last float64) float64 {
	if last <= 0 {
		return 0
	}
	return (current - last) / last
}

func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
	if s.HppSvc != nil {
		currentHpp, err := s.hppGlobalForPeriod(ctx, startDate, endExclusive)
		if err != nil {
			return 0, 0, err
		}

		lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
		lastHpp, err := s.hppGlobalForPeriod(ctx, lastMonthStart, lastMonthEndExclusive)
		if err != nil {
			return 0, 0, err
		}

		return currentHpp, lastHpp, nil
	}

	totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, nil)
	if err != nil {
		return 0, 0, err
	}
	totalCost, err := s.sumHppCost(ctx, nil, startDate, endExclusive)
	if err != nil {
		return 0, 0, err
	}

	hppCurrent := 0.0
	if totalEggKg > 0 {
		hppCurrent = totalCost / totalEggKg
	}

	lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
	lastEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, lastMonthStart, lastMonthEndExclusive, nil)
	if err != nil {
		return 0, 0, err
	}
	lastCost, err := s.sumHppCost(ctx, nil, lastMonthStart, lastMonthEndExclusive)
	if err != nil {
		return 0, 0, err
	}

	hppLast := 0.0
	if lastEggKg > 0 {
		hppLast = lastCost / lastEggKg
	}

	return hppCurrent, hppLast, nil
}

func (s dashboardService) hppGlobalForPeriod(ctx context.Context, startDate, endExclusive time.Time) (float64, error) {
	kandangIDs, err := s.Repository.ListProjectFlockKandangIDsByEggProduction(ctx, startDate, endExclusive, nil)
	if err != nil {
		return 0, err
	}
	if len(kandangIDs) == 0 {
		return 0, nil
	}

	endOfPeriod := endExclusive.Add(-time.Nanosecond)
	totalCost := 0.0
	totalWeightKg := 0.0
	for _, kandangID := range kandangIDs {
		hppCost, err := s.HppSvc.CalculateHppCost(kandangID, &endOfPeriod)
		if err != nil {
			return 0, err
		}
		if hppCost == nil {
			continue
		}
		totalCost += hppCost.Estimation.Total
		totalWeightKg += hppCost.Estimation.Kg
	}

	if totalWeightKg <= 0 {
		return 0, nil
	}

	return totalCost / totalWeightKg, nil
}

func (s dashboardService) calculateSellingPrice(ctx context.Context, endDate time.Time, location *time.Location) (float64, float64, error) {
	startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
	currentEndExclusive := endDate.AddDate(0, 0, 1)

	currentAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, currentEndExclusive)
	if err != nil {
		return 0, 0, err
	}

	lastAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, endPrevMonthExclusive)
	if err != nil {
		return 0, 0, err
	}

	return currentAvg, lastAvg, nil
}

func (s dashboardService) calculateFcr(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
	current, err := s.fcrValue(ctx, filter, startDate, endExclusive)
	if err != nil {
		return 0, 0, err
	}

	lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
	last, err := s.fcrValue(ctx, filter, lastMonthStart, lastMonthEndExclusive)
	if err != nil {
		return 0, 0, err
	}

	return current, last, nil
}

func (s dashboardService) calculateMortality(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
	current, err := s.mortalityValue(ctx, filter, startDate, endExclusive)
	if err != nil {
		return 0, 0, err
	}

	lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
	last, err := s.mortalityValue(ctx, filter, lastMonthStart, lastMonthEndExclusive)
	if err != nil {
		return 0, 0, err
	}

	return current, last, nil
}

func (s dashboardService) fcrValue(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) {
	eggWeightGrams, err := s.Repository.SumEggProductionWeightGrams(ctx, startDate, endExclusive, filter)
	if err != nil {
		return 0, err
	}

	feedRows, err := s.Repository.GetFeedUsageByUom(ctx, startDate, endExclusive, filter)
	if err != nil {
		return 0, err
	}
	feedUsageGrams := feedUsageToGrams(feedRows)

	if eggWeightGrams <= 0 {
		return 0, nil
	}

	return feedUsageGrams / eggWeightGrams, nil
}

func (s dashboardService) mortalityValue(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) {
	depletions, err := s.Repository.SumDepletions(ctx, startDate, endExclusive, filter)
	if err != nil {
		return 0, err
	}

	initialPopulation, err := s.Repository.SumInitialPopulation(ctx, endExclusive.AddDate(0, 0, -1), filter)
	if err != nil {
		return 0, err
	}

	if initialPopulation <= 0 {
		return 0, nil
	}

	return (depletions / initialPopulation) * 100, nil
}

func (s dashboardService) sumHppCost(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) {
	sapronak, err := s.Repository.SumSapronakCost(ctx, startDate, endExclusive, filter)
	if err != nil {
		return 0, err
	}

	bop, err := s.Repository.SumBopCost(ctx, startDate, endExclusive, filter)
	if err != nil {
		return 0, err
	}

	ekspedisi, err := s.Repository.SumEkspedisiCost(ctx, startDate, endExclusive, filter)
	if err != nil {
		return 0, err
	}

	return sapronak + bop + ekspedisi, nil
}

func (s dashboardService) avgSellingPrice(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) {
	result, err := s.Repository.SumSellingPrice(ctx, startDate, endExclusive, filter)
	if err != nil {
		return 0, err
	}

	if result.TotalWeight <= 0 {
		return 0, nil
	}

	return result.TotalPrice / result.TotalWeight, nil
}

func extendBodyWeightDatasetUntilEndDate(
	dataset []map[string]interface{},
	indexByWeek map[int]int,
	uniformities []repository.UniformityWeeklyMetric,
	uniformityMap map[int]repository.UniformityWeeklyMetric,
	standardMap map[int]repository.StandardWeeklyMetric,
	periodEnd time.Time,
) []map[string]interface{} {
	latestUniformityWeek := 0
	var latestUniformityDate time.Time
	for _, row := range uniformities {
		if row.Week <= 0 || row.UniformDate.IsZero() {
			continue
		}
		if latestUniformityDate.IsZero() || row.UniformDate.After(latestUniformityDate) || (row.UniformDate.Equal(latestUniformityDate) && row.Week > latestUniformityWeek) {
			latestUniformityDate = row.UniformDate
			latestUniformityWeek = row.Week
		}
	}

	if latestUniformityWeek <= 0 || latestUniformityDate.IsZero() || periodEnd.IsZero() || !periodEnd.After(latestUniformityDate) {
		return dataset
	}

	additionalWeeks := int(math.Ceil(periodEnd.Sub(latestUniformityDate).Hours() / (24 * 7)))
	if additionalWeeks <= 0 {
		return dataset
	}

	lastUniformity := uniformityMap[latestUniformityWeek]
	lastStandard := standardMap[latestUniformityWeek]
	latestBodyWeight := roundTo(lastUniformity.AverageWeight, 2)
	latestStdBodyWeight := roundTo(lastStandard.StdBodyWeight, 2)

	targetWeek := latestUniformityWeek + additionalWeeks
	for week := latestUniformityWeek + 1; week <= targetWeek; week++ {
		row := map[string]interface{}{
			"week":            week,
			"body_weight":     latestBodyWeight,
			"std_body_weight": latestStdBodyWeight,
		}

		if idx, ok := indexByWeek[week]; ok {
			dataset[idx] = row
			continue
		}

		dataset = append(dataset, row)
		indexByWeek[week] = len(dataset) - 1
	}

	sort.Slice(dataset, func(i, j int) bool {
		return datasetWeek(dataset[i]) < datasetWeek(dataset[j])
	})

	return dataset
}

func datasetWeek(row map[string]interface{}) int {
	week, _ := row["week"].(int)
	return week
}

func feedUsageToGrams(rows []repository.FeedUsageByUom) float64 {
	total := 0.0
	for _, row := range rows {
		total += feedUsageRowToGrams(row.TotalQty, row.UomName)
	}
	return total
}

func feedUsageRowToGrams(totalQty float64, uomName string) float64 {
	if totalQty <= 0 {
		return 0
	}
	switch strings.TrimSpace(strings.ToLower(uomName)) {
	case "kilogram", "kg", "kilograms", "kilo":
		return totalQty * 1000
	case "gram", "g", "grams":
		return totalQty
	default:
		return totalQty
	}
}

func roundTo(value float64, decimals int) float64 {
	if decimals <= 0 {
		return math.Round(value)
	}
	multiplier := math.Pow(10, float64(decimals))
	return math.Round(value*multiplier) / multiplier
}

func monthRange(t time.Time, location *time.Location) (time.Time, time.Time) {
	start := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, location)
	endExclusive := start.AddDate(0, 1, 0)
	return start, endExclusive
}

func currentPeriodDates(location *time.Location) (time.Time, time.Time, time.Time) {
	now := time.Now().In(location)
	startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location)
	endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
	endExclusive := endDate.AddDate(0, 0, 1)
	return startDate, endDate, endExclusive
}
