package service

import (
	"fmt"
	"io"
	"math"
	"mime/multipart"
	"strconv"
	"strings"

	utypes "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/types"

	"github.com/gofiber/fiber/v2"
	"github.com/xuri/excelize/v2"
)

type BodyWeightExcelRow struct {
	No     int     `json:"no"`
	Weight float64 `json:"weight"`
	Range  string  `json:"range,omitempty"`
}

func (s uniformityService) ParseBodyWeightExcel(_ *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) {
	if file == nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "file is required")
	}

	reader, err := file.Open()
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "failed to open file")
	}
	defer reader.Close()

	rows, err := parseBodyWeightExcelReader(reader)
	if err != nil {
		return nil, err
	}

	return rows, nil
}

func parseBodyWeightExcelReader(reader io.Reader) ([]BodyWeightExcelRow, error) {
	xlsx, err := excelize.OpenReader(reader)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read excel file")
	}
	defer func() {
		_ = xlsx.Close()
	}()

	sheets := xlsx.GetSheetList()
	if len(sheets) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "no sheets found in file")
	}

	sheetName := sheets[0]
	if len(sheets) > 1 {
		sheetName = sheets[1]
	}

	rows, err := xlsx.GetRows(sheetName, excelize.Options{RawCellValue: true})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read sheet rows")
	}

	return parseBodyWeightRows(rows)
}

func parseBodyWeightRows(rows [][]string) ([]BodyWeightExcelRow, error) {
	headerRowIdx, noCol, bwCol, rangeCol := findBodyWeightHeader(rows)
	if headerRowIdx < 0 || bwCol < 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "header BW not found")
	}

	result := make([]BodyWeightExcelRow, 0)
	lastNo := 0

	for i := headerRowIdx + 1; i < len(rows); i++ {
		row := rows[i]
		weightStr := cellAt(row, bwCol)
		weightVal, ok := parseNumber(weightStr)
		if !ok {
			continue
		}

		noVal := 0
		if noCol >= 0 {
			if parsed, ok := parseNumber(cellAt(row, noCol)); ok {
				noVal = int(parsed)
			}
		}
		if noVal <= 0 {
			noVal = lastNo + 1
		}
		if noVal > lastNo {
			lastNo = noVal
		}

		rangeVal := ""
		if rangeCol >= 0 {
			rangeVal = strings.TrimSpace(cellAt(row, rangeCol))
		}

		rowPayload := BodyWeightExcelRow{
			No:     noVal,
			Weight: weightVal,
			Range:  rangeVal,
		}
		if rowPayload.No <= 0 || rowPayload.Weight <= 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "invalid body weight row data")
		}

		result = append(result, rowPayload)
	}

	if len(result) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
	}

	return result, nil
}

func findBodyWeightHeader(rows [][]string) (rowIdx int, noCol int, bwCol int, rangeCol int) {
	rowIdx = -1
	noCol = -1
	bwCol = -1
	rangeCol = -1

	for i, row := range rows {
		tempNo := -1
		tempBW := -1
		tempRange := -1
		for j, cell := range row {
			label := normalizeHeader(cell)
			switch label {
			case "no":
				tempNo = j
			case "bw":
				tempBW = j
			case "outsiderange":
				tempRange = j
			default:
				if strings.HasPrefix(label, "bw") {
					tempBW = j
				} else if strings.HasPrefix(label, "no") {
					tempNo = j
				} else if strings.Contains(label, "range") {
					tempRange = j
				}
			}
		}
		if tempBW >= 0 {
			rowIdx = i
			bwCol = tempBW
			noCol = tempNo
			rangeCol = tempRange
			break
		}
	}

	return rowIdx, noCol, bwCol, rangeCol
}

func cellAt(row []string, idx int) string {
	if idx < 0 || idx >= len(row) {
		return ""
	}
	return strings.TrimSpace(row[idx])
}

func normalizeHeader(value string) string {
	trimmed := strings.ToLower(strings.TrimSpace(value))
	if trimmed == "" {
		return ""
	}
	var b strings.Builder
	for _, r := range trimmed {
		if r >= 'a' && r <= 'z' {
			b.WriteRune(r)
		}
	}
	return b.String()
}

func parseNumber(value string) (float64, bool) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return 0, false
	}

	if strings.Contains(trimmed, ",") {
		if strings.Contains(trimmed, ".") {
			trimmed = strings.ReplaceAll(trimmed, ",", "")
		} else {
			trimmed = strings.ReplaceAll(trimmed, ",", ".")
		}
	}

	parsed, err := strconv.ParseFloat(trimmed, 64)
	if err != nil {
		return 0, false
	}
	return parsed, true
}

func computeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error) {
	weights := make([]float64, 0, len(rows))
	details := make([]utypes.UniformityDetailItem, 0, len(rows))
	hasRangeLabels := false
	for idx, row := range rows {
		if row.Weight <= 0 {
			continue
		}
		id := row.No
		if id <= 0 {
			id = idx + 1
		}
		weights = append(weights, row.Weight)
		rangeLabel := strings.TrimSpace(row.Range)
		if rangeLabel != "" {
			upper := strings.ToUpper(rangeLabel)
			if upper == "HIGH" || upper == "LOW" {
				hasRangeLabels = true
			}
			rangeLabel = upper
		}
		details = append(details, utypes.UniformityDetailItem{
			Id:     id,
			Weight: row.Weight,
			Range:  rangeLabel,
		})
	}

	total := float64(len(weights))
	if total == 0 {
		return utypes.UniformityCalculation{}, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
	}

	var sum float64
	for _, w := range weights {
		sum += w
	}
	mean := sum / total
	meanUpThreshold := roundToPrecision(mean*1.10, 3)
	meanDownThreshold := roundToPrecision(mean*0.90, 3)

	var uniformCount float64
	for i := range details {
		if hasRangeLabels {
			if details[i].Range == "HIGH" || details[i].Range == "LOW" {
				details[i].Range = "Outside"
				continue
			}
			details[i].Range = "Ideal"
			uniformCount++
			continue
		}

		if details[i].Weight > meanUpThreshold || details[i].Weight < meanDownThreshold {
			details[i].Range = "Outside"
			continue
		}
		details[i].Range = "Ideal"
		uniformCount++
	}

	var deviationSum float64
	for _, w := range weights {
		deviation := w - mean
		deviationSum += deviation * deviation
	}
	stdDev := math.Sqrt(deviationSum / total)

	cv := 0.0
	if mean != 0 {
		cv = (stdDev / mean) * 100
	}

	outsideCount := total - uniformCount
	uniformity := 0.0
	if total > 0 {
		uniformity = (uniformCount / total) * 100
	}

	return utypes.UniformityCalculation{
		ChickQtyOfWeight: total,
		MeanWeight:       roundToPrecision(mean, 0),
		MeanDown:         roundToPrecision(meanDownThreshold, 0),
		MeanUp:           roundToPrecision(meanUpThreshold, 0),
		UniformQty:       uniformCount,
		OutsideQty:       outsideCount,
		Uniformity:       roundToPrecision(uniformity, 0),
		Cv:               roundToPrecision(cv, 1),
		Details:          details,
	}, nil
}

func extractWeights(rows []BodyWeightExcelRow) []float64 {
	weights := make([]float64, 0, len(rows))
	for _, row := range rows {
		if row.Weight <= 0 {
			continue
		}
		weights = append(weights, row.Weight)
	}
	return weights
}

func buildChartWeekSummary(weights []float64) utypes.UniformityChartWeek {
	if len(weights) == 0 {
		return utypes.UniformityChartWeek{
			HasData:            false,
			WeightDistribution: []utypes.UniformityChartRange{},
		}
	}

	minWeight := weights[0]
	maxWeight := weights[0]
	var sum float64
	for _, w := range weights {
		sum += w
		if w < minWeight {
			minWeight = w
		}
		if w > maxWeight {
			maxWeight = w
		}
	}
	mean := sum / float64(len(weights))

	idealMin := roundToPrecision(mean*0.90, 0)
	idealMax := roundToPrecision(mean*1.10, 0)
	idealCount := 0.0
	for _, w := range weights {
		if w >= idealMin && w <= idealMax {
			idealCount++
		}
	}

	const bucketSize = 5.0
	start := math.Floor(minWeight/bucketSize) * bucketSize
	end := math.Floor(maxWeight/bucketSize) * bucketSize

	distribution := make([]utypes.UniformityChartRange, 0)
	for bucket := start; bucket <= end; bucket += bucketSize {
		minBucket := bucket
		maxBucket := bucket + bucketSize - 1
		count := 0.0
		bucketWeights := make([]float64, 0)
		idealWeights := make([]float64, 0)
		outsideWeights := make([]float64, 0)
		for _, w := range weights {
			if w >= minBucket && w < minBucket+bucketSize {
				count++
				bucketWeights = append(bucketWeights, w)
				if w >= idealMin && w <= idealMax {
					idealWeights = append(idealWeights, w)
				} else {
					outsideWeights = append(outsideWeights, w)
				}
			}
		}
		idealRangeLabel := rangeFromValues(idealWeights)
		outsideRangeLabel := rangeFromValues(outsideWeights)
		isIdealRange := idealRangeLabel != ""
		distribution = append(distribution, utypes.UniformityChartRange{
			Range:        fmt.Sprintf("%d-%d", int(minBucket), int(maxBucket)),
			MinWeight:    minBucket,
			MaxWeight:    maxBucket,
			BirdCount:    count,
			IsIdealRange: isIdealRange,
			IdealRange:   idealRangeLabel,
			OutsideRange: outsideRangeLabel,
		})
	}

	statistics := &utypes.UniformityChartStatistics{
		MinWeight:          roundToPrecision(minWeight, 0),
		MaxWeight:          roundToPrecision(maxWeight, 0),
		AverageWeight:      roundToPrecision(mean, 1),
		TotalBirdsMeasured: float64(len(weights)),
	}

	return utypes.UniformityChartWeek{
		HasData:            true,
		WeightDistribution: distribution,
		IdealRange: &utypes.UniformityChartIdealRange{
			MinWeight:       idealMin,
			MaxWeight:       idealMax,
			TotalIdealBirds: idealCount,
		},
		Statistics: statistics,
	}
}

func roundToPrecision(value float64, precision int) float64 {
	if precision < 0 {
		return value
	}
	scale := math.Pow10(precision)
	scaled := value * scale
	fraction := scaled - math.Floor(scaled)
	if fraction >= 0.5 {
		return math.Ceil(scaled) / scale
	}
	return math.Floor(scaled) / scale
}

func rangeFromValues(values []float64) string {
	if len(values) == 0 {
		return ""
	}
	minValue := values[0]
	maxValue := values[0]
	for _, v := range values[1:] {
		if v < minValue {
			minValue = v
		}
		if v > maxValue {
			maxValue = v
		}
	}
	return formatRange(minValue, maxValue)
}

func formatRange(minValue, maxValue float64) string {
	minInt := int(math.Round(minValue))
	maxInt := int(math.Round(maxValue))
	if minInt == maxInt {
		return fmt.Sprintf("%d", minInt)
	}
	return fmt.Sprintf("%d-%d", minInt, maxInt)
}