package services_astrologer

import (
	"errors"
	"fmt"
	"math"
	"strings"

	"astrology-api/constants"
	dto "astrology-api/dto_astrologer"
	models "astrology-api/models/astrologermodel"
)

///////////////////////////////////////////////////////////
// Withdraw
///////////////////////////////////////////////////////////
//
// Two flows share this file.
//
//   GENERAL  the request is parked at Pending for an admin to approve. The
//            balance is held (debited from the wallet) at request time so the
//            astrologer cannot spend it twice, and the payout happens against
//            the saved account once the admin approves.
//
//   INSTANT  TDS and a flat instant charge come off the requested amount and
//            the remainder is paid out immediately. No payout provider is
//            wired in yet, so the request lands at Processing with an open
//            settlement row for the eventual payout call to close.
//
// Payout details are never taken from the request body. They are read from
// the astrologer's saved astrologer_bank_accounts row and snapshotted onto the
// withdraw row, so a modified client cannot pay itself to an arbitrary
// account, and editing the saved account later cannot redirect a payout that
// has already been approved.

///////////////////////////////////////////////////////////
// Configuration
///////////////////////////////////////////////////////////

// withdrawSettings is the systemflag configuration in force for one request.
// It is read once per call so every figure the astrologer is shown, and every
// figure stored on the row, comes from the same snapshot.
type withdrawSettings struct {
	MinAmount     float64
	TDSPercent    float64
	InstantCharge float64
	InstantMin    float64
	InstantMax    float64
}

// Defaults used when a systemflag row is missing, so the feature works on a
// server where the configuration migration has not been applied yet. They
// match the values the migration inserts.
const (
	defaultMinWithdrawAmount = 500
	defaultWithdrawTDS       = 0
	defaultInstantCharge     = 10
	defaultInstantMin        = 500
	defaultInstantMax        = 10000
)

// Quick-amount chips on the withdraw screen, above the ₹All chip the app adds
// from the available balance.
var withdrawQuickAmounts = []float64{2000, 5000, 10000}

// withdrawCutoffNote is the footnote under the summary card. It describes the
// admin's working hours, so it applies to general withdrawals only.
const withdrawCutoffNote = "Requests raised after 18:00 IST are processed the next working day."

const instantWithdrawNote = "Instant withdrawals are transferred to your saved account right away."

func (s *walletService) withdrawSettings() withdrawSettings {

	return withdrawSettings{
		MinAmount:     s.repository.GetSystemFlagFloat("MinWithdrawAmount", defaultMinWithdrawAmount),
		TDSPercent:    s.repository.GetSystemFlagFloat("WithdrawTDSPercent", defaultWithdrawTDS),
		InstantCharge: s.repository.GetSystemFlagFloat("InstantWithdrawCharge", defaultInstantCharge),
		InstantMin:    s.repository.GetSystemFlagFloat("InstantWithdrawMinAmount", defaultInstantMin),
		InstantMax:    s.repository.GetSystemFlagFloat("InstantWithdrawMaxAmount", defaultInstantMax),
	}
}

// minFor returns the floor that applies to a type. The instant floor is
// allowed to differ from the general one, but never to sit below it.
func (c withdrawSettings) minFor(withdrawType string) float64 {

	if withdrawType == models.WithdrawTypeInstant && c.InstantMin > c.MinAmount {
		return c.InstantMin
	}

	return c.MinAmount
}

// maxFor returns the ceiling that applies to a type, ignoring the balance.
// 0 means the balance is the only limit.
func (c withdrawSettings) maxFor(withdrawType string) float64 {

	if withdrawType == models.WithdrawTypeInstant {
		return c.InstantMax
	}

	return 0
}

///////////////////////////////////////////////////////////
// Input Normalisation
///////////////////////////////////////////////////////////

// normalizeWithdrawType accepts the type case-insensitively and treats an
// empty value as GENERAL, so a client written before the split keeps working.
func normalizeWithdrawType(value string) (string, error) {

	switch strings.ToUpper(strings.TrimSpace(value)) {

	case "", models.WithdrawTypeGeneral:
		return models.WithdrawTypeGeneral, nil

	case models.WithdrawTypeInstant:
		return models.WithdrawTypeInstant, nil
	}

	return "", errors.New("invalid withdraw type, expected GENERAL or INSTANT")
}

