package services_admin

import (
	"astrology-api/constants"
	dto "astrology-api/dto_admin"
	usermodel "astrology-api/models/usermodel"
	"astrology-api/notifications"
	"astrology-api/repositories_admin"
	"astrology-api/services"
	"errors"
	"fmt"
	"math"
	"strings"
	"time"
)

// The admin finance manager.
//
// Where the money is in the flow, and what this service is allowed to do to it:
//
//	PENDING        the session was billed to the customer and the astrologer's
//	               earning is recorded as owed. Review actions can move it.
//	TO_BE_SETTLED  an admin released it. The job will credit it.
//	SETTLED        credited to the astrologer wallet. Nothing here may touch
//	               it again — the money has moved and the astrologer can
//	               already have withdrawn it.
//	ON_HOLD        parked, pending a dispute or a bad review.
//	REJECTED       refused for good. Never credited.
//
// The customer is *not* refunded by a hold or a rejection. Those decide
// whether the astrologer is paid, which is a separate question from whether
// the customer was charged; a refund is a wallet credit and is not part of
// this flow.

// Review actions, as the panel sends them.
const (
	ActionApprove = "APPROVE"
	ActionHold    = "HOLD"
	ActionReject  = "REJECT"
	ActionRevert  = "REVERT"
)

type SettlementService interface {

	//------------------------------------------------
	// Review screens
	//------------------------------------------------

	ListConsultations(
		filter dto.SettlementListFilter,
	) (*dto.ConsultationSettlementListResponse, error)

	GetConsultationDetail(id uint) (*dto.ConsultationSettlementDetail, error)

	ReviewConsultations(
		request dto.ReviewSettlementRequest,
		adminID uint,
		systemCaller bool,
	) (*dto.ReviewSettlementResponse, error)

	//------------------------------------------------
	// The job
	//------------------------------------------------

	RunSettlement(
		request dto.RunSettlementRequest,
		adminID uint,
		systemCaller bool,
	) (*dto.RunSettlementResponse, error)

	SweepStaleConsultations(limit int) (*dto.SweepResponse, error)

	//------------------------------------------------
	// History
	//------------------------------------------------

	ListSettlementBatches(
		astrologerID uint,
		search string,
		fromDate string,
		toDate string,
		page int,
		limit int,
	) (*dto.SettlementBatchListResponse, error)

	GetSettlementBatch(id uint) (*dto.SettlementBatchDetail, error)

	//------------------------------------------------
	// Summary
	//------------------------------------------------

	GetFinanceSummary() (*dto.FinanceSummaryResponse, error)
}

type settlementService struct {
	repository repositories_admin.SettlementRepository

	settings SettingsService

	// The user-side consultation service owns billing. The sweep is wired to
	// it rather than reimplemented here, so an abandoned session is billed by
	// exactly the same code as one the customer ended.
	consultations services.ConsultationService
}

func NewSettlementService(
	repository repositories_admin.SettlementRepository,
	settings SettingsService,
	consultations services.ConsultationService,
) SettlementService {

	return &settlementService{
		repository:    repository,
		settings:      settings,
		consultations: consultations,
	}
}

//////////////////////////////////////////////////////////////
// Review screens
//////////////////////////////////////////////////////////////

func (s *settlementService) ListConsultations(
	filter dto.SettlementListFilter,
) (*dto.ConsultationSettlementListResponse, error) {

	rows, total, totals, err := s.repository.ListConsultations(repositories_admin.ListFilter{
		Page:             filter.Page,
		Limit:            filter.Limit,
		Search:           filter.Search,
		SettlementStatus: filter.SettlementStatus,
		Medium:           filter.Medium,
		AstrologerID:     filter.AstrologerID,
		UserID:           filter.UserID,
		FromDate:         filter.FromDate,
		ToDate:           filter.ToDate,
		SortBy:           filter.SortBy,
		SortDir:          filter.SortDir,
	})

	if err != nil {
		return nil, err
	}

	items := make([]dto.ConsultationSettlementItem, 0, len(rows))

	for index := range rows {
		items = append(items, mapConsultationRow(&rows[index]))
	}

	page := filter.Page
	limit := filter.Limit

	if page <= 0 {
		page = 1
	}

	if limit <= 0 {
		limit = 10
	}

	totalPages := 0

	if total > 0 {
		totalPages = int(math.Ceil(float64(total) / float64(limit)))
	}

	return &dto.ConsultationSettlementListResponse{
		Items:            items,
		Page:             page,
		Limit:            limit,
		Total:            total,
		TotalPages:       totalPages,
		TotalGross:       round2(totals.Gross),
		TotalPlatformFee: round2(totals.PlatformFee),
		TotalEarning:     round2(totals.Earning),
	}, nil
}

