package util

import (
	"errors"
	"fmt"
	"math"
	"strings"
	"time"

	basedomain "wucher/internal/domain/base"
	"wucher/internal/shared/pkg/suncalc"
)

var (
	ErrShiftScheduleMissing    = errors.New("base shift schedule not found")
	ErrShiftTwilightMissing    = errors.New("base twilight time not available")
	ErrShiftCoordinatesInvalid = errors.New("base coordinates invalid")
)

func ResolveShiftWindow(b *basedomain.Base, date time.Time) (string, string, error) {
	if b == nil || date.IsZero() {
		return "", "", ErrShiftScheduleMissing
	}

	shift := PickShiftForDate(b, date)
	loc := LocationFromUTCOffset(b.UTC)
	scheduleDay := calendarDate(date)
	scheduleDate := time.Date(scheduleDay.Year(), scheduleDay.Month(), scheduleDay.Day(), 12, 0, 0, 0, loc)

	if shift == nil {
		if shiftWindowNeedsCoordinates(b.DefaultStartTimeType, b.DefaultEndTimeType) && !ValidCoordinates(b.Latitude, b.Longitude) {
			return "", "", ErrShiftCoordinatesInvalid
		}
		return resolveBaseDefaultShiftWindow(b, scheduleDate, loc)
	}
	if shiftWindowNeedsCoordinates(shift.StartTimeType, shift.EndTimeType) && !ValidCoordinates(b.Latitude, b.Longitude) {
		return "", "", ErrShiftCoordinatesInvalid
	}

	start, err := resolveShiftClock(scheduleDate, loc, b, shift.StartTimeType, shift.ShiftStart, true)
	if err != nil {
		return "", "", err
	}
	end, err := resolveShiftClock(scheduleDate, loc, b, shift.EndTimeType, shift.ShiftEnd, false)
	if err != nil {
		return "", "", err
	}
	return start, end, nil
}

func shiftWindowNeedsCoordinates(startTimeType, endTimeType string) bool {
	return NormalizeShiftTimeType(startTimeType) != basedomain.ShiftTimeTypeFixed ||
		NormalizeShiftTimeType(endTimeType) != basedomain.ShiftTimeTypeFixed
}

func resolveBaseDefaultShiftWindow(b *basedomain.Base, scheduleDate time.Time, loc *time.Location) (string, string, error) {
	start, err := resolveShiftClock(scheduleDate, loc, b, b.DefaultStartTimeType, b.DefaultShiftStart, true)
	if err != nil {
		return "", "", err
	}
	end, err := resolveShiftClock(scheduleDate, loc, b, b.DefaultEndTimeType, b.DefaultShiftEnd, false)
	if err != nil {
		return "", "", err
	}
	if start == "" || end == "" {
		return "", "", ErrShiftScheduleMissing
	}
	return start, end, nil
}

func PickShiftForDate(b *basedomain.Base, date time.Time) *basedomain.BaseOperationalShiftTime {
	if b == nil {
		return nil
	}
	day := calendarDate(date)
	for i := range b.OperationalShiftTimes {
		row := &b.OperationalShiftTimes[i]
		if row.DateStart == nil || row.DateEnd == nil {
			continue
		}
		start := calendarDate(*row.DateStart)
		end := calendarDate(*row.DateEnd)
		if (day.Equal(start) || day.After(start)) && (day.Equal(end) || day.Before(end.Add(24*time.Hour))) {
			return row
		}
	}
	return nil
}

func calendarDate(t time.Time) time.Time {
	return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
}

func DefaultShiftWindow(b *basedomain.Base) (string, string, error) {
	if b == nil {
		return "", "", ErrShiftScheduleMissing
	}
	start := NormalizeClockValue(b.DefaultShiftStart)
	end := NormalizeClockValue(b.DefaultShiftEnd)
	if start == "" || end == "" {
		return "", "", ErrShiftScheduleMissing
	}
	return start, end, nil
}