func normalizePaymentMethod(value string) (string, error) {

	switch strings.ToUpper(strings.TrimSpace(value)) {

	case models.PaymentMethodUPI:
		return models.PaymentMethodUPI, nil

	case models.PaymentMethodBank, "BANK_TRANSFER", "BANKTRANSFER":
		return models.PaymentMethodBank, nil
	}

	return "", errors.New("invalid payment method, expected UPI or BANK")
}

// roundMoney keeps every stored and displayed figure at two decimals. Without
// it a percentage TDS produces a net amount that does not add back up to the
// gross on the summary card.
func roundMoney(value float64) float64 {
	return math.Round(value*100) / 100
}

// maskAccountNumber renders an account number the way the statement rows do —
// "····9012" — so a full number never leaves the server.
func maskAccountNumber(value string) string {

	trimmed := strings.TrimSpace(value)

	if trimmed == "" {
		return ""
	}

	if len(trimmed) <= 4 {
		return "····" + trimmed
	}

	return "····" + trimmed[len(trimmed)-4:]
}

///////////////////////////////////////////////////////////
// Payout Target
///////////////////////////////////////////////////////////

// withdrawPayout is the saved account resolved for one payment method, ready
// to be snapshotted onto the withdraw row.
type withdrawPayout struct {
	BankAccountID     uint
	PaymentMethod     string
	UPIID             string
	AccountNumber     string
	IFSCCode          string
	AccountHolderName string
	BankName          string

	// Display form: the UPI handle, or "SBI · ····9012".
	Display string
}

// resolvePayout picks the saved detail the chosen method needs.
//
// A rejected account is refused outright. An account still PENDING
// verification is allowed through, because nothing in the astrologer app
// verifies an account today and refusing them would leave every astrologer
// unable to withdraw; the status travels on the config response so the app
// can warn, and the admin approval step is the real gate for general
// withdrawals.
func resolvePayout(
	bank *models.AstrologerBankAccount,
	method string,
) (*withdrawPayout, error) {

	if bank == nil {
		return nil, errors.New("please add your bank details before requesting a withdrawal")
	}

	if strings.EqualFold(bank.VerificationStatus, "REJECTED") {
		return nil, errors.New("your saved bank details were rejected, please update them before withdrawing")
	}

	payout := &withdrawPayout{
		BankAccountID:     bank.ID,
		PaymentMethod:     method,
		AccountHolderName: strings.TrimSpace(bank.AccountHolderName),
		BankName:          strings.TrimSpace(bank.BankName),
	}

	switch method {

	case models.PaymentMethodUPI:

		payout.UPIID = strings.TrimSpace(bank.UPIID)

		if payout.UPIID == "" {
			return nil, errors.New("no upi id saved on your account, please add one or choose bank transfer")
		}

		payout.Display = payout.UPIID

	case models.PaymentMethodBank:

		payout.AccountNumber = strings.TrimSpace(bank.AccountNumber)
		payout.IFSCCode = strings.TrimSpace(bank.IFSCCode)

		if payout.AccountNumber == "" || payout.IFSCCode == "" {
			return nil, errors.New("no bank account saved on your account, please add one or choose upi")
		}

		masked := maskAccountNumber(payout.AccountNumber)

		if payout.BankName != "" {
			payout.Display = payout.BankName + " · " + masked
		} else {
			payout.Display = masked
		}

	default:
		return nil, errors.New("invalid payment method")
	}

	return payout, nil
}

///////////////////////////////////////////////////////////
// Breakdown
///////////////////////////////////////////////////////////

