package services_astrologer

import (
	dto "astrology-api/dto_astrologer"
	models "astrology-api/models/astrologermodel"
	"astrology-api/notifications"
	repositories_astrologer "astrology-api/repositories_astrologer"
	userservices "astrology-api/services"
	"errors"
	"time"

	repositories "astrology-api/repositories_astrologer"

	"gorm.io/gorm"
)

type dashboardService struct {
	dashboardRepo repositories.DashboardRepository

	statusRepo repositories.StatusRepository

	restTimeRepo repositories.RestTimeRepository

	offerRepo repositories.OfferRepository

	statisticsRepo repositories.StatisticsRepository

	statusLogRepo repositories.StatusLogRepository
}

func NewDashboardService(
	dashboardRepo repositories.DashboardRepository,
	statusRepo repositories.StatusRepository,
	statisticsRepo repositories.StatisticsRepository,
	offerRepo repositories.OfferRepository,
	restTimeRepo repositories.RestTimeRepository,
	statusLogRepo repositories_astrologer.StatusLogRepository,
) DashboardService {
	return &dashboardService{
		dashboardRepo:  dashboardRepo,
		statusRepo:     statusRepo,
		statisticsRepo: statisticsRepo,
		// walletRepo:     walletRepo,
		offerRepo:     offerRepo,
		restTimeRepo:  restTimeRepo,
		statusLogRepo: statusLogRepo,
	}
}

func NewDashboardServiceWithRepositories(

	dashboardRepo repositories.DashboardRepository,

	statusRepo repositories.StatusRepository,

	restRepo repositories.RestTimeRepository,

	offerRepo repositories.OfferRepository,

	statisticsRepo repositories.StatisticsRepository,

	statusLogRepo repositories.StatusLogRepository,

) DashboardService {

	return &dashboardService{

		dashboardRepo: dashboardRepo,

		statusRepo: statusRepo,

		restTimeRepo: restRepo,

		offerRepo: offerRepo,

		statisticsRepo: statisticsRepo,

		statusLogRepo: statusLogRepo,
	}
}

