package services_admin

import (
	"astrology-api/constants"
	dto "astrology-api/dto_admin"
	"astrology-api/repositories_admin"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"
)

// The finance half of the admin general settings screen.
//
// Every value lives in `systemflag`, which both this API and the admin
// panel's scheduler read. This API does not schedule anything: it stores the
// schedule the admin chooses and derives `SettlementCronExpression` from it,
// so the scheduler has one value to read and the two can never disagree about
// when the settlement job runs.

type SettingsService interface {
	GetGeneralSettings() (*dto.GeneralSettingsResponse, error)

	UpdateGeneralSettings(
		request dto.UpdateGeneralSettingsRequest,
	) (*dto.GeneralSettingsResponse, error)

	// The chat screen. Its own endpoint rather than more fields on the
	// general one — see chat_settings_service.go for why.
	GetChatSettings() (*dto.ChatSettingsResponse, error)

	UpdateChatSettings(
		request dto.UpdateChatSettingsRequest,
	) (*dto.ChatSettingsResponse, error)
}

type settingsService struct {
	repository repositories_admin.SettlementRepository

	// Only for the free-chat spend the chat screen reports. Optional: a nil
	// here costs the panel two numbers, not the screen.
	consultations repositories_admin.ConsultationRepository
}

func NewSettingsService(
	repository repositories_admin.SettlementRepository,
	consultations repositories_admin.ConsultationRepository,
) SettingsService {

	return &settingsService{
		repository:    repository,
		consultations: consultations,
	}
}

// flagSpec is one systemflag a settings screen owns: its valueType and the
// label the panel shows if the row has to be created.
type flagSpec struct {
	Name        string
	ValueType   string
	DisplayName string
}

// settlementFlags is every flag the finance screen owns.
var settlementFlags = []flagSpec{
	{constants.FlagPlatformCommissionPercent, "number", "Platform Commission Percent"},
	{constants.FlagMinConsultationMinutes, "number", "Minimum Consultation Minutes"},
	{constants.FlagConsultationGraceSeconds, "number", "Consultation Grace Seconds"},
	{constants.FlagConsultationTickTimeoutSecs, "number", "Consultation Tick Timeout"},
	{constants.FlagSettlementFrequency, "string", "Settlement Frequency"},
	{constants.FlagSettlementDayOfWeek, "number", "Settlement Day Of Week"},
	{constants.FlagSettlementDayOfMonth, "number", "Settlement Day Of Month"},
	{constants.FlagSettlementRunTime, "string", "Settlement Run Time"},
	{constants.FlagSettlementCronExpression, "string", "Settlement Cron Expression"},
	{constants.FlagSettlementAutoApprove, "number", "Settlement Auto Approve"},
	{constants.FlagSettlementMinPayout, "number", "Settlement Minimum Payout"},
}

// chatFlags is every flag the chat screen owns.
//
// A separate list rather than more entries on settlementFlags, because the two
// screens are edited by different people at different times and a save on one
// tab must not carry the other tab's values with it.
var chatFlags = []flagSpec{
	{constants.FlagChatConsultationEnabled, "number", "Chat Consultation Enabled"},
	{constants.FlagFreeChatEnabled, "number", "Free Chat Enabled"},
	{constants.FlagFreeChatMinutes, "number", "Free Chat Minutes"},
	{constants.FlagFreeChatMinConnectedSeconds, "number", "Free Chat Minimum Connected Seconds"},
	{constants.FlagConsultationRingTimeoutSecs, "number", "Consultation Ring Timeout Seconds"},
	{constants.FlagConsultationJoinTimeoutSecs, "number", "Consultation Join Timeout Seconds"},
	{constants.FlagConsultationWarningSeconds, "number", "Consultation Warning Seconds"},
	{constants.FlagConsultationMaxMinutes, "number", "Consultation Maximum Minutes"},
	{constants.FlagChatTickTimeoutSecs, "number", "Chat Tick Timeout Seconds"},
	{constants.FlagConsultationAvailabilityStrict, "number", "Consultation Availability Strict"},
	{constants.FlagChatTranscriptWindowHours, "number", "Chat Transcript Window Hours"},
}

func names(list []flagSpec) []string {

	collected := make([]string, 0, len(list))

	for _, flag := range list {
		collected = append(collected, flag.Name)
	}

	return collected
}

func flagNames() []string {
	return names(settlementFlags)
}

// flagMeta searches both screens' lists, so SetSystemFlag can create a missing
// row with the right type and label whichever one wrote it.
func flagMeta(name string) (string, string) {

	for _, flag := range settlementFlags {

		if flag.Name == name {
			return flag.ValueType, flag.DisplayName
		}
	}

	for _, flag := range chatFlags {

		if flag.Name == name {
			return flag.ValueType, flag.DisplayName
		}
	}

	return "string", name
}