// buildBreakdown turns a gross amount into the Summary card.
//
// A general withdrawal is charged nothing at request time: TDS on it, if it
// is ever applied, is the admin's call at approval, and charging it here
// would show the astrologer a deduction that has not happened.
func buildBreakdown(
	amount float64,
	withdrawType string,
	payout *withdrawPayout,
	settings withdrawSettings,
	balanceAfter float64,
) (*dto.WithdrawPreviewResponse, error) {

	amount = roundMoney(amount)

	preview := &dto.WithdrawPreviewResponse{
		WithdrawType:  withdrawType,
		PaymentMethod: payout.PaymentMethod,
		Amount:        amount,
		BalanceAfter:  roundMoney(balanceAfter),
		PayoutTo:      payout.Display,
		BankName:      payout.BankName,
	}

	if withdrawType == models.WithdrawTypeInstant {

		preview.TDSPercent = settings.TDSPercent
		preview.TDSAmount = roundMoney(amount * settings.TDSPercent / 100)
		preview.InstantCharge = roundMoney(settings.InstantCharge)
		preview.Note = instantWithdrawNote

	} else {

		preview.Note = withdrawCutoffNote
	}

	preview.TotalDeduction = roundMoney(preview.TDSAmount + preview.InstantCharge)
	preview.NetAmount = roundMoney(amount - preview.TotalDeduction)

	// A small instant withdrawal can be eaten entirely by the flat charge.
	// Refusing it here is better than paying out zero and still debiting the
	// wallet for the full amount.
	if preview.NetAmount <= 0 {

		return nil, fmt.Errorf(
			"amount is too small: ₹%.2f in charges would leave nothing to transfer",
			preview.TotalDeduction,
		)
	}

	//------------------------------------------------
	// Summary Card Lines
	//------------------------------------------------

	preview.Lines = []dto.WithdrawSummaryLineResponse{
		{
			Label: "Withdrawal amount",
			Value: preview.Amount,
			Kind:  "DEBIT",
		},
	}

	if preview.TDSAmount > 0 {

		preview.Lines = append(preview.Lines, dto.WithdrawSummaryLineResponse{
			Label: fmt.Sprintf("TDS (%.2f%%)", preview.TDSPercent),
			Value: preview.TDSAmount,
			Kind:  "DEBIT",
		})
	}

	// The flat charge is listed even at zero, because the withdraw screen
	// shows a "Processing fee ₹0" row rather than hiding it.
	if withdrawType == models.WithdrawTypeInstant {

		preview.Lines = append(preview.Lines, dto.WithdrawSummaryLineResponse{
			Label: "Instant transfer fee",
			Value: preview.InstantCharge,
			Kind:  "DEBIT",
		})

	} else {

		preview.Lines = append(preview.Lines, dto.WithdrawSummaryLineResponse{
			Label: "Processing fee",
			Value: 0,
			Kind:  "DEBIT",
		})
	}

	preview.Lines = append(preview.Lines, dto.WithdrawSummaryLineResponse{
		Label: "You receive",
		Value: preview.NetAmount,
		Kind:  "TOTAL",
	})

	return preview, nil
}

///////////////////////////////////////////////////////////
// Shared Validation
///////////////////////////////////////////////////////////

// withdrawContext is everything both the preview and the create path need to
// look up before they can say anything about a request.
type withdrawContext struct {
	Astrologer   *models.Astrologer
	Wallet       *models.UserWallet
	Balance      float64
	Settings     withdrawSettings
	WithdrawType string
	Payout       *withdrawPayout
}

// prepareWithdraw runs every check that is common to previewing and creating
// a withdrawal. Preview and create therefore cannot disagree about whether a
// request is allowed, which is what would let the app show a summary for a
// request the server then refuses.
func (s *walletService) prepareWithdraw(
	userID uint,
	request dto.WithdrawRequest,
) (*withdrawContext, error) {

	//------------------------------------------------
	// Normalise Input
	//------------------------------------------------

	withdrawType, err := normalizeWithdrawType(request.WithdrawType)
	if err != nil {
		return nil, err
	}

	method, err := normalizePaymentMethod(request.PaymentMethod)
	if err != nil {
		return nil, err
	}

	//------------------------------------------------
	// Astrologer
	//------------------------------------------------

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

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

	//------------------------------------------------
	// Wallet
	//------------------------------------------------

	wallet, err := s.repository.GetWallet(userID)
	if err != nil {
		return nil, err
	}

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

	balance := 0.0

	if wallet.Amount != nil {
		balance = *wallet.Amount
	}

	//------------------------------------------------
	// Amount
	//------------------------------------------------

	settings := s.withdrawSettings()

	amount := roundMoney(request.Amount)

	if amount <= 0 {
		return nil, errors.New("withdraw amount must be greater than zero")
	}

	minimum := settings.minFor(withdrawType)

	if amount < minimum {
		return nil, fmt.Errorf("minimum withdrawal amount is ₹%.0f", minimum)
	}

	if maximum := settings.maxFor(withdrawType); maximum > 0 && amount > maximum {
		return nil, fmt.Errorf("maximum instant withdrawal amount is ₹%.0f", maximum)
	}

	if balance <= 0 {
		return nil, errors.New("wallet balance is zero")
	}

	if amount > balance {
		return nil, errors.New("insufficient wallet balance")
	}

	//------------------------------------------------
	// Payout Target
	//------------------------------------------------

	bank, err := s.repository.GetBankAccountByAstrologerID(astrologer.ID)
	if err != nil {
		return nil, err
	}

	payout, err := resolvePayout(bank, method)
	if err != nil {
		return nil, err
	}

	return &withdrawContext{
		Astrologer:   astrologer,
		Wallet:       wallet,
		Balance:      balance,
		Settings:     settings,
		WithdrawType: withdrawType,
		Payout:       payout,
	}, nil
}