func (s *dashboardService) GetDashboard(userID uint) (*dto.DashboardResponse, error) {

	// Validate User
	if userID == 0 {
		return nil, errors.New("invalid user id")
	}
	// Load Astrologer
	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return nil, err
	}
	if astrologer == nil {
		return nil, errors.New("astrologer not found")
	}
	// Load Status
	status, err := s.dashboardRepo.GetStatus(astrologer.ID)

	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	// Load Wallet
	walletBalance, err := s.dashboardRepo.GetWalletBalance(userID)
	if err != nil {
		return nil, err
	}
	// Load Today's Statistics
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	// Default Status
	if status == nil {
		status = &models.AstrologerStatus{
			UserID:        userID,
			AstrologerID:  astrologer.ID,
			CurrentStatus: "Offline",
			IsOnline:      false,
			IsChatOnline:  false,
			IsCallOnline:  false,
			IsBusy:        false,
			QuickDND:      false,
		}
	}
	// Default Statistics
	statistics, err := s.statisticsRepo.GetTodayStatistics(astrologer.ID)
	if statistics == nil {
		statistics = &models.AstrologerDailyStatistics{
			UserID:           userID,
			AstrologerID:     astrologer.ID,
			OnlineMinutes:    0,
			TotalCalls:       0,
			CompletedCalls:   0,
			MissedCalls:      0,
			CancelledCalls:   0,
			TotalChats:       0,
			CompletedChats:   0,
			MissedChats:      0,
			CancelledChats:   0,
			TotalCallMinutes: 0,
			TotalChatMinutes: 0,
			CallEarning:      0,
			ChatEarning:      0,
			ReportEarning:    0,
			GiftEarning:      0,
			TotalEarning:     0,
			AverageRating:    0,
			AcceptanceRate:   0,
			ResponseRate:     0,
		}
	}

	// Create Response Object
	response := &dto.DashboardResponse{}

	// Dashboard Mapping
	response.ID = astrologer.ID
	response.UserID = astrologer.UserID
	response.Name = astrologer.Name
	response.ProfileImage = astrologer.ProfileImage

	// Status Information
	response.CurrentStatus = status.CurrentStatus
	response.IsOnline = status.IsOnline
	response.IsChatOnline = status.IsChatOnline
	response.IsCallOnline = status.IsCallOnline
	response.QuickDND = status.QuickDND

	// Consultation Rates
	response.ChatRate = astrologer.ChatRate
	response.CallRate = astrologer.AudioCallRate
	response.VideoCallRate = astrologer.VideoCallRate

	// Break Time
	//
	// Read from astrologer_rest_time rather than from astrologer_status:
	// current_status is enum('offline','online','busy','dnd'), so it cannot
	// carry a break of its own.
	//
	// GetActiveRestTime, not GetTodayRestTime: the latter filters on
	// repeat_type/repeat_days, which hides a break that /rest-time/start just
	// opened whenever its repeat configuration does not name today. Every row
	// this returns is active and undeleted, which is exactly "started and not
	// yet ended".
	restTimes, err := s.restTimeRepo.GetActiveRestTime(astrologer.ID)

	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	if window := ongoingRestWindow(restTimes, time.Now()); window != nil {

		response.IsOnBreak = true
		response.RestTimeStart = window.FromTime
		response.RestTimeEnd = window.ToTime
		response.RestTitle = window.Title

		// A break outranks the stored status: the astrologer is unreachable
		// whatever the row says.
		response.CurrentStatus = "Break Time"
	}

	// Wallet
	response.WalletBalance = walletBalance
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	// Today's Statistics
	response.TodayEarning = statistics.TotalEarning
	response.TodayCalls = statistics.TotalCalls
	response.TodayChats = statistics.TotalChats
	response.TodayCallMinutes = statistics.TotalCallMinutes
	response.TodayChatMinutes = statistics.TotalChatMinutes
	response.OnlineMinutes = statistics.OnlineMinutes
	response.AverageRating = statistics.AverageRating
	response.AcceptanceRate = statistics.AcceptanceRate
	response.ResponseRate = statistics.ResponseRate

	// Request Queue badge. A failure here costs a number on the dashboard,
	// not the dashboard, so it is deliberately not propagated. 0 means "use
	// the configured ring timeout".
	if pending, err := s.dashboardRepo.CountPendingRequests(astrologer.ID, 0); err == nil {
		response.PendingRequestCount = int(pending)
	}

	// Normalize Current Status
	if response.CurrentStatus == "" {
		if response.IsOnline {
			response.CurrentStatus = "Online"

		} else {

			response.CurrentStatus = "Offline"
		}
	}

	// Safety Checks
	if response.ProfileImage == "" {
		response.ProfileImage = ""
	}
	if response.Name == "" {
		response.Name = "Astrologer"
	}
	if response.WalletBalance < 0 {
		response.WalletBalance = 0
	}
	if response.TodayCalls < 0 {
		response.TodayCalls = 0
	}
	if response.TodayChats < 0 {
		response.TodayChats = 0
	}
	if response.TodayCallMinutes < 0 {
		response.TodayCallMinutes = 0
	}
	if response.TodayChatMinutes < 0 {
		response.TodayChatMinutes = 0
	}
	if response.OnlineMinutes < 0 {
		response.OnlineMinutes = 0
	}
	if response.TodayEarning < 0 {
		response.TodayEarning = 0
	}
	if response.AverageRating < 0 {
		response.AverageRating = 0
	}
	if response.AcceptanceRate < 0 {
		response.AcceptanceRate = 0
	}
	if response.ResponseRate < 0 {
		response.ResponseRate = 0
	}
	// Return Response
	return response, nil

}