func resolveShiftClock(date time.Time, loc *time.Location, b *basedomain.Base, timeType, fixedValue string, morning bool) (string, error) {
	switch NormalizeShiftTimeType(timeType) {
	case basedomain.ShiftTimeTypeFixed:
		clock := NormalizeClockValue(fixedValue)
		if clock == "" {
			return "", ErrShiftScheduleMissing
		}
		return clock, nil
	case basedomain.ShiftTimeTypeBMCT:
		return resolveSunCalcClock(date, loc, b, true)
	case basedomain.ShiftTimeTypeECET:
		return resolveSunCalcClock(date, loc, b, false)
	default:
		return "", fmt.Errorf("invalid shift time type %q", timeType)
	}
}

func resolveSunCalcClock(date time.Time, loc *time.Location, b *basedomain.Base, morning bool) (string, error) {
	twilight, err := suncalc.GetCivilTwilight(date, b.Latitude, b.Longitude)
	if err != nil {
		return "", ErrShiftTwilightMissing
	}
	var resolved time.Time
	if morning {
		resolved = twilight.Dawn
	} else {
		resolved = twilight.Dusk
	}
	if resolved.IsZero() {
		return "", ErrShiftTwilightMissing
	}
	if loc == nil {
		loc = time.UTC
	}
	return resolved.In(loc).Format("15:04"), nil
}

func NormalizeClockValue(raw string) string {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return ""
	}
	if len(trimmed) == 5 {
		trimmed += ":00"
	}
	layouts := []string{
		"15:04:05",
		"15:04",
		"2006-01-02 15:04:05",
		"2006-01-02 15:04",
		time.RFC3339,
		time.RFC3339Nano,
	}
	for _, layout := range layouts {
		parsed, err := time.Parse(layout, trimmed)
		if err == nil {
			return parsed.UTC().Format("15:04")
		}
	}
	return ""
}

func LocationFromUTCOffset(raw string) *time.Location {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" || trimmed == "0" {
		return time.UTC
	}
	if loc, err := time.LoadLocation(trimmed); err == nil {
		return loc
	}
	if offset, ok := parseUTCOffset(trimmed); ok {
		return time.FixedZone("UTC", offset)
	}
	return time.UTC
}

func parseUTCOffset(raw string) (int, bool) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return 0, false
	}
	sign := 1
	switch {
	case strings.HasPrefix(trimmed, "+"):
		trimmed = strings.TrimPrefix(trimmed, "+")
	case strings.HasPrefix(trimmed, "-"):
		sign = -1
		trimmed = strings.TrimPrefix(trimmed, "-")
	}
	trimmed = strings.TrimPrefix(strings.ToUpper(trimmed), "UTC")
	trimmed = strings.TrimSpace(trimmed)
	switch {
	case strings.HasPrefix(trimmed, "+"):
		trimmed = strings.TrimPrefix(trimmed, "+")
	case strings.HasPrefix(trimmed, "-"):
		sign = -1
		trimmed = strings.TrimPrefix(trimmed, "-")
	}
	if trimmed == "" {
		return 0, false
	}
	parts := strings.Split(trimmed, ":")
	hours := 0
	minutes := 0
	switch len(parts) {
	case 1:
		if _, err := fmt.Sscanf(parts[0], "%d", &hours); err != nil {
			return 0, false
		}
	case 2:
		if _, err := fmt.Sscanf(parts[0], "%d", &hours); err != nil {
			return 0, false
		}
		if _, err := fmt.Sscanf(parts[1], "%d", &minutes); err != nil {
			return 0, false
		}
	default:
		return 0, false
	}
	total := ((hours * 3600) + (minutes * 60)) * sign
	return total, true
}

func ValidCoordinates(lat, lon float64) bool {
	return !math.IsNaN(lat) && !math.IsNaN(lon) && lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180
}

func NormalizeShiftTimeType(raw string) string {
	switch strings.ToUpper(strings.TrimSpace(raw)) {
	case "", basedomain.ShiftTimeTypeFixed:
		return basedomain.ShiftTimeTypeFixed
	case basedomain.ShiftTimeTypeBMCT:
		return basedomain.ShiftTimeTypeBMCT
	case basedomain.ShiftTimeTypeECET:
		return basedomain.ShiftTimeTypeECET
	default:
		return strings.ToUpper(strings.TrimSpace(raw))
	}
}