///////////////////////////////////////////////////////////
// Withdraw Config
///////////////////////////////////////////////////////////

// GetWithdrawConfig fills the withdraw screen before the astrologer types an
// amount: balance, limits, the fees in force and the saved payout methods.
//
// Unavailable methods and types are returned with a reason rather than
// omitted, so the app can render the tab disabled and say why.
func (s *walletService) GetWithdrawConfig(
	userID uint,
) (*dto.WithdrawConfigResponse, error) {

	//------------------------------------------------
	// Astrologer
	//------------------------------------------------

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

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

	//------------------------------------------------
	// Balance
	//------------------------------------------------

	balance := 0.0

	wallet, err := s.repository.GetWallet(userID)
	if err != nil {
		return nil, err
	}

	if wallet != nil && wallet.Amount != nil {
		balance = roundMoney(*wallet.Amount)
	}

	if balance < 0 {
		balance = 0
	}

	settings := s.withdrawSettings()

	//------------------------------------------------
	// Quick Amounts
	//------------------------------------------------
	//
	// A chip for more than the astrologer holds is only a dead button, so it
	// is dropped here rather than disabled in the app.

	quickAmounts := make([]float64, 0, len(withdrawQuickAmounts))

	for _, amount := range withdrawQuickAmounts {

		if amount <= balance && amount >= settings.MinAmount {
			quickAmounts = append(quickAmounts, amount)
		}
	}

	//------------------------------------------------
	// Payout Methods
	//------------------------------------------------

	bank, err := s.repository.GetBankAccountByAstrologerID(astrologer.ID)
	if err != nil {
		return nil, err
	}

	methods := buildPayoutMethods(bank)

	hasMethod := false

	for _, method := range methods {

		if method.IsAvailable {
			hasMethod = true
			break
		}
	}

	//------------------------------------------------
	// Pending General Request
	//------------------------------------------------
	//
	// Only a general request blocks another one. An instant withdrawal sits
	// at Processing until the payout provider closes it, and holding up every
	// later withdrawal on that would strand the astrologer.

	pending, err := s.repository.GetPendingWithdrawRequest(
		astrologer.ID,
		models.WithdrawTypeGeneral,
	)

	if err != nil {
		return nil, err
	}

	//------------------------------------------------
	// Response
	//------------------------------------------------

	response := &dto.WithdrawConfigResponse{
		AvailableBalance: balance,
		MinAmount:        settings.MinAmount,
		MaxAmount:        balance,
		QuickAmounts:     quickAmounts,
		Methods:          methods,
		Note:             withdrawCutoffNote,
	}

	if pending != nil {

		response.HasPendingRequest = true
		response.PendingRequestID = pending.ID
		response.PendingMessage = fmt.Sprintf(
			"You already have a withdrawal request of ₹%.2f awaiting approval.",
			pending.WithdrawAmount,
		)
	}

	//------------------------------------------------
	// Types
	//------------------------------------------------

	general := dto.WithdrawTypeOptionResponse{
		Type:        models.WithdrawTypeGeneral,
		Label:       "Standard withdrawal",
		Description: "Sent to admin for approval and transferred to your saved account once approved.",
		MinAmount:   settings.minFor(models.WithdrawTypeGeneral),
		MaxAmount:   settings.maxFor(models.WithdrawTypeGeneral),
		IsAvailable: true,
	}

	instant := dto.WithdrawTypeOptionResponse{
		Type:          models.WithdrawTypeInstant,
		Label:         "Instant withdrawal",
		Description:   "Transferred right away. TDS and a flat instant transfer fee are deducted.",
		MinAmount:     settings.minFor(models.WithdrawTypeInstant),
		MaxAmount:     settings.maxFor(models.WithdrawTypeInstant),
		TDSPercent:    settings.TDSPercent,
		InstantCharge: settings.InstantCharge,
		IsAvailable:   true,
	}

	switch {

	case !hasMethod:

		general.IsAvailable = false
		general.Reason = "Add your bank or UPI details to withdraw."

		instant.IsAvailable = false
		instant.Reason = general.Reason

	case balance < general.MinAmount:

		general.IsAvailable = false
		general.Reason = fmt.Sprintf("Minimum withdrawal amount is ₹%.0f.", general.MinAmount)

	case response.HasPendingRequest:

		general.IsAvailable = false
		general.Reason = response.PendingMessage
	}

	if instant.IsAvailable && balance < instant.MinAmount {

		instant.IsAvailable = false
		instant.Reason = fmt.Sprintf("Minimum instant withdrawal amount is ₹%.0f.", instant.MinAmount)
	}

	response.Types = []dto.WithdrawTypeOptionResponse{general, instant}

	return response, nil
}