func (s *dashboardService) GetDashboardStatistics(userID uint) (*dto.DashboardStatisticsResponse, error) {

	// Validate User
	if userID == 0 {
		return nil, errors.New("invalid user id")
	}

	// Get Astrologer
	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return nil, err
	}

	if astrologer == nil {
		return nil, errors.New("astrologer not found")
	}

	// Get Today's Statistics
	stats, err := s.statisticsRepo.GetTodayStatistics(astrologer.ID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	// Default Statistics
	if stats == nil {

		now := time.Now()

		stats = &models.AstrologerDailyStatistics{
			UserID:           userID,
			AstrologerID:     astrologer.ID,
			StatisticDate:    now,
			OnlineMinutes:    0,
			TotalCalls:       0,
			CompletedCalls:   0,
			MissedCalls:      0,
			CancelledCalls:   0,
			TotalChats:       0,
			CompletedChats:   0,
			MissedChats:      0,
			CancelledChats:   0,
			TotalCallMinutes: 0,
			TotalChatMinutes: 0,
			CallEarning:      0,
			ChatEarning:      0,
			ReportEarning:    0,
			GiftEarning:      0,
			TotalEarning:     0,
			AverageRating:    0,
			TotalReviews:     0,
			AcceptanceRate:   0,
			ResponseRate:     0,
		}
	}

	// DTO Mapping
	response := &dto.DashboardStatisticsResponse{

		ID:           stats.ID,
		UserID:       stats.UserID,
		AstrologerID: stats.AstrologerID,

		StatisticDate: stats.StatisticDate.Format("2006-01-02"),

		OnlineMinutes: stats.OnlineMinutes,

		TotalCalls:     stats.TotalCalls,
		CompletedCalls: stats.CompletedCalls,
		MissedCalls:    stats.MissedCalls,
		CancelledCalls: stats.CancelledCalls,

		TotalChats:     stats.TotalChats,
		CompletedChats: stats.CompletedChats,
		MissedChats:    stats.MissedChats,
		CancelledChats: stats.CancelledChats,

		TotalCallMinutes: stats.TotalCallMinutes,
		TotalChatMinutes: stats.TotalChatMinutes,

		CallEarning:   stats.CallEarning,
		ChatEarning:   stats.ChatEarning,
		ReportEarning: stats.ReportEarning,
		GiftEarning:   stats.GiftEarning,
		TotalEarning:  stats.TotalEarning,

		AverageRating: stats.AverageRating,
		TotalReviews:  stats.TotalReviews,

		AcceptanceRate: stats.AcceptanceRate,
		ResponseRate:   stats.ResponseRate,
	}

	// Safety Checks
	if response.OnlineMinutes < 0 {
		response.OnlineMinutes = 0
	}

	if response.TotalCalls < 0 {
		response.TotalCalls = 0
	}

	if response.CompletedCalls < 0 {
		response.CompletedCalls = 0
	}

	if response.MissedCalls < 0 {
		response.MissedCalls = 0
	}

	if response.CancelledCalls < 0 {
		response.CancelledCalls = 0
	}

	if response.TotalChats < 0 {
		response.TotalChats = 0
	}

	if response.CompletedChats < 0 {
		response.CompletedChats = 0
	}

	if response.MissedChats < 0 {
		response.MissedChats = 0
	}

	if response.CancelledChats < 0 {
		response.CancelledChats = 0
	}

	if response.TotalCallMinutes < 0 {
		response.TotalCallMinutes = 0
	}

	if response.TotalChatMinutes < 0 {
		response.TotalChatMinutes = 0
	}

	if response.CallEarning < 0 {
		response.CallEarning = 0
	}

	if response.ChatEarning < 0 {
		response.ChatEarning = 0
	}

	if response.ReportEarning < 0 {
		response.ReportEarning = 0
	}

	if response.GiftEarning < 0 {
		response.GiftEarning = 0
	}

	if response.TotalEarning < 0 {
		response.TotalEarning = 0
	}

	if response.AverageRating < 0 {
		response.AverageRating = 0
	}

	if response.AcceptanceRate < 0 {
		response.AcceptanceRate = 0
	}

	if response.ResponseRate < 0 {
		response.ResponseRate = 0
	}

	// Return
	return response, nil
}

