package exportprogress

import (
	"fmt"
	"sort"
	"strings"
	"time"

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

const (
	UnassignedKandangName = "Farm-level / Unassigned"
	jakartaTZ             = "Asia/Jakarta"
)

type Query struct {
	StartDate    time.Time
	EndDate      time.Time
	StartDateRaw string
	EndDateRaw   string
}

type Row struct {
	Module       string
	FarmName     string
	KandangName  string
	ActivityDate time.Time
	Count        int
}

type monthBlock struct {
	Start time.Time
	Weeks int
}

func IsProgressExportRequest(c *fiber.Ctx) bool {
	return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") &&
		strings.EqualFold(strings.TrimSpace(c.Query("type")), "progress")
}

func ParseQuery(c *fiber.Ctx) (*Query, error) {
	location, err := time.LoadLocation(jakartaTZ)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
	}

	startRaw := strings.TrimSpace(c.Query("start_date"))
	endRaw := strings.TrimSpace(c.Query("end_date"))
	if startRaw == "" || endRaw == "" {
		return nil, fiber.NewError(fiber.StatusBadRequest, "start_date and end_date are required")
	}

	startDate, err := time.ParseInLocation("2006-01-02", startRaw, location)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "start_date must use format YYYY-MM-DD")
	}
	endDate, err := time.ParseInLocation("2006-01-02", endRaw, location)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "end_date must use format YYYY-MM-DD")
	}
	if endDate.Before(startDate) {
		return nil, fiber.NewError(fiber.StatusBadRequest, "end_date must be greater than or equal to start_date")
	}

	return &Query{
		StartDate:    startDate,
		EndDate:      endDate,
		StartDateRaw: startRaw,
		EndDateRaw:   endRaw,
	}, nil
}

