package middleware

import (
	"time"

	"github.com/labstack/echo/v4"
)

const (
	// TimezoneKey is the key used to store the user's timezone *time.Location in the context
	TimezoneKey = "user_timezone"
	// TimezoneNameKey stores the raw IANA timezone string from the request header
	TimezoneNameKey = "user_timezone_name"
	// TimezoneChangedKey is a bool context key indicating whether the timezone
	// differs from the previously cached value for this user. Handlers should
	// only persist the timezone to DB when this is true.
	TimezoneChangedKey = "timezone_changed"
	// UserNowKey is the key used to store the timezone-aware "now" time in the context
	UserNowKey = "user_now"
	// TimezoneHeader is the HTTP header name for the user's timezone
	TimezoneHeader = "X-Timezone"
)

// package-level timezone cache shared across requests. It is safe for
// concurrent use and has no TTL — entries are only updated when a new
// timezone value is observed for a given user.
var tzCache = NewTimezoneCache()

// TimezoneMiddleware extracts the user's timezone from the request header
// and stores a timezone-aware "now" time in the context.
//
// The timezone should be an IANA timezone name (e.g., "America/Los_Angeles", "Europe/London")
// or a UTC offset (e.g., "-08:00", "+05:30").
//
// If no timezone is provided or it's invalid, UTC is used as the default.
//
// The middleware also compares the incoming timezone with a cached value per
// user and sets TimezoneChangedKey in the context so downstream handlers
// know whether a DB write is needed.
func TimezoneMiddleware() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			tzName := c.Request().Header.Get(TimezoneHeader)
			loc := parseTimezone(tzName)

			// Store the location and the raw name in the context
			c.Set(TimezoneKey, loc)
			c.Set(TimezoneNameKey, tzName)

			// Determine whether the timezone changed for this user so handlers
			// can skip unnecessary DB writes.
			changed := false
			if tzName != "" {
				if user := GetAuthUser(c); user != nil {
					if !tzCache.GetAndCompare(user.ID, tzName) {
						changed = true
					}
				}
			}
			c.Set(TimezoneChangedKey, changed)

			// Calculate "now" in the user's timezone, then get start of day
			// For date comparisons, we want to compare against the START of the user's current day
			userNow := time.Now().In(loc)
			startOfDay := time.Date(userNow.Year(), userNow.Month(), userNow.Day(), 0, 0, 0, 0, loc)
			c.Set(UserNowKey, startOfDay)

			return next(c)
		}
	}
}

// IsTimezoneChanged returns true when the user's timezone header differs from
// the previously observed value. Handlers should only persist the timezone to
// DB when this returns true.
func IsTimezoneChanged(c echo.Context) bool {
	val, ok := c.Get(TimezoneChangedKey).(bool)
	return ok && val
}

// GetTimezoneName returns the raw timezone string from the request header.
func GetTimezoneName(c echo.Context) string {
	val, _ := c.Get(TimezoneNameKey).(string)
	return val
}

// parseTimezone parses a timezone string and returns a *time.Location.
// Supports IANA timezone names (e.g., "America/Los_Angeles") and
// UTC offsets (e.g., "-08:00", "+05:30").
// Returns UTC if the timezone is invalid or empty.
func parseTimezone(tz string) *time.Location {
	if tz == "" {
		return time.UTC
	}

	// Try parsing as IANA timezone name first
	loc, err := time.LoadLocation(tz)
	if err == nil {
		return loc
	}

	// Try parsing as UTC offset (e.g., "-08:00", "+05:30")
	// We parse a reference time with the given offset to extract the offset value
	t, err := time.Parse("-07:00", tz)
	if err == nil {
		// time.Parse returns a time, we need to extract the offset
		// The parsed time will have the offset embedded
		_, offset := t.Zone()
		return time.FixedZone(tz, offset)
	}

	// Also try without colon (e.g., "-0800")
	t, err = time.Parse("-0700", tz)
	if err == nil {
		_, offset := t.Zone()
		return time.FixedZone(tz, offset)
	}

	// Default to UTC
	return time.UTC
}

// GetUserTimezone retrieves the user's timezone from the Echo context.
// Returns UTC if not set or if the stored value is not a *time.Location.
func GetUserTimezone(c echo.Context) *time.Location {
	val := c.Get(TimezoneKey)
	if val == nil {
		return time.UTC
	}
	loc, ok := val.(*time.Location)
	if !ok {
		return time.UTC
	}
	return loc
}

// GetUserNow retrieves the timezone-aware "now" time from the Echo context.
// This represents the start of the current day in the user's timezone.
// Returns time.Now().UTC() if not set or if the stored value is not a time.Time.
func GetUserNow(c echo.Context) time.Time {
	val := c.Get(UserNowKey)
	if val == nil {
		return time.Now().UTC()
	}
	now, ok := val.(time.Time)
	if !ok {
		return time.Now().UTC()
	}
	return now
}