func (s *dashboardService) applyOnlineStatus(userID uint, isOnline bool) error {

	// Validate User
	if userID == 0 {
		return errors.New("invalid user id")
	}
	// Get Astrologer
	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return err
	}

	if astrologer == nil {
		return errors.New("astrologer not found")
	}
	// Get Existing Status
	status, err := s.statusRepo.GetStatus(astrologer.ID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	// Create Default Status
	if status == nil {
		now := time.Now()
		status = &models.AstrologerStatus{
			UserID:        userID,
			AstrologerID:  astrologer.ID,
			CurrentStatus: "OFFLINE",
			IsOnline:      false,
			IsChatOnline:  false,
			IsCallOnline:  false,
			IsBusy:        false,
			QuickDND:      false,
			LastActiveAt:  &now,
			IsActive:      true,
			IsDelete:      false,
		}

		if err := s.statusRepo.CreateStatus(status); err != nil {
			return err
		}
	}

	// Store Previous Status
	previousStatus := status.CurrentStatus

	// Update Repository
	if err := s.statusRepo.UpdateOnlineStatus(
		astrologer.ID,
		isOnline,
	); err != nil {
		return err
	}

	// Create Status Log
	now := time.Now()

	currentStatus := "OFFLINE"
	if isOnline {
		currentStatus = "ONLINE"
	}

	log := &models.AstrologerStatusLog{
		UserID:         userID,
		AstrologerID:   astrologer.ID,
		StatusType:     "ONLINE_STATUS",
		PreviousStatus: previousStatus,
		CurrentStatus:  currentStatus,
		StartedAt:      now,
		IsActive:       true,
		IsDelete:       false,
	}

	if err := s.statusLogRepo.CreateStatusLog(log); err != nil {
		return err
	}

	//------------------------------------------------
	// Notify
	//------------------------------------------------
	//
	// Only on the transition into ONLINE, not on every save of an already
	// online status — an app that re-sends its status on resume would
	// otherwise push every follower each time.
	//
	// Two audiences: everyone who favourited this astrologer, and anyone
	// sitting in the waiting queue for them.

	if isOnline && previousStatus != "ONLINE" {

		notifications.Notify().FavouriteAstrologerOnline(astrologer.ID)

		userservices.KickWaitingQueue(astrologer.ID) // FIFO: only the head of the queue is notified
	}

	return nil
}

func (s *dashboardService) applyChatStatus(userID uint, isAvailable bool) error {

	if userID == 0 {
		return errors.New("invalid user id")
	}

	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return err
	}

	if astrologer == nil {
		return errors.New("astrologer not found")
	}

	status, err := s.statusRepo.GetStatus(astrologer.ID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}

	if status == nil {

		now := time.Now()

		status = &models.AstrologerStatus{
			UserID:        userID,
			AstrologerID:  astrologer.ID,
			CurrentStatus: "OFFLINE",
			IsOnline:      false,
			IsChatOnline:  false,
			IsCallOnline:  false,
			IsBusy:        false,
			QuickDND:      false,
			LastActiveAt:  &now,
			IsActive:      true,
			IsDelete:      false,
		}

		if err := s.statusRepo.CreateStatus(status); err != nil {
			return err
		}
	}

	previous := "CHAT_OFFLINE"
	current := "CHAT_OFFLINE"

	if status.IsChatOnline {
		previous = "CHAT_ONLINE"
	}

	if isAvailable {
		current = "CHAT_ONLINE"
	}

	if err := s.statusRepo.UpdateChatStatus(astrologer.ID, isAvailable); err != nil {
		return err
	}

	// Becoming available for chat is what the people queued for a chat are
	// waiting to hear. Again only on the transition, so a repeated save does
	// not re-notify the queue.
	if isAvailable && previous != "CHAT_ONLINE" {
		userservices.KickWaitingQueue(astrologer.ID) // FIFO: only the head of the queue is notified
	}

	return s.createStatusLog(
		userID,
		astrologer.ID,
		"CHAT_STATUS",
		previous,
		current,
	)
}