func (s *settlementService) GetConsultationDetail(
	id uint,
) (*dto.ConsultationSettlementDetail, error) {

	row, err := s.repository.GetConsultationRow(id)

	if err != nil {
		return nil, err
	}

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

	detail := &dto.ConsultationSettlementDetail{
		ConsultationSettlementItem: mapConsultationRow(row),

		ChannelName:        row.ChannelName,
		DurationSeconds:    row.DurationSeconds,
		BilledSeconds:      row.BilledSeconds,
		MaxBillableSeconds: row.MaxBillableSeconds,

		// A session that ran exactly to its cap was cut off by the balance
		// rather than ended by either party. Worth seeing on the review
		// screen, because it is the common cause of a complaint.
		HitBalanceCap: row.MaxBillableSeconds > 0 &&
			row.BilledSeconds >= row.MaxBillableSeconds,

		ConsulteeName:    row.ConsulteeName,
		BirthTime:        row.BirthTime,
		BirthPlace:       row.BirthPlace,
		Gender:           row.Gender,
		AstrologerMobile: row.AstrologerMobile,
		UserEmail:        row.UserEmail,
	}

	if row.BirthDate != nil {
		detail.ConsulteeBirth = row.BirthDate.Format("2006-01-02")
	}

	if row.StartedAt != nil {
		detail.StartedAt = row.StartedAt.Format("02 Jan 2006 03:04 PM")
	}

	if row.EndedAt != nil {
		detail.EndedAt = row.EndedAt.Format("02 Jan 2006 03:04 PM")
	}

	//------------------------------------------------
	// What Actually Left The Wallet
	//------------------------------------------------

	if debit, err := s.repository.GetWalletDebit(row.ConsultationNo); err == nil {
		detail.WalletDebit = round2(debit)
	}

	//------------------------------------------------
	// Settlement Trail
	//------------------------------------------------

	logs, err := s.repository.GetSettlementLogs(id)

	if err != nil {
		return nil, err
	}

	detail.History = make([]dto.SettlementLogItem, 0, len(logs))

	for _, log := range logs {

		item := dto.SettlementLogItem{
			FromStatus:   log.FromStatus,
			ToStatus:     log.ToStatus,
			Remarks:      log.Remarks,
			ActionBy:     log.ActionBy,
			ActionSource: log.ActionSource,
		}

		if log.SettlementID != nil {
			item.SettlementID = *log.SettlementID
		}

		if log.CreatedAt != nil {
			item.ActionAt = log.CreatedAt.Format("02 Jan 2006 03:04 PM")
		}

		detail.History = append(detail.History, item)
	}

	return detail, nil
}

//////////////////////////////////////////////////////////////
// Review actions
//////////////////////////////////////////////////////////////

