package services_astrologer

import (
	"astrology-api/constants"
	dto "astrology-api/dto_astrologer"
	models "astrology-api/models/astrologermodel"
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
)

///////////////////////////////////////////////////////////
// Settlement
///////////////////////////////////////////////////////////
//
// The astrologer's side of the settlement flow, which is read-only. Nothing
// here can release or settle anything — that is the admin's decision and the
// settlement job's write.
//
// The three screens:
//
//	settlement/upcoming        what has been earned but not credited yet,
//	                           split by where it is in the review flow
//	settlement/history         the batches that have been credited
//	settlement/history/:id     the sessions one batch closed
//
// Only what has reached the wallet is withdrawable, and the withdraw flow
// reads the wallet balance, so there is no way for an unsettled earning to
// leak into a payout.

//////////////////////////////////////////////////////////////
// Upcoming
//////////////////////////////////////////////////////////////

func (s *walletService) GetUpcomingSettlement(
	userID uint,
) (*dto.UpcomingSettlementResponse, error) {

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

	if err != nil {
		return nil, err
	}

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

	records, err := s.repository.GetUnsettledConsultations(astrologer.ID)

	if err != nil {
		return nil, err
	}

	response := &dto.UpcomingSettlementResponse{
		Consultations: make([]dto.SettlementConsultationItem, 0, len(records)),
	}

	for index := range records {

		record := records[index]

		switch record.SettlementStatus {

		case constants.SettlementStatusPending:
			response.PendingCount++
			response.PendingAmount += record.AstrologerEarning

		case constants.SettlementStatusToBeSettled:
			response.ToBeSettledCount++
			response.ToBeSettledAmount += record.AstrologerEarning

		case constants.SettlementStatusOnHold:
			response.OnHoldCount++
			response.OnHoldAmount += record.AstrologerEarning
		}

		response.Consultations = append(
			response.Consultations,
			mapSettlementConsultation(&record),
		)
	}

	response.PendingAmount = roundAmount(response.PendingAmount)
	response.ToBeSettledAmount = roundAmount(response.ToBeSettledAmount)
	response.OnHoldAmount = roundAmount(response.OnHoldAmount)

	// On-hold money is deliberately left out: it is not on its way anywhere
	// until an admin releases it.
	response.TotalUnsettled = roundAmount(
		response.PendingAmount + response.ToBeSettledAmount,
	)

	//------------------------------------------------
	// Next Run
	//------------------------------------------------
	//
	// Read from the same systemflag rows the admin general settings screen
	// writes, so the astrologer app and the panel quote the same date.

	if next := s.nextSettlementDate(); next != "" {
		response.NextSettlementAt = next
	}

	return response, nil
}

// nextSettlementDate works out when the settlement job is next due from the
// stored schedule. Empty for a custom cron expression — parsing one belongs to
// whatever runs it, not here.
func (s *walletService) nextSettlementDate() string {

	frequency := strings.ToUpper(strings.TrimSpace(
		s.repository.GetSystemFlagString(
			constants.FlagSettlementFrequency,
			constants.SettlementFrequencyWeekly,
		),
	))

	if frequency == constants.SettlementFrequencyCustom {
		return ""
	}

	runTime := s.repository.GetSystemFlagString(constants.FlagSettlementRunTime, "02:00")

	hour, minute := 2, 0

	if parts := strings.Split(runTime, ":"); len(parts) == 2 {

		if value, err := strconv.Atoi(strings.TrimSpace(parts[0])); err == nil && value >= 0 && value <= 23 {
			hour = value
		}

		if value, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil && value >= 0 && value <= 59 {
			minute = value
		}
	}

	now := time.Now()

	if frequency == constants.SettlementFrequencyMonthly {

		day := int(s.repository.GetSystemFlagFloat(constants.FlagSettlementDayOfMonth, 1))

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

		candidate := time.Date(now.Year(), now.Month(), day, hour, minute, 0, 0, now.Location())

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

		return candidate.Format("02 Jan 2006 03:04 PM")
	}

	weekday := int(s.repository.GetSystemFlagFloat(constants.FlagSettlementDayOfWeek, 1))

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

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

	candidate = candidate.AddDate(0, 0, (weekday-int(candidate.Weekday())+7)%7)

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

	return candidate.Format("02 Jan 2006 03:04 PM")
}

//////////////////////////////////////////////////////////////
// History
//////////////////////////////////////////////////////////////