func (s *dashboardService) createStatusLog(userID uint, astrologerID uint, statusType string, previousStatus string, currentStatus string) error {
	now := time.Now()
	log := &models.AstrologerStatusLog{
		UserID:          userID,
		AstrologerID:    astrologerID,
		StatusType:      statusType,
		PreviousStatus:  previousStatus,
		CurrentStatus:   currentStatus,
		Remarks:         "",
		StartedAt:       now,
		EndedAt:         nil,
		DurationSeconds: 0,
		IPAddress:       "",
		DeviceType:      "MOBILE",
		AppVersion:      "",
		IsActive:        true,
		IsDelete:        false,
	}
	return s.statusLogRepo.CreateStatusLog(log)
}

func (s *dashboardService) applyCallStatus(userID uint, isAvailable bool) error {

	if userID == 0 {
		return errors.New("invalid user id")
	}

	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return err
	}

	status, err := s.statusRepo.GetStatus(astrologer.ID)
	if err != nil {
		return err
	}

	previous := "CALL_OFFLINE"
	if status.IsCallOnline {
		previous = "CALL_ONLINE"
	}

	current := "CALL_OFFLINE"
	if isAvailable {
		current = "CALL_ONLINE"
	}

	if err := s.statusRepo.UpdateCallStatus(astrologer.ID, isAvailable); err != nil {
		return err
	}

	// The call queue's turn — audio and video both queue as CALL.
	if isAvailable && previous != "CALL_ONLINE" {
		userservices.KickWaitingQueue(astrologer.ID) // FIFO: only the head of the queue is notified
	}

	return s.createStatusLog(
		userID,
		astrologer.ID,
		"CALL_STATUS",
		previous,
		current,
	)
}

func (s *dashboardService) applyBusyStatus(userID uint, isBusy bool) error {

	if userID == 0 {
		return errors.New("invalid user id")
	}

	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return err
	}

	status, err := s.statusRepo.GetStatus(astrologer.ID)
	if err != nil {
		return err
	}

	status.IsBusy = isBusy

	if isBusy {
		status.CurrentStatus = "busy"
	} else if status.IsOnline {
		status.CurrentStatus = "online"
	} else {
		status.CurrentStatus = "offline"
	}

	if err := s.statusRepo.UpdateStatus(status); err != nil {
		return err
	}

	previous := "AVAILABLE"
	current := "AVAILABLE"

	if isBusy {
		current = "BUSY"
	}

	if status.IsBusy {
		previous = "BUSY"
	}

	// Coming out of BUSY is the "available again after the last call" moment
	// for whoever is queued — the same notification the end of a session
	// sends, for the case where the astrologer flips the flag by hand.
	if !isBusy && previous == "BUSY" {
		userservices.KickWaitingQueue(astrologer.ID) // FIFO: only the head of the queue is notified
	}

	return s.createStatusLog(
		userID,
		astrologer.ID,
		"BUSY_STATUS",
		previous,
		current,
	)
}

func (s *dashboardService) applyQuickDND(userID uint, enable bool) error {

	if userID == 0 {
		return errors.New("invalid user id")
	}

	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return err
	}

	status, err := s.statusRepo.GetStatus(astrologer.ID)
	if err != nil {
		return err
	}

	previous := "DND_OFF"

	if status.QuickDND {
		previous = "DND_ON"
	}

	current := "DND_OFF"

	if enable {
		current = "DND_ON"
	}

	if err := s.statusRepo.UpdateQuickDND(
		astrologer.ID,
		enable,
	); err != nil {
		return err
	}

	return s.createStatusLog(
		userID,
		astrologer.ID,
		"QUICK_DND",
		previous,
		current,
	)
}