func (s *settlementService) ReviewConsultations(
	request dto.ReviewSettlementRequest,
	adminID uint,
	systemCaller bool,
) (*dto.ReviewSettlementResponse, error) {

	action := strings.ToUpper(strings.TrimSpace(request.Action))

	toStatus, allowedFrom, err := transitionFor(action)

	if err != nil {
		return nil, err
	}

	if len(request.ConsultationIDs) == 0 {
		return nil, errors.New("consultation_ids is required")
	}

	// A rejection or a hold is a decision somebody has to be able to explain
	// later, so it carries a reason.
	if (action == ActionReject || action == ActionHold) &&
		strings.TrimSpace(request.Remarks) == "" {

		return nil, errors.New("remarks are required when holding or rejecting a settlement")
	}

	records, err := s.repository.GetConsultationsByIDs(request.ConsultationIDs)

	if err != nil {
		return nil, err
	}

	found := map[uint]usermodel.Consultation{}

	for index := range records {
		found[records[index].ID] = records[index]
	}

	//------------------------------------------------
	// Decide Per Row
	//------------------------------------------------
	//
	// One bad id must not fail the whole batch: the panel reviews in bulk and
	// needs to be told which rows moved and which did not.

	eligible := make([]uint, 0, len(request.ConsultationIDs))
	fromStatuses := map[uint]string{}
	skipped := make([]dto.SkippedConsultation, 0)

	total := float64(0)

	for _, id := range request.ConsultationIDs {

		record, exists := found[id]

		if !exists {

			skipped = append(skipped, dto.SkippedConsultation{
				ConsultationID: id,
				Reason:         "consultation not found",
			})

			continue
		}

		if record.SettlementStatus == constants.SettlementStatusSettled {

			skipped = append(skipped, dto.SkippedConsultation{
				ConsultationID: id,
				Reason:         "already settled, the amount is in the astrologer wallet",
			})

			continue
		}

		if !allowedFrom[record.SettlementStatus] {

			skipped = append(skipped, dto.SkippedConsultation{
				ConsultationID: id,
				Reason: fmt.Sprintf(
					"cannot %s a consultation in %s",
					strings.ToLower(action),
					record.SettlementStatus,
				),
			})

			continue
		}

		eligible = append(eligible, id)
		fromStatuses[id] = record.SettlementStatus
		total += record.AstrologerEarning
	}

	//------------------------------------------------
	// Apply
	//------------------------------------------------

	source := constants.ActionSourceAdmin

	if systemCaller {
		source = constants.ActionSourceSystem
	}

	if len(eligible) > 0 {

		err = s.repository.ApplyReview(
			eligible,
			toStatus,
			fromStatuses,
			strings.TrimSpace(request.Remarks),
			adminID,
			source,
		)

		if err != nil {
			return nil, err
		}

		//------------------------------------------------
		// Notify The Astrologers
		//------------------------------------------------
		//
		// One notification per astrologer, not per consultation: approving
		// forty rows is one decision and should read as one message. The
		// money is not credited yet — SettlementCredited says that, and only
		// the settlement run sends it.

		for astrologerID, group := range groupByAstrologer(found, eligible) {

			notifications.Notify().SettlementStatusChanged(
				astrologerID,
				toStatus,
				group.count,
				round2(group.amount),
			)
		}
	}

	return &dto.ReviewSettlementResponse{
		Action:       action,
		UpdatedCount: len(eligible),
		UpdatedIDs:   eligible,
		Skipped:      skipped,
		TotalEarning: round2(total),
	}, nil
}

// astrologerGroup is one astrologer's share of a bulk review action.
type astrologerGroup struct {
	count  int
	amount float64
}

// groupByAstrologer collects the moved consultations per astrologer so each
// one is told once about their own earnings.
func groupByAstrologer(
	records map[uint]usermodel.Consultation,
	ids []uint,
) map[uint]astrologerGroup {

	groups := map[uint]astrologerGroup{}

	for _, id := range ids {

		record, exists := records[id]

		if !exists {
			continue
		}

		group := groups[record.AstrologerID]

		group.count++
		group.amount += record.AstrologerEarning

		groups[record.AstrologerID] = group
	}

	return groups
}

// transitionFor maps an action to the status it writes and the statuses it may
// be applied from. SETTLED appears in no allowed set — once the wallet has
// been credited, the decision is final.
func transitionFor(action string) (string, map[string]bool, error) {

	switch action {

	case ActionApprove:

		return constants.SettlementStatusToBeSettled, map[string]bool{
			constants.SettlementStatusPending: true,
			constants.SettlementStatusOnHold:  true,
		}, nil

	case ActionHold:

		return constants.SettlementStatusOnHold, map[string]bool{
			constants.SettlementStatusPending:     true,
			constants.SettlementStatusToBeSettled: true,
		}, nil

	case ActionReject:

		return constants.SettlementStatusRejected, map[string]bool{
			constants.SettlementStatusPending:     true,
			constants.SettlementStatusToBeSettled: true,
			constants.SettlementStatusOnHold:      true,
		}, nil

	case ActionRevert:

		return constants.SettlementStatusPending, map[string]bool{
			constants.SettlementStatusToBeSettled: true,
			constants.SettlementStatusOnHold:      true,
			constants.SettlementStatusRejected:    true,
		}, nil

	default:
		return "", nil, errors.New("action must be APPROVE, HOLD, REJECT or REVERT")
	}
}