// buildPayoutMethods renders the UPI and Bank transfer tabs from the saved
// account. Both tabs are always returned so the app's layout does not change
// with the data.
func buildPayoutMethods(
	bank *models.AstrologerBankAccount,
) []dto.WithdrawPayoutMethodResponse {

	upi := dto.WithdrawPayoutMethodResponse{
		Method:   models.PaymentMethodUPI,
		Label:    "UPI",
		Subtitle: "Instant · transferred to your UPI id",
	}

	account := dto.WithdrawPayoutMethodResponse{
		Method:   models.PaymentMethodBank,
		Label:    "Bank transfer",
		Subtitle: "Transferred to your saved bank account",
	}

	if bank == nil {

		upi.Reason = "No UPI id saved."
		account.Reason = "No bank account saved."

		return []dto.WithdrawPayoutMethodResponse{upi, account}
	}

	rejected := strings.EqualFold(bank.VerificationStatus, "REJECTED")

	upi.VerificationStatus = bank.VerificationStatus
	upi.AccountHolderName = bank.AccountHolderName

	account.VerificationStatus = bank.VerificationStatus
	account.AccountHolderName = bank.AccountHolderName
	account.BankName = bank.BankName

	//------------------------------------------------
	// UPI
	//------------------------------------------------

	if upiID := strings.TrimSpace(bank.UPIID); upiID != "" {

		upi.Title = upiID
		upi.MaskedRef = upiID
		upi.IsAvailable = !rejected
		upi.IsDefault = !rejected

		if rejected {
			upi.Reason = "Your saved details were rejected."
		}

	} else {

		upi.Reason = "No UPI id saved."
	}

	//------------------------------------------------
	// Bank
	//------------------------------------------------

	if number := strings.TrimSpace(bank.AccountNumber); number != "" && strings.TrimSpace(bank.IFSCCode) != "" {

		masked := maskAccountNumber(number)

		account.MaskedRef = masked
		account.IsAvailable = !rejected

		if bank.BankName != "" {
			account.Title = bank.BankName + " · " + masked
		} else {
			account.Title = masked
		}

		// Bank transfer is the default only when there is no UPI to prefer.
		account.IsDefault = account.IsAvailable && !upi.IsAvailable

		if rejected {
			account.Reason = "Your saved details were rejected."
		}

	} else {

		account.Reason = "No bank account saved."
	}

	return []dto.WithdrawPayoutMethodResponse{upi, account}
}

///////////////////////////////////////////////////////////
// Withdraw Preview
///////////////////////////////////////////////////////////

// PreviewWithdraw computes the Summary card without writing anything, so the
// astrologer sees the exact deductions before confirming. It runs the same
// validation as the create call, which is what stops the app from showing a
// summary for a request the server would then refuse.
func (s *walletService) PreviewWithdraw(
	userID uint,
	request dto.WithdrawRequest,
) (*dto.WithdrawPreviewResponse, error) {

	context, err := s.prepareWithdraw(userID, request)
	if err != nil {
		return nil, err
	}

	amount := roundMoney(request.Amount)

	return buildBreakdown(
		amount,
		context.WithdrawType,
		context.Payout,
		context.Settings,
		context.Balance-amount,
	)
}