func (s *dashboardService) applyStartRestTime(userID uint, req dto.RestTimeRequest) (*models.AstrologerRestTime, error) {

	//------------------------------------------------
	// Validate User
	//------------------------------------------------

	if userID == 0 {
		return nil, errors.New("invalid user id")
	}

	//------------------------------------------------
	// Validate Request
	//------------------------------------------------

	if req.Title == "" {
		return nil, errors.New("title is required")
	}

	if req.RestType == "" {
		return nil, errors.New("rest type is required")
	}

	if req.FromTime == "" {
		return nil, errors.New("from time is required")
	}

	if req.ToTime == "" {
		return nil, errors.New("to time is required")
	}

	//------------------------------------------------
	// Get Astrologer
	//------------------------------------------------

	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return nil, err
	}

	if astrologer == nil {
		return nil, errors.New("astrologer not found")
	}

	//------------------------------------------------
	// Create Rest Time
	//------------------------------------------------

	now := time.Now()

	restTime := &models.AstrologerRestTime{

		UserID:       userID,
		AstrologerID: astrologer.ID,

		Title: req.Title,

		// Both columns are enums with lower-case members, and a value outside
		// the set is stored as an empty string instead of being refused. A
		// rest_type of "BREAK" was therefore lost on the way in; it now
		// becomes the enum's own catch-all, 'custom'.
		RestType:   normaliseRestType(req.RestType),
		RepeatType: normaliseRepeatType(req.RepeatType),

		FromTime:   req.FromTime,
		ToTime:     req.ToTime,
		RepeatDays: req.RepeatDays,
		Timezone:   req.Timezone,

		IsActive: true,
		IsDelete: false,

		CreatedAt: &now,
		UpdatedAt: &now,
	}

	if err := s.restTimeRepo.CreateRestTime(restTime); err != nil {
		return nil, err
	}

	// Read the row back, so the response shows what was stored rather than
	// what was sent — from_time "14:00" comes back from the TIME column as
	// "14:00:00", and the dashboard reports it in that form too.
	if stored, err := s.restTimeRepo.GetRestTimeByID(restTime.ID); err == nil && stored != nil {
		restTime = stored
	}

	//------------------------------------------------
	// Mark Astrologer Busy
	//------------------------------------------------

	status, err := s.statusRepo.GetStatus(astrologer.ID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	if status != nil {

		status.IsBusy = true

		// "REST" is not a member of enum('offline','online','busy','dnd'), so
		// it was stored as an empty string and left the astrologer showing as
		// neither online nor offline. A break is a busy state; the dashboard
		// reads the rest window itself to label it "Break Time".
		status.CurrentStatus = "busy"

		if err := s.statusRepo.UpdateStatus(status); err != nil {
			return nil, err
		}
	}

	//------------------------------------------------
	// Create Status Log
	//------------------------------------------------

	if err := s.createStatusLog(
		userID,
		astrologer.ID,
		"REST_TIME",
		"AVAILABLE",
		"REST",
	); err != nil {
		return nil, err
	}

	return restTime, nil
}

func (s *dashboardService) applyEndRestTime(userID uint) (*models.AstrologerRestTime, error) {

	//------------------------------------------------
	// Validate User
	//------------------------------------------------

	if userID == 0 {
		return nil, errors.New("invalid user id")
	}

	//------------------------------------------------
	// Get Astrologer
	//------------------------------------------------

	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return nil, err
	}

	if astrologer == nil {
		return nil, errors.New("astrologer not found")
	}

	//------------------------------------------------
	// Get Active Rest Times
	//------------------------------------------------

	restTimes, err := s.restTimeRepo.GetRestTimes(astrologer.ID)
	if err != nil {
		return nil, err
	}

	//------------------------------------------------
	// End Latest Active Rest Time
	//------------------------------------------------

	// The row that was ended, returned so the endpoint can say which break it
	// closed. Nil when there was no break running.
	var ended *models.AstrologerRestTime

	if len(restTimes) > 0 {

		latest := &restTimes[0]

		latest.IsActive = false
		latest.IsDelete = true

		now := time.Now()
		latest.UpdatedAt = &now

		if err := s.restTimeRepo.UpdateRestTime(latest); err != nil {
			return nil, err
		}

		ended = latest
	}

	//------------------------------------------------
	// Update Status
	//------------------------------------------------

	status, err := s.statusRepo.GetStatus(astrologer.ID)
	if err != nil {
		return nil, err
	}

	if status != nil {

		status.IsBusy = false

		if status.IsOnline {
			status.CurrentStatus = "online"
		} else {
			status.CurrentStatus = "offline"
		}

		if err := s.statusRepo.UpdateStatus(status); err != nil {
			return nil, err
		}
	}

	//------------------------------------------------
	// Create Status Log
	//------------------------------------------------

	if err := s.createStatusLog(
		userID,
		astrologer.ID,
		"REST_TIME",
		"REST",
		status.CurrentStatus,
	); err != nil {
		return nil, err
	}

	return ended, nil
}