func BuildWorkbook(moduleTitle string, query *Query, rows []Row) ([]byte, error) {
	file := excelize.NewFile()
	defer file.Close()

	sheetName := moduleTitle
	defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
	if defaultSheet != sheetName {
		if err := file.SetSheetName(defaultSheet, sheetName); err != nil {
			return nil, err
		}
	}

	location, err := time.LoadLocation(jakartaTZ)
	if err != nil {
		return nil, err
	}

	titleStyle, metaStyle, monthStyle, weekStyle, dayHeaderStyle, farmStyle, textStyle, numberStyle, subtotalStyle, err := buildStyles(file)
	if err != nil {
		return nil, err
	}

	months := monthBlocksBetween(query.StartDate, query.EndDate)
	maxWeeks := 4
	for _, block := range months {
		if block.Weeks > maxWeeks {
			maxWeeks = block.Weeks
		}
	}
	lastColName, err := excelize.ColumnNumberToName(1 + (maxWeeks * 7) + 1)
	if err != nil {
		return nil, err
	}

	if err := file.MergeCell(sheetName, "A1", lastColName+"1"); err != nil {
		return nil, err
	}
	if err := file.SetCellValue(sheetName, "A1", moduleTitle); err != nil {
		return nil, err
	}
	if err := file.SetCellStyle(sheetName, "A1", lastColName+"1", titleStyle); err != nil {
		return nil, err
	}

	metaValue := fmt.Sprintf(
		"Range: %s to %s | Generated at: %s",
		query.StartDateRaw,
		query.EndDateRaw,
		time.Now().In(location).Format("2006-01-02 15:04:05 MST"),
	)
	if err := file.MergeCell(sheetName, "A2", lastColName+"2"); err != nil {
		return nil, err
	}
	if err := file.SetCellValue(sheetName, "A2", metaValue); err != nil {
		return nil, err
	}
	if err := file.SetCellStyle(sheetName, "A2", lastColName+"2", metaStyle); err != nil {
		return nil, err
	}

	if err := applyColumnWidths(file, sheetName, maxWeeks); err != nil {
		return nil, err
	}

	grouped := groupRows(rows)
	currentRow := 4
	for _, month := range months {
		lastColIndex := 1 + (month.Weeks * 7) + 1
		monthLastCol, err := excelize.ColumnNumberToName(lastColIndex)
		if err != nil {
			return nil, err
		}

		if err := renderMonthHeader(file, sheetName, currentRow, month, monthLastCol, monthStyle, weekStyle, dayHeaderStyle); err != nil {
			return nil, err
		}
		currentRow += 4

		monthData := grouped[month.Start.Format("2006-01")]
		if len(monthData) == 0 {
			if err := file.MergeCell(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow)); err != nil {
				return nil, err
			}
			if err := file.SetCellValue(sheetName, "A"+fmt.Sprint(currentRow), "No progress data"); err != nil {
				return nil, err
			}
			if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow), textStyle); err != nil {
				return nil, err
			}
			currentRow += 2
			continue
		}

		farms := sortedKeys(monthData)
		for _, farm := range farms {
			if err := file.MergeCell(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow)); err != nil {
				return nil, err
			}
			if err := file.SetCellValue(sheetName, "A"+fmt.Sprint(currentRow), farm); err != nil {
				return nil, err
			}
			if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow), farmStyle); err != nil {
				return nil, err
			}
			currentRow++

			kandangs := sortedKeys(monthData[farm])
			farmTotals := make(map[string]int)
			farmGrandTotal := 0
			for _, kandang := range kandangs {
				rowCounts := monthData[farm][kandang]
				rowTotal := 0

				if err := file.SetCellValue(sheetName, "A"+fmt.Sprint(currentRow), kandang); err != nil {
					return nil, err
				}
				if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), "A"+fmt.Sprint(currentRow), textStyle); err != nil {
					return nil, err
				}

				for dayKey, count := range rowCounts {
					activityDate, err := time.ParseInLocation("2006-01-02", dayKey, location)
					if err != nil {
						return nil, err
					}
					colIndex := dayColumnIndex(month, activityDate)
					colName, err := excelize.ColumnNumberToName(colIndex)
					if err != nil {
						return nil, err
					}
					if err := file.SetCellValue(sheetName, colName+fmt.Sprint(currentRow), count); err != nil {
						return nil, err
					}
					if err := file.SetCellStyle(sheetName, colName+fmt.Sprint(currentRow), colName+fmt.Sprint(currentRow), numberStyle); err != nil {
						return nil, err
					}
					rowTotal += count
					farmTotals[dayKey] += count
					farmGrandTotal += count
				}

				if err := file.SetCellValue(sheetName, monthLastCol+fmt.Sprint(currentRow), rowTotal); err != nil {
					return nil, err
				}
				if err := file.SetCellStyle(sheetName, monthLastCol+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow), subtotalStyle); err != nil {
					return nil, err
				}
				if err := file.SetCellStyle(sheetName, "B"+fmt.Sprint(currentRow), prevColumn(monthLastCol)+fmt.Sprint(currentRow), numberStyle); err != nil {
					return nil, err
				}

				currentRow++
			}

			if err := file.SetCellValue(sheetName, "A"+fmt.Sprint(currentRow), "Subtotal"); err != nil {
				return nil, err
			}
			if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), "A"+fmt.Sprint(currentRow), subtotalStyle); err != nil {
				return nil, err
			}
			for dayKey, count := range farmTotals {
				activityDate, err := time.ParseInLocation("2006-01-02", dayKey, location)
				if err != nil {
					return nil, err
				}
				colIndex := dayColumnIndex(month, activityDate)
				colName, err := excelize.ColumnNumberToName(colIndex)
				if err != nil {
					return nil, err
				}
				if err := file.SetCellValue(sheetName, colName+fmt.Sprint(currentRow), count); err != nil {
					return nil, err
				}
			}
			if err := file.SetCellValue(sheetName, monthLastCol+fmt.Sprint(currentRow), farmGrandTotal); err != nil {
				return nil, err
			}
			if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow), subtotalStyle); err != nil {
				return nil, err
			}
			currentRow += 2
		}
	}

	if err := file.SetPanes(sheetName, &excelize.Panes{
		Freeze:      true,
		YSplit:      2,
		TopLeftCell: "A3",
		ActivePane:  "bottomLeft",
	}); err != nil {
		return nil, err
	}

	buffer, err := file.WriteToBuffer()
	if err != nil {
		return nil, err
	}
	return buffer.Bytes(), nil
}