///////////////////////////////////////////////////////////
// Withdraw
///////////////////////////////////////////////////////////

// Withdraw creates a withdrawal of either type.
//
// The gross amount leaves the wallet in both cases. For a general withdrawal
// that is a hold against an approval that has not happened yet; for an
// instant one it is the payout itself. The deductions are not a second debit
// on the wallet — they come out of the amount already withdrawn, which is why
// the ledger records the gross and the row records the net.
func (s *walletService) Withdraw(
	userID uint,
	request dto.WithdrawRequest,
) (*dto.WithdrawCreateResponse, error) {

	//------------------------------------------------
	// Validate
	//------------------------------------------------

	context, err := s.prepareWithdraw(userID, request)
	if err != nil {
		return nil, err
	}

	amount := roundMoney(request.Amount)

	//------------------------------------------------
	// One Open General Request at a Time
	//------------------------------------------------
	//
	// Checked only for general withdrawals. An instant one settles on its
	// own, so an open instant request is no reason to refuse the next
	// withdrawal.

	if context.WithdrawType == models.WithdrawTypeGeneral {

		pending, err := s.repository.GetPendingWithdrawRequest(
			context.Astrologer.ID,
			models.WithdrawTypeGeneral,
		)

		if err != nil {
			return nil, err
		}

		if pending != nil {
			return nil, errors.New("you already have a pending withdrawal request")
		}
	}

	//------------------------------------------------
	// Breakdown
	//------------------------------------------------

	remainingBalance := roundMoney(context.Balance - amount)

	breakdown, err := buildBreakdown(
		amount,
		context.WithdrawType,
		context.Payout,
		context.Settings,
		remainingBalance,
	)

	if err != nil {
		return nil, err
	}

	//------------------------------------------------
	// Status
	//------------------------------------------------
	//
	// A general request waits for an admin. An instant one is already on its
	// way, so it opens at Processing and is closed by the payout provider —
	// not integrated yet, which is why nothing here marks it Paid.

	status := models.WithdrawStatusPending
	remarks := "Withdraw request submitted for approval"

	if context.WithdrawType == models.WithdrawTypeInstant {
		status = models.WithdrawStatusProcessing
		remarks = "Instant withdrawal submitted for transfer"
	}

	//------------------------------------------------
	// Withdraw Row
	//------------------------------------------------

	withdraw := &models.WalletWithdrawRequest{
		AstrologerID:   context.Astrologer.ID,
		WithdrawAmount: amount,
		WithdrawType:   context.WithdrawType,
		Status:         status,
		Remarks:        remarks,

		PaymentMethod:     context.Payout.PaymentMethod,
		UPIID:             context.Payout.UPIID,
		AccountNumber:     context.Payout.AccountNumber,
		IFSCCode:          context.Payout.IFSCCode,
		AccountHolderName: context.Payout.AccountHolderName,
		BankAccountID:     context.Payout.BankAccountID,
		BankName:          context.Payout.BankName,

		TDSPercent:    breakdown.TDSPercent,
		TDSAmount:     breakdown.TDSAmount,
		InstantCharge: breakdown.InstantCharge,
		NetAmount:     breakdown.NetAmount,

		IsActive: true,
		IsDelete: false,
	}

	//------------------------------------------------
	// Ledger Entry
	//------------------------------------------------
	//
	// The gross amount, because that is what left the wallet. The net is on
	// the withdraw row for the statement to show.

	ledger := &models.WalletLedger{
		UserID:          userID,
		AstrologerID:    context.Astrologer.ID,
		TransactionType: "WITHDRAW",
		Credit:          0,
		Debit:           amount,
		BalanceAfter:    remainingBalance,
		Remarks:         remarks,
	}

	//------------------------------------------------
	// Settlement
	//------------------------------------------------
	//
	// Opened up front for an instant withdrawal so the payout integration has
	// a row to close, carrying the net amount that is actually transferred.

	var settlement *models.WalletSettlement

	if context.WithdrawType == models.WithdrawTypeInstant {

		settlement = &models.WalletSettlement{
			UserID:       userID,
			AstrologerID: context.Astrologer.ID,

			// Money leaving the wallet, not the consultation batches coming
			// into it. Without the type these rows would show up in the
			// app's settlement history alongside the batches.
			SettlementType:   constants.SettlementTypeWithdrawPayout,
			Amount:           breakdown.NetAmount,
			SettlementStatus: models.WithdrawStatusProcessing,
			PaymentMode:      context.Payout.PaymentMethod,
			Remarks:          "Instant withdrawal awaiting payout transfer",
		}
	}

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

	if err := s.repository.CreateWithdrawWithLedger(
		withdraw,
		ledger,
		settlement,
		context.Wallet.ID,
		amount,
		userID,
	); err != nil {
		return nil, err
	}

	//------------------------------------------------
	// Response
	//------------------------------------------------

	message := "Withdrawal request submitted successfully."

	if context.WithdrawType == models.WithdrawTypeInstant {
		message = fmt.Sprintf(
			"Instant withdrawal of ₹%.2f is being transferred to %s.",
			breakdown.NetAmount,
			context.Payout.Display,
		)
	}

	return &dto.WithdrawCreateResponse{
		Withdraw:         mapWithdrawResponse(*withdraw),
		Breakdown:        *breakdown,
		AvailableBalance: remainingBalance,
		Message:          message,
	}, nil
}