//////////////////////////////////////////////////////////////
// Read
//////////////////////////////////////////////////////////////

func (s *settingsService) GetGeneralSettings() (*dto.GeneralSettingsResponse, error) {

	values, err := s.repository.GetSystemFlags(flagNames())

	if err != nil {
		return nil, err
	}

	return s.compose(values), nil
}

// compose fills the response from whatever the flags hold, falling back to the
// documented defaults for anything the configuration migration has not
// created yet.
func (s *settingsService) compose(
	values map[string]string,
) *dto.GeneralSettingsResponse {

	response := &dto.GeneralSettingsResponse{
		PlatformCommissionPercent:   flagFloat(values, constants.FlagPlatformCommissionPercent, 20),
		MinConsultationMinutes:      flagFloat(values, constants.FlagMinConsultationMinutes, 2),
		ConsultationGraceSeconds:    flagFloat(values, constants.FlagConsultationGraceSeconds, 0),
		ConsultationTickTimeoutSecs: flagFloat(values, constants.FlagConsultationTickTimeoutSecs, 120),

		SettlementFrequency:  flagFrequency(values),
		SettlementDayOfWeek:  int(flagFloat(values, constants.FlagSettlementDayOfWeek, 1)),
		SettlementDayOfMonth: int(flagFloat(values, constants.FlagSettlementDayOfMonth, 1)),
		SettlementRunTime:    flagString(values, constants.FlagSettlementRunTime, "02:00"),

		SettlementAutoApprove: flagBool(values, constants.FlagSettlementAutoApprove),
		SettlementMinPayout:   flagFloat(values, constants.FlagSettlementMinPayout, 0),
	}

	//------------------------------------------------
	// Schedule
	//------------------------------------------------

	if response.SettlementFrequency == constants.SettlementFrequencyCustom {

		response.SettlementCronExpression = flagString(
			values,
			constants.FlagSettlementCronExpression,
			"0 2 * * 1",
		)

		response.ScheduleDescription = "Custom schedule: " + response.SettlementCronExpression

		// Deliberately blank. Working out when a cron expression next fires
		// belongs to whatever runs it, not to this API.
		return response
	}

	response.SettlementCronExpression = BuildCronExpression(
		response.SettlementFrequency,
		response.SettlementDayOfWeek,
		response.SettlementDayOfMonth,
		response.SettlementRunTime,
	)

	response.ScheduleDescription = describeSchedule(
		response.SettlementFrequency,
		response.SettlementDayOfWeek,
		response.SettlementDayOfMonth,
		response.SettlementRunTime,
	)

	if next := nextRun(
		response.SettlementFrequency,
		response.SettlementDayOfWeek,
		response.SettlementDayOfMonth,
		response.SettlementRunTime,
		time.Now(),
	); next != nil {

		response.NextRunAt = next.Format("2006-01-02 15:04:05")
	}

	return response
}

//////////////////////////////////////////////////////////////
// Write
//////////////////////////////////////////////////////////////