///////////////////////////////////////////////////////////
// Rest time
//
// The apply* methods above hold the original logic; these add the response
// body, which both endpoints used to send as null.
///////////////////////////////////////////////////////////

func (s *dashboardService) StartRestTime(
	userID uint,
	req dto.RestTimeRequest,
) (*dto.RestTimeResponse, error) {

	restTime, err := s.applyStartRestTime(userID, req)

	if err != nil {
		return nil, err
	}

	return mapRestTime(restTime), nil
}

func (s *dashboardService) EndRestTime(userID uint) (*dto.RestTimeResponse, error) {

	restTime, err := s.applyEndRestTime(userID)

	if err != nil {
		return nil, err
	}

	return mapRestTime(restTime), nil
}

// mapRestTime converts a stored break to its response form. A nil row — no
// break was running when /rest-time/end was called — maps to nil, which the
// endpoint reports as a null data with its success message.
func mapRestTime(restTime *models.AstrologerRestTime) *dto.RestTimeResponse {

	if restTime == nil {
		return nil
	}

	return &dto.RestTimeResponse{
		ID:         restTime.ID,
		Title:      restTime.Title,
		RestType:   restTime.RestType,
		FromTime:   restTime.FromTime,
		ToTime:     restTime.ToTime,
		RepeatType: restTime.RepeatType,
		RepeatDays: restTime.RepeatDays,
		Timezone:   restTime.Timezone,
		IsActive:   restTime.IsActive,
	}
}

func (s *dashboardService) UpdateOffer(userID uint, req dto.OfferRequest) error {

	//------------------------------------------------
	// Validate User
	//------------------------------------------------

	if userID == 0 {
		return errors.New("invalid user id")
	}

	//------------------------------------------------
	// Get Astrologer
	//------------------------------------------------

	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)
	if err != nil {
		return err
	}

	if astrologer == nil {
		return errors.New("astrologer not found")
	}

	//------------------------------------------------
	// Validate Request
	//------------------------------------------------

	if req.PO5Amount < 0 {
		return errors.New("invalid po5 amount")
	}

	if req.FreeChatMinutes < 0 {
		return errors.New("invalid free chat minutes")
	}

	if req.FreeCallMinutes < 0 {
		return errors.New("invalid free call minutes")
	}

	//------------------------------------------------
	// Get Existing Offer
	//------------------------------------------------

	offer, err := s.offerRepo.GetOffer(astrologer.ID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}

	now := time.Now()

	if offer == nil {

		offer = &models.AstrologerOfferSetting{
			UserID:       userID,
			AstrologerID: astrologer.ID,
			IsActive:     true,
			IsDelete:     false,
			CreatedAt:    &now,
			UpdatedAt:    &now,
		}

		if err := s.offerRepo.CreateOffer(offer); err != nil {
			return err
		}
	}

	//------------------------------------------------
	// Map DTO
	//------------------------------------------------

	offer.PO5Enabled = req.PO5Enabled
	offer.PO5Amount = req.PO5Amount

	offer.FreeChatEnabled = req.FreeChatEnabled
	offer.FreeChatMinutes = req.FreeChatMinutes

	offer.FreeCallEnabled = req.FreeCallEnabled
	offer.FreeCallMinutes = req.FreeCallMinutes

	offer.CampaignEnabled = req.CampaignEnabled
	offer.CampaignTitle = req.CampaignTitle
	offer.CampaignDescription = req.CampaignDescription

	//------------------------------------------------
	// Parse Start Date
	//------------------------------------------------

	if req.StartDate != "" {

		startDate, err := time.Parse("2006-01-02", req.StartDate)
		if err != nil {
			return errors.New("invalid start date format")
		}

		offer.StartDate = &startDate
	} else {
		offer.StartDate = nil
	}

	//------------------------------------------------
	// Parse End Date
	//------------------------------------------------

	if req.EndDate != "" {

		endDate, err := time.Parse("2006-01-02", req.EndDate)
		if err != nil {
			return errors.New("invalid end date format")
		}

		offer.EndDate = &endDate
	} else {
		offer.EndDate = nil
	}

	//------------------------------------------------
	// Validate Date Range
	//------------------------------------------------

	if offer.StartDate != nil &&
		offer.EndDate != nil &&
		offer.EndDate.Before(*offer.StartDate) {

		return errors.New("end date must be after start date")
	}

	offer.IsActive = true
	offer.IsDelete = false
	offer.UpdatedAt = &now

	//------------------------------------------------
	// Save Offer
	//------------------------------------------------

	if err := s.offerRepo.UpdateOffer(offer); err != nil {
		return err
	}

	//------------------------------------------------
	// Create Status Log
	//------------------------------------------------

	if err := s.createStatusLog(
		userID,
		astrologer.ID,
		"OFFER_UPDATED",
		"OLD_OFFER",
		"NEW_OFFER",
	); err != nil {
		return err
	}

	return nil
}