///////////////////////////////////////////////////////////
// Withdraw History
///////////////////////////////////////////////////////////

// GetWithdrawHistory backs the Withdrawals tab of the Statement screen.
func (s *walletService) GetWithdrawHistory(
	userID uint,
) ([]dto.WithdrawResponse, error) {

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

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

	history, err := s.repository.GetWithdrawHistory(astrologer.ID)
	if err != nil {
		return nil, err
	}

	response := make([]dto.WithdrawResponse, 0, len(history))

	for _, item := range history {
		response = append(response, mapWithdrawResponse(item))
	}

	return response, nil
}

// mapWithdrawResponse renders one stored request as a statement row.
func mapWithdrawResponse(
	item models.WalletWithdrawRequest,
) dto.WithdrawResponse {

	withdrawType := item.WithdrawType

	// Rows written before the two-type split carry no type.
	if withdrawType == "" {
		withdrawType = models.WithdrawTypeGeneral
	}

	// The same fallback for rows written before netAmount existed: nothing
	// was deducted from them, so the gross was the net.
	netAmount := item.NetAmount

	if netAmount == 0 {
		netAmount = item.WithdrawAmount
	}

	response := dto.WithdrawResponse{
		ID:            item.ID,
		RequestNo:     item.RequestNo,
		Amount:        item.WithdrawAmount,
		WithdrawType:  withdrawType,
		Status:        item.Status,
		StatusLabel:   withdrawStatusLabel(item.Status),
		PaymentMethod: item.PaymentMethod,
		TDSPercent:    item.TDSPercent,
		TDSAmount:     item.TDSAmount,
		InstantCharge: item.InstantCharge,
		NetAmount:     netAmount,
		BankName:      item.BankName,
		Remarks:       item.Remarks,
		IsInstant:     withdrawType == models.WithdrawTypeInstant,
	}

	//------------------------------------------------
	// Payout Target
	//------------------------------------------------

	if item.PaymentMethod == models.PaymentMethodUPI {

		response.PayoutTo = item.UPIID

	} else if masked := maskAccountNumber(item.AccountNumber); masked != "" {

		if item.BankName != "" {
			response.PayoutTo = item.BankName + " · " + masked
		} else {
			response.PayoutTo = masked
		}
	}

	//------------------------------------------------
	// Dates
	//------------------------------------------------

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

	if item.ProcessedAt != nil {
		response.ProcessedDate = item.ProcessedAt.Format("02 Jan 2006 03:04 PM")
	}

	return response
}

// withdrawStatusLabel is the badge text on the statement rows — "Paid",
// "Processing", "Pending".
func withdrawStatusLabel(status string) string {

	switch strings.ToUpper(strings.TrimSpace(status)) {

	case strings.ToUpper(models.WithdrawStatusPending):
		return "Pending"

	case strings.ToUpper(models.WithdrawStatusProcessing):
		return "Processing"

	case strings.ToUpper(models.WithdrawStatusApproved):
		return "Approved"

	case strings.ToUpper(models.WithdrawStatusPaid):
		return "Paid"

	case strings.ToUpper(models.WithdrawStatusRejected):
		return "Rejected"

	case strings.ToUpper(models.WithdrawStatusFailed):
		return "Failed"
	}

	return status
}