//////////////////////////////////////////////////////////////
// The settlement job
//////////////////////////////////////////////////////////////

// RunSettlement is what the admin panel's cron calls. It sweeps every
// TO_BE_SETTLED consultation into per-astrologer batches and credits each
// astrologer's wallet.
//
// Batched per astrologer, in its own transaction, on purpose: one astrologer
// with a broken wallet row must not stop everybody else from being paid, and a
// run that dies halfway leaves whole astrologers settled rather than a
// half-written batch.
func (s *settlementService) RunSettlement(
	request dto.RunSettlementRequest,
	adminID uint,
	systemCaller bool,
) (*dto.RunSettlementResponse, error) {

	settings, err := s.settings.GetGeneralSettings()

	if err != nil {
		return nil, err
	}

	//------------------------------------------------
	// What Counts As Settlable
	//------------------------------------------------
	//
	// TO_BE_SETTLED always. PENDING only when the admin has explicitly
	// switched auto-approve on, because settling PENDING means paying out
	// sessions nobody reviewed.

	statuses := []string{constants.SettlementStatusToBeSettled}

	if settings.SettlementAutoApprove {
		statuses = append(statuses, constants.SettlementStatusPending)
	}

	//------------------------------------------------
	// Period Cut-off
	//------------------------------------------------

	var upTo *time.Time

	if value := strings.TrimSpace(request.UpToDate); value != "" {

		parsed, err := time.Parse("2006-01-02", value)

		if err != nil {
			return nil, errors.New("up_to_date must be YYYY-MM-DD")
		}

		// Inclusive of the whole day.
		end := parsed.Add(24*time.Hour - time.Second)

		upTo = &end
	}

	astrologerIDs, err := s.repository.GetSettlableAstrologerIDs(
		statuses,
		request.AstrologerID,
		upTo,
	)

	if err != nil {
		return nil, err
	}

	now := time.Now()

	response := &dto.RunSettlementResponse{
		DryRun:  request.DryRun,
		Batches: make([]dto.SettlementBatchResult, 0, len(astrologerIDs)),
		RunAt:   now.Format("2006-01-02 15:04:05"),
	}

	source := constants.ActionSourceAdmin

	if systemCaller {
		source = constants.ActionSourceSystem
	}

	remarks := fmt.Sprintf("Settlement run %s", now.Format("02 Jan 2006 15:04"))

	for _, astrologerID := range astrologerIDs {

		result := dto.SettlementBatchResult{
			AstrologerID: astrologerID,
		}

		if astrologer, err := s.repository.GetAstrologer(astrologerID); err == nil && astrologer != nil {

			result.AstrologerName = astrologer.DisplayName

			if strings.TrimSpace(result.AstrologerName) == "" {
				result.AstrologerName = astrologer.Name
			}
		}

		//------------------------------------------------
		// Preview
		//------------------------------------------------
		//
		// Read before writing, so a dry run and a real run report the same
		// thing and the minimum-payout rule can be applied without opening a
		// transaction.

		pending, err := s.repository.GetSettlableConsultations(astrologerID, statuses, upTo)

		if err != nil {

			result.Skipped = true
			result.Reason = err.Error()

			response.Batches = append(response.Batches, result)

			continue
		}

		var gross, fee, amount float64
		var periodFrom, periodTo *time.Time

		for index := range pending {

			record := pending[index]

			gross += record.GrossAmount
			fee += record.PlatformFeeAmount
			amount += record.AstrologerEarning

			moment := record.EndedAt

			if moment == nil {
				moment = record.CreatedAt
			}

			if moment != nil {

				if periodFrom == nil || moment.Before(*periodFrom) {
					periodFrom = moment
				}

				if periodTo == nil || moment.After(*periodTo) {
					periodTo = moment
				}
			}
		}

		result.ConsultationCount = len(pending)
		result.GrossAmount = round2(gross)
		result.PlatformFee = round2(fee)
		result.SettledAmount = round2(amount)

		if periodFrom != nil {
			result.PeriodFrom = periodFrom.Format("2006-01-02")
		}

		if periodTo != nil {
			result.PeriodTo = periodTo.Format("2006-01-02")
		}

		if len(pending) == 0 {

			result.Skipped = true
			result.Reason = "nothing to settle"

			response.Batches = append(response.Batches, result)

			continue
		}

		//------------------------------------------------
		// Minimum Payout
		//------------------------------------------------
		//
		// Below the threshold the batch is left alone and rolls into the next
		// run, rather than filling the wallet with tiny credits.

		if settings.SettlementMinPayout > 0 && result.SettledAmount < settings.SettlementMinPayout {

			result.Skipped = true
			result.Reason = fmt.Sprintf(
				"below minimum payout of %.2f, rolled to the next run",
				settings.SettlementMinPayout,
			)

			response.Batches = append(response.Batches, result)

			continue
		}

		if request.DryRun {

			response.AstrologerCount++
			response.ConsultationCount += result.ConsultationCount
			response.SettledAmount = round2(response.SettledAmount + result.SettledAmount)

			response.Batches = append(response.Batches, result)

			continue
		}

		//------------------------------------------------
		// Settle
		//------------------------------------------------

		outcome, err := s.repository.SettleAstrologer(
			astrologerID,
			statuses,
			upTo,
			adminID,
			source,
			remarks,
		)

		if err != nil {

			// Counted and reported, not returned: the next astrologer still
			// gets paid.
			result.Skipped = true
			result.Reason = err.Error()

			response.Batches = append(response.Batches, result)

			continue
		}

		if outcome == nil {

			result.Skipped = true
			result.Reason = "already settled by another run"

			response.Batches = append(response.Batches, result)

			continue
		}

		result.SettlementID = outcome.SettlementID
		result.SettlementNo = outcome.SettlementNo
		result.ConsultationCount = outcome.Count
		result.GrossAmount = outcome.Gross
		result.PlatformFee = outcome.PlatformFee
		result.SettledAmount = outcome.Amount
		result.WalletBalanceAfter = outcome.WalletBalance

		if outcome.PeriodFrom != nil {
			result.PeriodFrom = outcome.PeriodFrom.Format("2006-01-02")
		}

		if outcome.PeriodTo != nil {
			result.PeriodTo = outcome.PeriodTo.Format("2006-01-02")
		}

		response.AstrologerCount++
		response.ConsultationCount += outcome.Count
		response.SettledAmount = round2(response.SettledAmount + outcome.Amount)

		response.Batches = append(response.Batches, result)

		//------------------------------------------------
		// Notify The Astrologer
		//------------------------------------------------
		//
		// The money is in the wallet as of this batch's commit, so this is
		// the notification that tells the astrologer they can withdraw.

		notifications.Notify().SettlementCredited(
			astrologerID,
			outcome.Amount,
			outcome.WalletBalance,
			outcome.Count,
			outcome.SettlementNo,
		)
	}

	return response, nil
}