///////////////////////////////////////////////////////////
// Status flags
//
// Each endpoint applies its own change and then answers with the whole set of
// flags as they stand afterwards. The apply* methods hold the original logic;
// these wrappers add the snapshot, which is what the endpoints return instead
// of the null data they used to send.
///////////////////////////////////////////////////////////

func (s *dashboardService) UpdateOnlineStatus(
	userID uint,
	isOnline bool,
) (*dto.StatusFlagsResponse, error) {

	if err := s.applyOnlineStatus(userID, isOnline); err != nil {
		return nil, err
	}

	return s.statusFlags(userID)
}

func (s *dashboardService) UpdateChatStatus(
	userID uint,
	isAvailable bool,
) (*dto.StatusFlagsResponse, error) {

	if err := s.applyChatStatus(userID, isAvailable); err != nil {
		return nil, err
	}

	return s.statusFlags(userID)
}

func (s *dashboardService) UpdateCallStatus(
	userID uint,
	isAvailable bool,
) (*dto.StatusFlagsResponse, error) {

	if err := s.applyCallStatus(userID, isAvailable); err != nil {
		return nil, err
	}

	return s.statusFlags(userID)
}

func (s *dashboardService) UpdateBusyStatus(
	userID uint,
	isBusy bool,
) (*dto.StatusFlagsResponse, error) {

	if err := s.applyBusyStatus(userID, isBusy); err != nil {
		return nil, err
	}

	return s.statusFlags(userID)
}

func (s *dashboardService) UpdateQuickDND(
	userID uint,
	enable bool,
) (*dto.StatusFlagsResponse, error) {

	if err := s.applyQuickDND(userID, enable); err != nil {
		return nil, err
	}

	return s.statusFlags(userID)
}

// statusFlags re-reads astrologer_status after a change, so the response
// reflects the stored row rather than the caller's own idea of it — the
// repositories also move current_status and the *_at timestamps.
func (s *dashboardService) statusFlags(userID uint) (*dto.StatusFlagsResponse, error) {

	astrologer, err := s.dashboardRepo.GetAstrologerByUserID(userID)

	if err != nil {
		return nil, err
	}

	if astrologer == nil {
		return nil, errors.New("astrologer not found")
	}

	status, err := s.statusRepo.GetStatus(astrologer.ID)

	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	// The write above created the row when it was missing, so this is only
	// reached if it has since gone; report the flags as all off rather than
	// failing a change that did land.
	if status == nil {
		return &dto.StatusFlagsResponse{CurrentStatus: "OFFLINE"}, nil
	}

	return &dto.StatusFlagsResponse{
		IsOnline:      status.IsOnline,
		IsChatOnline:  status.IsChatOnline,
		IsCallOnline:  status.IsCallOnline,
		IsBusy:        status.IsBusy,
		QuickDND:      status.QuickDND,
		CurrentStatus: status.CurrentStatus,
		LastActiveAt:  status.LastActiveAt,
	}, nil
}