func (s *settingsService) UpdateGeneralSettings(
	request dto.UpdateGeneralSettingsRequest,
) (*dto.GeneralSettingsResponse, error) {

	current, err := s.repository.GetSystemFlags(flagNames())

	if err != nil {
		return nil, err
	}

	// Staged, then written: a rejected value must not leave half the screen
	// saved.
	writes := map[string]string{}

	//------------------------------------------------
	// Consultation Billing
	//------------------------------------------------

	if request.PlatformCommissionPercent != nil {

		value := *request.PlatformCommissionPercent

		if value < 0 || value > 100 {
			return nil, errors.New("platform commission percent must be between 0 and 100")
		}

		writes[constants.FlagPlatformCommissionPercent] = trimFloat(value)
	}

	if request.MinConsultationMinutes != nil {

		value := *request.MinConsultationMinutes

		if value < 0 {
			return nil, errors.New("minimum consultation minutes cannot be negative")
		}

		writes[constants.FlagMinConsultationMinutes] = trimFloat(value)
	}

	if request.ConsultationGraceSeconds != nil {

		value := *request.ConsultationGraceSeconds

		if value < 0 {
			return nil, errors.New("consultation grace seconds cannot be negative")
		}

		writes[constants.FlagConsultationGraceSeconds] = trimFloat(value)
	}

	if request.ConsultationTickTimeoutSecs != nil {

		value := *request.ConsultationTickTimeoutSecs

		// Below half a minute an ordinary network hiccup would start
		// closing live sessions as abandoned.
		if value < 30 {
			return nil, errors.New("consultation tick timeout must be at least 30 seconds")
		}

		writes[constants.FlagConsultationTickTimeoutSecs] = trimFloat(value)
	}

	//------------------------------------------------
	// Schedule
	//------------------------------------------------

	frequency := flagFrequency(current)

	if request.SettlementFrequency != nil {

		value := strings.ToUpper(strings.TrimSpace(*request.SettlementFrequency))

		switch value {

		case constants.SettlementFrequencyWeekly,
			constants.SettlementFrequencyMonthly,
			constants.SettlementFrequencyCustom:

			frequency = value

		default:
			return nil, errors.New("settlement frequency must be WEEKLY, MONTHLY or CUSTOM")
		}

		writes[constants.FlagSettlementFrequency] = frequency
	}

	dayOfWeek := int(flagFloat(current, constants.FlagSettlementDayOfWeek, 1))

	if request.SettlementDayOfWeek != nil {

		if *request.SettlementDayOfWeek < 0 || *request.SettlementDayOfWeek > 6 {
			return nil, errors.New("settlement day of week must be between 0 (Sunday) and 6 (Saturday)")
		}

		dayOfWeek = *request.SettlementDayOfWeek

		writes[constants.FlagSettlementDayOfWeek] = strconv.Itoa(dayOfWeek)
	}

	dayOfMonth := int(flagFloat(current, constants.FlagSettlementDayOfMonth, 1))

	if request.SettlementDayOfMonth != nil {

		// Capped at 28 so every month has the day — a 31st schedule would
		// simply not fire in February.
		if *request.SettlementDayOfMonth < 1 || *request.SettlementDayOfMonth > 28 {
			return nil, errors.New("settlement day of month must be between 1 and 28")
		}

		dayOfMonth = *request.SettlementDayOfMonth

		writes[constants.FlagSettlementDayOfMonth] = strconv.Itoa(dayOfMonth)
	}

	runTime := flagString(current, constants.FlagSettlementRunTime, "02:00")

	if request.SettlementRunTime != nil {

		value := strings.TrimSpace(*request.SettlementRunTime)

		if _, _, err := parseRunTime(value); err != nil {
			return nil, err
		}

		runTime = value

		writes[constants.FlagSettlementRunTime] = runTime
	}

	//------------------------------------------------
	// Cron Expression
	//------------------------------------------------
	//
	// Derived for WEEKLY and MONTHLY, so the parts and the expression cannot
	// drift apart. Only CUSTOM takes what the admin typed.

	if frequency == constants.SettlementFrequencyCustom {

		if request.SettlementCronExpression != nil {

			value := strings.TrimSpace(*request.SettlementCronExpression)

			if err := validateCronExpression(value); err != nil {
				return nil, err
			}

			writes[constants.FlagSettlementCronExpression] = value
		}

	} else {

		writes[constants.FlagSettlementCronExpression] = BuildCronExpression(
			frequency,
			dayOfWeek,
			dayOfMonth,
			runTime,
		)
	}

	//------------------------------------------------
	// Settlement Rules
	//------------------------------------------------

	if request.SettlementAutoApprove != nil {

		value := "0"

		if *request.SettlementAutoApprove {
			value = "1"
		}

		writes[constants.FlagSettlementAutoApprove] = value
	}

	if request.SettlementMinPayout != nil {

		if *request.SettlementMinPayout < 0 {
			return nil, errors.New("settlement minimum payout cannot be negative")
		}

		writes[constants.FlagSettlementMinPayout] = trimFloat(*request.SettlementMinPayout)
	}

	//------------------------------------------------
	// Persist
	//------------------------------------------------

	for name, value := range writes {

		valueType, displayName := flagMeta(name)

		if err := s.repository.SetSystemFlag(name, value, valueType, displayName); err != nil {
			return nil, err
		}
	}

	return s.GetGeneralSettings()
}

//////////////////////////////////////////////////////////////
// Schedule helpers
//////////////////////////////////////////////////////////////

// BuildCronExpression renders the five-field expression the admin panel's
// scheduler runs on. Exported because the settlement service reports it
// alongside a run.
func BuildCronExpression(
	frequency string,
	dayOfWeek int,
	dayOfMonth int,
	runTime string,
) string {

	hour, minute, err := parseRunTime(runTime)

	if err != nil {
		hour, minute = 2, 0
	}

	if frequency == constants.SettlementFrequencyMonthly {
		return fmt.Sprintf("%d %d %d * *", minute, hour, dayOfMonth)
	}

	return fmt.Sprintf("%d %d * * %d", minute, hour, dayOfWeek)
}