// SweepStaleConsultations is the panel scheduler's one call, and it now runs
// every pass the consultation lifecycle needs, not just the abandoned-session
// one the name suggests.
//
// The route and the name are unchanged on purpose: the panel's cron entry is
// already configured against them, and renaming it would mean coordinating a
// deployment in another repository to gain nothing.
//
// Passes, in order — warn before cutting, and close a capped session with the
// honest reason before the stale pass could label the same row ABANDONED:
//
//	warnings -> cap reached -> stale tick -> expired requests ->
//	accepted but never opened -> busy flag reconcile
//
// Every pass is idempotent and re-entrant, so two overlapping runs cannot
// double-bill, and a pass that fails is reported rather than aborting the
// rest.
func (s *settlementService) SweepStaleConsultations(
	limit int,
) (*dto.SweepResponse, error) {

	result, err := s.consultations.Sweep(limit)

	if err != nil {
		return nil, err
	}

	return &dto.SweepResponse{
		ClosedCount:    result.Abandoned + result.Capped,
		WarnedCount:    result.Warned,
		CappedCount:    result.Capped,
		AbandonedCount: result.Abandoned,
		MissedCount:    result.Missed,
		UnjoinedCount:  result.Unjoined,
		BusyReconciled: result.BusyReconciled,
		Errors:         result.Errors,
		RunAt:          result.RunAt,
	}, nil
}