func (s *walletService) GetSettlementBatches(
	userID uint,
	page int,
	limit int,
) (*dto.SettlementHistoryResponse, error) {

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

	if err != nil {
		return nil, err
	}

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

	batches, total, totalSettled, err := s.repository.GetSettlementBatches(
		astrologer.ID,
		page,
		limit,
	)

	if err != nil {
		return nil, err
	}

	items := make([]dto.SettlementHistoryItem, 0, len(batches))

	for index := range batches {
		items = append(items, mapSettlementBatch(&batches[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.SettlementHistoryResponse{
		Items:        items,
		Page:         page,
		Limit:        limit,
		Total:        total,
		TotalPages:   totalPages,
		TotalSettled: roundAmount(totalSettled),
	}, nil
}

func (s *walletService) GetSettlementBatchDetail(
	userID uint,
	settlementID uint,
) (*dto.SettlementBatchDetailResponse, error) {

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

	if err != nil {
		return nil, err
	}

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

	// Scoped to this astrologer, so one cannot read another's settlement by
	// guessing an id.
	batch, err := s.repository.GetSettlementBatch(astrologer.ID, settlementID)

	if err != nil {
		return nil, err
	}

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

	records, err := s.repository.GetSettlementConsultations(astrologer.ID, settlementID)

	if err != nil {
		return nil, err
	}

	items := make([]dto.SettlementConsultationItem, 0, len(records))

	for index := range records {
		items = append(items, mapSettlementConsultation(&records[index]))
	}

	return &dto.SettlementBatchDetailResponse{
		SettlementHistoryItem: mapSettlementBatch(batch),
		Consultations:         items,
	}, nil
}

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

func mapSettlementBatch(batch *models.WalletSettlement) dto.SettlementHistoryItem {

	item := dto.SettlementHistoryItem{
		ID:                batch.ID,
		SettlementNo:      batch.SettlementNo,
		Amount:            batch.Amount,
		GrossAmount:       batch.GrossAmount,
		PlatformFee:       batch.PlatformFee,
		ConsultationCount: batch.ConsultationCount,
		Status:            batch.SettlementStatus,
		Remarks:           batch.Remarks,
	}

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

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

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

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

	return item
}

func mapSettlementConsultation(
	record *models.Consultation,
) dto.SettlementConsultationItem {

	item := dto.SettlementConsultationItem{
		ConsultationID:    record.ID,
		ConsultationNo:    record.ConsultationNo,
		Medium:            record.Medium,
		Duration:          formatSessionDuration(record.DurationSeconds),
		BilledMinutes:     record.BilledMinutes,
		RatePerMinute:     record.RatePerMinute,
		GrossAmount:       record.GrossAmount,
		PlatformFee:       record.PlatformFeeAmount,
		AstrologerEarning: record.AstrologerEarning,
		SettlementStatus:  record.SettlementStatus,
		StatusLabel:       settlementStatusLabel(record.SettlementStatus),
		IsFreeChat:        record.FreeMinutes > 0,
		FreeMinutes:       record.FreeMinutes,
	}

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

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

	return item
}

// settlementStatusLabel keeps the internal status strings out of the app's UI
// while still returning the raw value alongside it.
func settlementStatusLabel(status string) string {

	switch status {

	case constants.SettlementStatusPending:
		return "Awaiting review"

	case constants.SettlementStatusToBeSettled:
		return "Approved, awaiting settlement"

	case constants.SettlementStatusSettled:
		return "Settled"

	case constants.SettlementStatusOnHold:
		return "On hold"

	case constants.SettlementStatusRejected:
		return "Rejected"

	default:
		return "Not billed"
	}
}

func formatSessionDuration(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 roundAmount(value float64) float64 {
	return math.Round(value*100) / 100
}

//////////////////////////////////////////////////////////////
// Consultation earnings
//////////////////////////////////////////////////////////////

// GetConsultationEarnings is the astrologer's "what have I earned" screen.
//
// It exists because settlement/history answers a narrower question than its
// name suggests: that endpoint lists credited BATCHES, and a batch only exists
// after an admin has approved sessions and the settlement job has run. Until
// then an astrologer who has taken a dozen chats sees an empty screen, which
// reads as a bug rather than as "nothing has been paid out yet".
//
// This one shows every billed session from the first one, with where its money
// has got to.
func (s *walletService) GetConsultationEarnings(
	userID uint,
	status string,
	medium string,
	page int,
	limit int,
) (*dto.ConsultationEarningsResponse, error) {

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

	if err != nil {
		return nil, err
	}

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

	records, total, totals, err := s.repository.ListConsultationEarnings(
		astrologer.ID,
		status,
		medium,
		page,
		limit,
	)

	if err != nil {
		return nil, err
	}

	if page <= 0 {
		page = 1
	}

	if limit <= 0 || limit > 100 {
		limit = 20
	}

	response := &dto.ConsultationEarningsResponse{
		Items: make([]dto.SettlementConsultationItem, 0, len(records)),
		Page:  page,
		Limit: limit,
		Total: total,
	}

	for index := range records {
		response.Items = append(response.Items, mapSettlementConsultation(&records[index]))
	}

	response.PendingAmount = roundAmount(totals[constants.SettlementStatusPending])
	response.ToBeSettledAmount = roundAmount(totals[constants.SettlementStatusToBeSettled])
	response.OnHoldAmount = roundAmount(totals[constants.SettlementStatusOnHold])
	response.SettledAmount = roundAmount(totals[constants.SettlementStatusSettled])

	// On-hold and rejected money is left out: neither is on its way anywhere
	// until an admin releases it.
	response.TotalEarning = roundAmount(
		response.PendingAmount + response.ToBeSettledAmount + response.SettledAmount,
	)

	response.TotalPages = int(total) / limit

	if int(total)%limit != 0 {
		response.TotalPages++
	}

	return response, nil
}