// validateCronExpression checks the shape only — five space-separated fields.
// Anything more would mean reimplementing the scheduler's own parser here and
// disagreeing with it on the edges.
func validateCronExpression(expression string) error {

	if expression == "" {
		return errors.New("cron expression is required for a custom schedule")
	}

	if len(strings.Fields(expression)) != 5 {
		return errors.New("cron expression must have 5 fields, for example 0 2 * * 1")
	}

	return nil
}

func parseRunTime(value string) (int, int, error) {

	parts := strings.Split(strings.TrimSpace(value), ":")

	if len(parts) != 2 {
		return 0, 0, errors.New("settlement run time must be HH:MM")
	}

	hour, err := strconv.Atoi(strings.TrimSpace(parts[0]))

	if err != nil || hour < 0 || hour > 23 {
		return 0, 0, errors.New("settlement run time hour must be between 00 and 23")
	}

	minute, err := strconv.Atoi(strings.TrimSpace(parts[1]))

	if err != nil || minute < 0 || minute > 59 {
		return 0, 0, errors.New("settlement run time minute must be between 00 and 59")
	}

	return hour, minute, nil
}

var weekdayNames = []string{
	"Sunday",
	"Monday",
	"Tuesday",
	"Wednesday",
	"Thursday",
	"Friday",
	"Saturday",
}

func describeSchedule(
	frequency string,
	dayOfWeek int,
	dayOfMonth int,
	runTime string,
) string {

	if frequency == constants.SettlementFrequencyMonthly {
		return fmt.Sprintf("Monthly on day %d at %s", dayOfMonth, runTime)
	}

	day := "Monday"

	if dayOfWeek >= 0 && dayOfWeek < len(weekdayNames) {
		day = weekdayNames[dayOfWeek]
	}

	return fmt.Sprintf("Weekly on %s at %s", day, runTime)
}

// nextRun is when the stored schedule fires next. Only WEEKLY and MONTHLY are
// computed; a custom expression is the scheduler's business.
func nextRun(
	frequency string,
	dayOfWeek int,
	dayOfMonth int,
	runTime string,
	from time.Time,
) *time.Time {

	hour, minute, err := parseRunTime(runTime)

	if err != nil {
		return nil
	}

	if frequency == constants.SettlementFrequencyMonthly {

		if dayOfMonth < 1 || dayOfMonth > 28 {
			dayOfMonth = 1
		}

		candidate := time.Date(
			from.Year(),
			from.Month(),
			dayOfMonth,
			hour,
			minute,
			0,
			0,
			from.Location(),
		)

		if !candidate.After(from) {
			candidate = candidate.AddDate(0, 1, 0)
		}

		return &candidate
	}

	if dayOfWeek < 0 || dayOfWeek > 6 {
		dayOfWeek = 1
	}

	candidate := time.Date(
		from.Year(),
		from.Month(),
		from.Day(),
		hour,
		minute,
		0,
		0,
		from.Location(),
	)

	shift := (dayOfWeek - int(candidate.Weekday()) + 7) % 7

	candidate = candidate.AddDate(0, 0, shift)

	if !candidate.After(from) {
		candidate = candidate.AddDate(0, 0, 7)
	}

	return &candidate
}

//////////////////////////////////////////////////////////////
// Flag helpers
//////////////////////////////////////////////////////////////

func flagString(values map[string]string, name string, fallback string) string {

	if value, ok := values[name]; ok && strings.TrimSpace(value) != "" {
		return strings.TrimSpace(value)
	}

	return fallback
}

func flagFloat(values map[string]string, name string, fallback float64) float64 {

	value, ok := values[name]

	if !ok {
		return fallback
	}

	parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)

	if err != nil {
		return fallback
	}

	return parsed
}

func flagBool(values map[string]string, name string) bool {

	value := strings.ToLower(strings.TrimSpace(values[name]))

	return value == "1" || value == "true" || value == "yes"
}

func flagFrequency(values map[string]string) string {

	value := strings.ToUpper(flagString(
		values,
		constants.FlagSettlementFrequency,
		constants.SettlementFrequencyWeekly,
	))

	switch value {

	case constants.SettlementFrequencyWeekly,
		constants.SettlementFrequencyMonthly,
		constants.SettlementFrequencyCustom:

		return value

	default:
		return constants.SettlementFrequencyWeekly
	}
}

// trimFloat writes a number back without a trailing .00, because these values
// are also read by the admin panel as plain strings.
func trimFloat(value float64) string {

	return strconv.FormatFloat(value, 'f', -1, 64)
}