//////////////////////////////////////////////////////////////
// Settlement history
//////////////////////////////////////////////////////////////

func (s *settlementService) ListSettlementBatches(
	astrologerID uint,
	search string,
	fromDate string,
	toDate string,
	page int,
	limit int,
) (*dto.SettlementBatchListResponse, error) {

	rows, total, totalAmount, err := s.repository.ListSettlementBatches(
		astrologerID,
		search,
		fromDate,
		toDate,
		page,
		limit,
	)

	if err != nil {
		return nil, err
	}

	items := make([]dto.SettlementBatchItem, 0, len(rows))

	for index := range rows {
		items = append(items, mapBatchRow(&rows[index]))
	}

	if page <= 0 {
		page = 1
	}

	if limit <= 0 {
		limit = 10
	}

	totalPages := 0

	if total > 0 {
		totalPages = int(math.Ceil(float64(total) / float64(limit)))
	}

	return &dto.SettlementBatchListResponse{
		Items:       items,
		Page:        page,
		Limit:       limit,
		Total:       total,
		TotalPages:  totalPages,
		TotalAmount: round2(totalAmount),
	}, nil
}

func (s *settlementService) GetSettlementBatch(
	id uint,
) (*dto.SettlementBatchDetail, error) {

	row, err := s.repository.GetSettlementBatch(id)

	if err != nil {
		return nil, err
	}

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

	consultations, err := s.repository.GetConsultationsBySettlementID(id)

	if err != nil {
		return nil, err
	}

	items := make([]dto.ConsultationSettlementItem, 0, len(consultations))

	for index := range consultations {
		items = append(items, mapConsultationRow(&consultations[index]))
	}

	return &dto.SettlementBatchDetail{
		SettlementBatchItem: mapBatchRow(row),
		Consultations:       items,
	}, nil
}

//////////////////////////////////////////////////////////////
// Summary
//////////////////////////////////////////////////////////////

func (s *settlementService) GetFinanceSummary() (*dto.FinanceSummaryResponse, error) {

	response := &dto.FinanceSummaryResponse{}

	//------------------------------------------------
	// Per Status
	//------------------------------------------------

	aggregates, err := s.repository.GetStatusAggregates()

	if err != nil {
		return nil, err
	}

	for _, aggregate := range aggregates {

		switch aggregate.SettlementStatus {

		case constants.SettlementStatusPending:
			response.PendingCount = aggregate.Count
			response.PendingAmount = round2(aggregate.Amount)

		case constants.SettlementStatusToBeSettled:
			response.ToBeSettledCount = aggregate.Count
			response.ToBeSettledAmount = round2(aggregate.Amount)

		case constants.SettlementStatusOnHold:
			response.OnHoldCount = aggregate.Count
			response.OnHoldAmount = round2(aggregate.Amount)

		case constants.SettlementStatusRejected:
			response.RejectedCount = aggregate.Count
			response.RejectedAmount = round2(aggregate.Amount)

		case constants.SettlementStatusSettled:
			response.SettledCount = aggregate.Count
			response.SettledAmount = round2(aggregate.Amount)
		}
	}

	//------------------------------------------------
	// Billed
	//------------------------------------------------

	gross, fee, err := s.repository.GetBilledTotals(nil)

	if err != nil {
		return nil, err
	}

	response.TotalGrossBilled = round2(gross)
	response.TotalPlatformFee = round2(fee)

	now := time.Now()

	monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())

	monthGross, monthFee, err := s.repository.GetBilledTotals(&monthStart)

	if err != nil {
		return nil, err
	}

	response.MonthGrossBilled = round2(monthGross)
	response.MonthPlatformFee = round2(monthFee)

	monthSettled, err := s.repository.GetSettledAmount(&monthStart)

	if err != nil {
		return nil, err
	}

	response.MonthSettled = round2(monthSettled)

	//------------------------------------------------
	// Outstanding Liability
	//------------------------------------------------

	liability, err := s.repository.GetAstrologerWalletLiability()

	if err != nil {
		return nil, err
	}

	response.AstrologerWalletBalance = round2(liability)

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

	if last, err := s.repository.GetLastSettlementAt(); err == nil && last != nil {
		response.LastSettlementAt = last.Format("02 Jan 2006 03:04 PM")
	}

	if settings, err := s.settings.GetGeneralSettings(); err == nil {
		response.NextSettlementAt = settings.NextRunAt
	}

	return response, nil
}

