package services_astrologer

import (
	models "astrology-api/models/astrologermodel"
	"strings"
	"time"
)

// Break time on the dashboard.
//
// astrologer_rest_time holds from_time/to_time as MySQL TIME columns, which
// GORM reads into the model's string fields as "HH:MM:SS".
//
// A row's lifecycle is the break: /rest-time/start writes it with
// is_active = 1, is_delete = 0, and /rest-time/end sets is_active = 0,
// is_delete = 1 on the latest one. So an active row means a break the
// astrologer started and has not ended — reported as on break whatever the
// clock says, which is what start/end promises. The from/to window still
// matters for a recurring break nobody explicitly started, and it decides
// which row to report when more than one is active.

// restTimeLayouts are the forms a TIME column comes back in, plus the shorter
// one a client may have written.
var restTimeLayouts = []string{"15:04:05", "15:04"}

// minutesOfDay parses a stored rest time into minutes since midnight. The
// second return is false for a value that cannot be read, so a malformed row
// is skipped rather than treated as midnight.
func minutesOfDay(value string) (int, bool) {

	trimmed := strings.TrimSpace(value)

	if trimmed == "" {
		return 0, false
	}

	for _, layout := range restTimeLayouts {

		if parsed, err := time.Parse(layout, trimmed); err == nil {
			return parsed.Hour()*60 + parsed.Minute(), true
		}
	}

	return 0, false
}

// restNow is the current time in the window's own timezone. The column
// defaults to Asia/Kolkata, and a break set for 14:00 there is not a break at
// 14:00 wherever the server happens to run. An unset or unknown zone falls
// back to the server's.
func restNow(window models.AstrologerRestTime, now time.Time) time.Time {

	zone := strings.TrimSpace(window.Timezone)

	if zone == "" {
		return now
	}

	location, err := time.LoadLocation(zone)

	if err != nil {
		return now
	}

	return now.In(location)
}

// restWindowContains reports whether now falls inside the window.
//
// A window whose to_time is at or before its from_time runs over midnight
// (22:00 to 06:00) and is treated as two spans — from_time to midnight, and
// midnight to to_time — so a night break is detected on both sides of it.
func restWindowContains(window models.AstrologerRestTime, now time.Time) bool {

	from, ok := minutesOfDay(window.FromTime)

	if !ok {
		return false
	}

	to, ok := minutesOfDay(window.ToTime)

	if !ok {
		return false
	}

	local := restNow(window, now)
	current := local.Hour()*60 + local.Minute()

	switch {

	case from < to:
		return current >= from && current < to

	case from > to:
		// Wraps midnight.
		return current >= from || current < to

	default:
		// from == to is a zero-length window, not an all-day break.
		return false
	}
}

// activeRestWindow returns the rest window that now falls inside, or nil.
func activeRestWindow(
	restTimes []models.AstrologerRestTime,
	now time.Time,
) *models.AstrologerRestTime {

	for i := range restTimes {

		window := restTimes[i]

		if !window.IsActive || window.IsDelete {
			continue
		}

		if restWindowContains(window, now) {
			return &restTimes[i]
		}
	}

	return nil
}

// ongoingRestWindow returns the break in progress, or nil.
//
// A window containing the current time wins. Failing that, any row still
// active is a break that /rest-time/start opened and /rest-time/end has not
// closed, and the newest of those is reported — the astrologer is on break
// even when they are outside the hours they entered, which is the case the
// window check alone got wrong.
func ongoingRestWindow(
	restTimes []models.AstrologerRestTime,
	now time.Time,
) *models.AstrologerRestTime {

	if window := activeRestWindow(restTimes, now); window != nil {
		return window
	}

	var latest *models.AstrologerRestTime

	for i := range restTimes {

		window := restTimes[i]

		if !window.IsActive || window.IsDelete {
			continue
		}

		if latest == nil || newerRestTime(restTimes[i], *latest) {
			latest = &restTimes[i]
		}
	}

	return latest
}

// newerRestTime compares two rows by creation time, falling back to the id for
// rows written in the same second.
func newerRestTime(a models.AstrologerRestTime, b models.AstrologerRestTime) bool {

	if a.CreatedAt != nil && b.CreatedAt != nil && !a.CreatedAt.Equal(*b.CreatedAt) {
		return a.CreatedAt.After(*b.CreatedAt)
	}

	return a.ID > b.ID
}

// restTypes and repeatTypes are the members of the two enum columns on
// astrologer_rest_time. MySQL stores a value outside an enum as an empty
// string rather than refusing it, so anything unrecognised is mapped to the
// column's catch-all here instead of being lost.
var (
	restTypes = map[string]bool{
		"morning": true,
		"lunch":   true,
		"evening": true,
		"night":   true,
		"custom":  true,
	}

	repeatTypes = map[string]bool{
		"daily":    true,
		"weekdays": true,
		"weekends": true,
		"custom":   true,
	}
)

func normaliseRestType(value string) string {

	trimmed := strings.ToLower(strings.TrimSpace(value))

	if restTypes[trimmed] {
		return trimmed
	}

	return "custom"
}

func normaliseRepeatType(value string) string {

	trimmed := strings.ToLower(strings.TrimSpace(value))

	if repeatTypes[trimmed] {
		return trimmed
	}

	// The column's own default, and what a client that sends nothing means.
	return "daily"
}