func ParseActivityDate(value string) (time.Time, error) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return time.Time{}, fmt.Errorf("empty activity date")
	}

	layouts := []string{
		"2006-01-02",
		time.RFC3339,
		time.RFC3339Nano,
		"2006-01-02 15:04:05Z07:00",
		"2006-01-02 15:04:05.999999999Z07:00",
	}
	for _, layout := range layouts {
		if parsed, err := time.Parse(layout, trimmed); err == nil {
			return parsed, nil
		}
	}

	if len(trimmed) >= len("2006-01-02") {
		if parsed, err := time.Parse("2006-01-02", trimmed[:10]); err == nil {
			return parsed, nil
		}
	}

	return time.Time{}, fmt.Errorf("unsupported activity date format: %s", value)
}

func buildStyles(file *excelize.File) (int, int, int, int, int, int, int, int, int, error) {
	titleStyle, err := file.NewStyle(&excelize.Style{
		Font:      &excelize.Font{Bold: true, Size: 18, Color: "1F2937"},
		Fill:      excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"DCEBFA"}},
		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}
	metaStyle, err := file.NewStyle(&excelize.Style{
		Font:      &excelize.Font{Italic: true, Color: "4B5563"},
		Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center"},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}
	monthStyle, err := file.NewStyle(&excelize.Style{
		Font:      &excelize.Font{Bold: true, Color: "FFFFFF"},
		Fill:      excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"1D4ED8"}},
		Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center"},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}
	weekStyle, err := file.NewStyle(&excelize.Style{
		Font:      &excelize.Font{Bold: true, Color: "1F2937"},
		Fill:      excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"DBEAFE"}},
		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
		Border:    []excelize.Border{{Type: "bottom", Color: "93C5FD", Style: 1}},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}
	dayHeaderStyle, err := file.NewStyle(&excelize.Style{
		Font:      &excelize.Font{Bold: true, Color: "374151"},
		Fill:      excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"EFF6FF"}},
		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
		Border: []excelize.Border{
			{Type: "left", Color: "BFDBFE", Style: 1},
			{Type: "top", Color: "BFDBFE", Style: 1},
			{Type: "bottom", Color: "BFDBFE", Style: 1},
			{Type: "right", Color: "BFDBFE", Style: 1},
		},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}
	farmStyle, err := file.NewStyle(&excelize.Style{
		Font:      &excelize.Font{Bold: true, Color: "111827"},
		Fill:      excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E5E7EB"}},
		Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center"},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}
	textStyle, err := file.NewStyle(&excelize.Style{
		Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center"},
		Border: []excelize.Border{
			{Type: "left", Color: "D1D5DB", Style: 1},
			{Type: "top", Color: "D1D5DB", Style: 1},
			{Type: "bottom", Color: "D1D5DB", Style: 1},
			{Type: "right", Color: "D1D5DB", Style: 1},
		},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}
	numberStyle, err := file.NewStyle(&excelize.Style{
		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
		Border: []excelize.Border{
			{Type: "left", Color: "D1D5DB", Style: 1},
			{Type: "top", Color: "D1D5DB", Style: 1},
			{Type: "bottom", Color: "D1D5DB", Style: 1},
			{Type: "right", Color: "D1D5DB", Style: 1},
		},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}
	subtotalStyle, err := file.NewStyle(&excelize.Style{
		Font:      &excelize.Font{Bold: true, Color: "1F2937"},
		Fill:      excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"F3F4F6"}},
		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
		Border: []excelize.Border{
			{Type: "left", Color: "9CA3AF", Style: 1},
			{Type: "top", Color: "9CA3AF", Style: 1},
			{Type: "bottom", Color: "9CA3AF", Style: 1},
			{Type: "right", Color: "9CA3AF", Style: 1},
		},
	})
	if err != nil {
		return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
	}

	return titleStyle, metaStyle, monthStyle, weekStyle, dayHeaderStyle, farmStyle, textStyle, numberStyle, subtotalStyle, nil
}

func applyColumnWidths(file *excelize.File, sheet string, maxWeeks int) error {
	if err := file.SetColWidth(sheet, "A", "A", 28); err != nil {
		return err
	}
	for col := 2; col <= 1+(maxWeeks*7); col++ {
		colName, err := excelize.ColumnNumberToName(col)
		if err != nil {
			return err
		}
		if err := file.SetColWidth(sheet, colName, colName, 6); err != nil {
			return err
		}
	}
	totalCol, err := excelize.ColumnNumberToName(1 + (maxWeeks * 7) + 1)
	if err != nil {
		return err
	}
	return file.SetColWidth(sheet, totalCol, totalCol, 10)
}

func renderMonthHeader(file *excelize.File, sheet string, startRow int, block monthBlock, monthLastCol string, monthStyle, weekStyle, dayHeaderStyle int) error {
	if err := file.MergeCell(sheet, "A"+fmt.Sprint(startRow), monthLastCol+fmt.Sprint(startRow)); err != nil {
		return err
	}
	if err := file.SetCellValue(sheet, "A"+fmt.Sprint(startRow), block.Start.Format("January 2006")); err != nil {
		return err
	}
	if err := file.SetCellStyle(sheet, "A"+fmt.Sprint(startRow), monthLastCol+fmt.Sprint(startRow), monthStyle); err != nil {
		return err
	}

	if err := file.MergeCell(sheet, "A"+fmt.Sprint(startRow+1), "A"+fmt.Sprint(startRow+3)); err != nil {
		return err
	}
	if err := file.SetCellValue(sheet, "A"+fmt.Sprint(startRow+1), "Kandang"); err != nil {
		return err
	}
	if err := file.SetCellStyle(sheet, "A"+fmt.Sprint(startRow+1), "A"+fmt.Sprint(startRow+3), dayHeaderStyle); err != nil {
		return err
	}

	totalColIndex := 1 + (block.Weeks * 7) + 1
	totalColName, err := excelize.ColumnNumberToName(totalColIndex)
	if err != nil {
		return err
	}
	if err := file.MergeCell(sheet, totalColName+fmt.Sprint(startRow+1), totalColName+fmt.Sprint(startRow+3)); err != nil {
		return err
	}
	if err := file.SetCellValue(sheet, totalColName+fmt.Sprint(startRow+1), "Total"); err != nil {
		return err
	}
	if err := file.SetCellStyle(sheet, totalColName+fmt.Sprint(startRow+1), totalColName+fmt.Sprint(startRow+3), dayHeaderStyle); err != nil {
		return err
	}

	weekdayNames := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
	for week := 0; week < block.Weeks; week++ {
		startCol := 2 + (week * 7)
		endCol := startCol + 6
		startColName, err := excelize.ColumnNumberToName(startCol)
		if err != nil {
			return err
		}
		endColName, err := excelize.ColumnNumberToName(endCol)
		if err != nil {
			return err
		}
		if err := file.MergeCell(sheet, startColName+fmt.Sprint(startRow+1), endColName+fmt.Sprint(startRow+1)); err != nil {
			return err
		}
		if err := file.SetCellValue(sheet, startColName+fmt.Sprint(startRow+1), fmt.Sprintf("Week %d", week+1)); err != nil {
			return err
		}
		if err := file.SetCellStyle(sheet, startColName+fmt.Sprint(startRow+1), endColName+fmt.Sprint(startRow+1), weekStyle); err != nil {
			return err
		}

		for weekday := 0; weekday < 7; weekday++ {
			colIndex := startCol + weekday
			colName, err := excelize.ColumnNumberToName(colIndex)
			if err != nil {
				return err
			}
			if err := file.SetCellValue(sheet, colName+fmt.Sprint(startRow+2), weekdayNames[weekday]); err != nil {
				return err
			}
			if err := file.SetCellStyle(sheet, colName+fmt.Sprint(startRow+2), colName+fmt.Sprint(startRow+2), dayHeaderStyle); err != nil {
				return err
			}
		}
	}

	daysInMonth := time.Date(block.Start.Year(), block.Start.Month()+1, 0, 0, 0, 0, 0, block.Start.Location()).Day()
	for day := 1; day <= daysInMonth; day++ {
		date := time.Date(block.Start.Year(), block.Start.Month(), day, 0, 0, 0, 0, block.Start.Location())
		colIndex := dayColumnIndex(block, date)
		colName, err := excelize.ColumnNumberToName(colIndex)
		if err != nil {
			return err
		}
		if err := file.SetCellValue(sheet, colName+fmt.Sprint(startRow+3), day); err != nil {
			return err
		}
		if err := file.SetCellStyle(sheet, colName+fmt.Sprint(startRow+3), colName+fmt.Sprint(startRow+3), dayHeaderStyle); err != nil {
			return err
		}
	}

	return nil
}

func groupRows(rows []Row) map[string]map[string]map[string]map[string]int {
	grouped := make(map[string]map[string]map[string]map[string]int)
	for _, row := range rows {
		monthKey := row.ActivityDate.Format("2006-01")
		if _, exists := grouped[monthKey]; !exists {
			grouped[monthKey] = make(map[string]map[string]map[string]int)
		}
		farmName := strings.TrimSpace(row.FarmName)
		if farmName == "" {
			farmName = "Unknown Farm"
		}
		if _, exists := grouped[monthKey][farmName]; !exists {
			grouped[monthKey][farmName] = make(map[string]map[string]int)
		}
		kandangName := strings.TrimSpace(row.KandangName)
		if kandangName == "" {
			kandangName = UnassignedKandangName
		}
		if _, exists := grouped[monthKey][farmName][kandangName]; !exists {
			grouped[monthKey][farmName][kandangName] = make(map[string]int)
		}
		dayKey := row.ActivityDate.Format("2006-01-02")
		grouped[monthKey][farmName][kandangName][dayKey] += row.Count
	}
	return grouped
}

func monthBlocksBetween(startDate, endDate time.Time) []monthBlock {
	location := startDate.Location()
	current := time.Date(startDate.Year(), startDate.Month(), 1, 0, 0, 0, 0, location)
	last := time.Date(endDate.Year(), endDate.Month(), 1, 0, 0, 0, 0, location)

	blocks := make([]monthBlock, 0)
	for !current.After(last) {
		blocks = append(blocks, monthBlock{
			Start: current,
			Weeks: monthWeeks(current),
		})
		current = current.AddDate(0, 1, 0)
	}
	return blocks
}

func monthWeeks(monthStart time.Time) int {
	daysInMonth := time.Date(monthStart.Year(), monthStart.Month()+1, 0, 0, 0, 0, 0, monthStart.Location()).Day()
	offset := mondayIndex(monthStart.Weekday())
	totalSlots := offset + daysInMonth
	weeks := totalSlots / 7
	if totalSlots%7 != 0 {
		weeks++
	}
	if weeks < 4 {
		return 4
	}
	return weeks
}

func dayColumnIndex(block monthBlock, date time.Time) int {
	day := date.Day()
	offset := mondayIndex(block.Start.Weekday())
	position := offset + (day - 1)
	return 2 + position
}

func mondayIndex(weekday time.Weekday) int {
	switch weekday {
	case time.Sunday:
		return 6
	default:
		return int(weekday) - 1
	}
}

func sortedKeys[V any](input map[string]V) []string {
	keys := make([]string, 0, len(input))
	for key := range input {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func prevColumn(col string) string {
	index, err := excelize.ColumnNameToNumber(col)
	if err != nil || index <= 1 {
		return col
	}
	result, err := excelize.ColumnNumberToName(index - 1)
	if err != nil {
		return col
	}
	return result
}