//////////////////////////////////////////////////////////////
// Mapping
//////////////////////////////////////////////////////////////

func mapConsultationRow(
	row *repositories_admin.ConsultationRow,
) dto.ConsultationSettlementItem {

	item := dto.ConsultationSettlementItem{
		ConsultationID:     row.ID,
		ConsultationNo:     row.ConsultationNo,
		UserID:             row.UserID,
		UserName:           row.UserName,
		UserMobile:         row.UserMobile,
		AstrologerID:       row.AstrologerID,
		AstrologerName:     row.AstrologerName,
		Medium:             row.Medium,
		Status:             row.Status,
		EndReason:          row.EndReason,
		Duration:           formatDuration(row.DurationSeconds),
		BilledMinutes:      row.BilledMinutes,
		RatePerMinute:      row.RatePerMinute,
		GrossAmount:        row.GrossAmount,
		PlatformFeePercent: row.PlatformFeePercent,
		PlatformFeeAmount:  row.PlatformFeeAmount,
		AstrologerEarning:  row.AstrologerEarning,
		SettlementStatus:   row.SettlementStatus,
		SettlementNo:       row.SettlementNo,
		ReviewRating:       row.ReviewRating,
		ReviewText:         row.ReviewText,
		AdminRemarks:       row.AdminRemarks,
	}

	if row.SettlementID != nil {
		item.SettlementID = *row.SettlementID
	}

	if row.SettledAt != nil {
		item.SettledAt = row.SettledAt.Format("02 Jan 2006 03:04 PM")
	}

	if row.CreatedAt != nil {
		item.ConsultationDate = row.CreatedAt.Format("02 Jan 2006 03:04 PM")
	}

	return item
}

func mapBatchRow(
	row *repositories_admin.SettlementBatchRow,
) dto.SettlementBatchItem {

	item := dto.SettlementBatchItem{
		SettlementID:      row.ID,
		SettlementNo:      row.SettlementNo,
		AstrologerID:      row.AstrologerID,
		AstrologerName:    row.AstrologerName,
		ConsultationCount: row.ConsultationCount,
		GrossAmount:       row.GrossAmount,
		PlatformFee:       row.PlatformFee,
		Amount:            row.Amount,
		Status:            row.SettlementStatus,
		ProcessedBy:       row.ProcessedBy,
		ProcessedByName:   row.ProcessedByName,
		Remarks:           row.Remarks,
	}

	// 0 is the scheduler, not a person.
	if row.ProcessedBy == 0 {
		item.ProcessedByName = "Scheduled job"
	}

	if row.PeriodFrom != nil {
		item.PeriodFrom = row.PeriodFrom.Format("02 Jan 2006")
	}

	if row.PeriodTo != nil {
		item.PeriodTo = row.PeriodTo.Format("02 Jan 2006")
	}

	if row.SettlementDate != nil {
		item.SettlementDate = row.SettlementDate.Format("02 Jan 2006 03:04 PM")
	}

	if row.CreatedAt != nil {
		item.CreatedAt = row.CreatedAt.Format("02 Jan 2006 03:04 PM")
	}

	return item
}

func formatDuration(seconds int) string {

	if seconds <= 0 {
		return "00:00"
	}

	if seconds < 3600 {
		return fmt.Sprintf("%02d:%02d", seconds/60, seconds%60)
	}

	return fmt.Sprintf("%02d:%02d:%02d", seconds/3600, (seconds%3600)/60, seconds%60)
}

func round2(value float64) float64 {
	return math.Round(value*100) / 100
}
