package repositories_astrologer

import (
	"astrology-api/constants"
	models "astrology-api/models/astrologermodel"
	usermodel "astrology-api/models/usermodel"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
)

type walletRepository struct {
	db *gorm.DB
}

func NewWalletRepository(db *gorm.DB) WalletRepository {

	if db == nil {
		panic("WalletRepository: database connection is nil")
	}

	return &walletRepository{
		db: db,
	}
}

func (r *walletRepository) GetAstrologerByUserID(userID uint) (*models.Astrologer, error) {

	var astrologer models.Astrologer

	err := r.db.
		Where("userId = ?", userID).
		Where("isDelete = ?", false).
		Where("isActive = ?", true).
		First(&astrologer).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, errors.New("astrologer not found")
		}

		return nil, err
	}

	return &astrologer, nil
}

func (r *walletRepository) GetWallet(userID uint) (*models.UserWallet, error) {

	var wallet models.UserWallet

	err := r.db.
		Where("userId = ?", userID).
		Where("isDelete = ?", false).
		Where("isActive = ?", true).
		First(&wallet).Error

	if err != nil {

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

		return nil, err
	}

	return &wallet, nil
}

func (r *walletRepository) CreateWallet(wallet *models.UserWallet) error {

	return r.db.Transaction(func(tx *gorm.DB) error {

		if err := tx.Create(wallet).Error; err != nil {
			return err
		}

		return nil
	})
}

func (r *walletRepository) UpdateWallet(wallet *models.UserWallet) error {

	return r.db.Transaction(func(tx *gorm.DB) error {

		result := tx.
			Model(&models.UserWallet{}).
			Where("id = ?", wallet.ID).
			Updates(map[string]interface{}{
				"amount":     wallet.Amount,
				"modifiedBy": wallet.ModifiedBy,
				"updated_at": gorm.Expr("NOW()"),
			})

		if result.Error != nil {
			return result.Error
		}

		if result.RowsAffected == 0 {
			return errors.New("wallet not found")
		}

		return nil
	})
}

func (r *walletRepository) GetAvailableBalance(userID uint) (float64, error) {

	var wallet models.UserWallet

	err := r.db.
		Where("userId = ?", userID).
		Where("isActive = ?", true).
		Where("isDelete = ?", false).
		First(&wallet).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, nil
		}

		return 0, err
	}

	if wallet.Amount == nil {
		return 0, nil
	}

	return *wallet.Amount, nil
}

func (r *walletRepository) GetLastThreeMonthEarnings(astrologerID uint) (float64, error) {

	var total float64

	fromDate := time.Now().AddDate(0, -3, 0)

	err := r.db.
		Model(&models.WalletLedger{}).
		Select("COALESCE(SUM(credit),0)").
		Where("astrologer_id = ?", astrologerID).
		Where("created_at >= ?", fromDate).
		Scan(&total).Error

	if err != nil {
		return 0, err
	}

	return total, nil
}

func (r *walletRepository) GetMonthlyEarnings(astrologerID uint) (float64, error) {

	var total float64

	now := time.Now()

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

	err := r.db.
		Model(&models.WalletLedger{}).
		Select("COALESCE(SUM(credit),0)").
		Where("astrologer_id = ?", astrologerID).
		Where("created_at >= ?", firstDay).
		Scan(&total).Error

	if err != nil {
		return 0, err
	}

	return total, nil
}

func (r *walletRepository) GetWeeklyEarnings(astrologerID uint) (float64, error) {

	var total float64

	fromDate := time.Now().AddDate(0, 0, -7)

	err := r.db.
		Model(&models.WalletLedger{}).
		Select("COALESCE(SUM(credit),0)").
		Where("astrologer_id = ?", astrologerID).
		Where("created_at >= ?", fromDate).
		Scan(&total).Error

	if err != nil {
		return 0, err
	}

	return total, nil
}

// GetPendingEarnings is money the astrologer has earned that has not reached
// the wallet yet: consultations billed to a customer and sitting at PENDING or
// TO_BE_SETTLED.
//
// This used to be computed as ledger credits minus completed settlements,
// which no longer describes anything: the settlement job writes *both* a
// ledger credit and a settlement row for the same money, so the two cancelled
// out and every settled batch was double-counted out of the total. The
// consultations table is now the source of truth for what is owed.
func (r *walletRepository) GetPendingEarnings(astrologerID uint) (float64, error) {

	var pending float64

	err := r.db.
		Model(&models.Consultation{}).
		Select("COALESCE(SUM(astrologerEarning),0)").
		Where("astrologerId = ?", astrologerID).
		Where("settlementStatus IN ?", []string{
			constants.SettlementStatusPending,
			constants.SettlementStatusToBeSettled,
		}).
		Where("isDelete = ?", false).
		Scan(&pending).Error

	if err != nil {
		return 0, err
	}

	if pending < 0 {
		return 0, nil
	}

	return pending, nil
}

func (r *walletRepository) GetTodayAstromallEarnings(astrologerID uint) (float64, error) {

	var total float64

	today := time.Now()

	start := time.Date(
		today.Year(),
		today.Month(),
		today.Day(),
		0,
		0,
		0,
		0,
		today.Location(),
	)

	end := start.Add(24 * time.Hour)

	err := r.db.
		Model(&models.AstromallOrder{}).
		Select("COALESCE(SUM(astrologer_earning),0)").
		Where("astrologer_id = ?", astrologerID).
		Where("created_at >= ?", start).
		Where("created_at < ?", end).
		Scan(&total).Error

	if err != nil {
		return 0, err
	}

	return total, nil
}

func (r *walletRepository) GetPayableAmount(astrologerID uint) (float64, error) {

	var wallet models.UserWallet

	err := r.db.
		Where("userId = ?", astrologerID).
		First(&wallet).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, nil
		}

		return 0, err
	}

	var pendingWithdraw float64

	err = r.db.
		Model(&models.WalletWithdrawRequest{}).
		Select("COALESCE(SUM(withdrawAmount),0)").
		Where("astrologerId = ?", astrologerID).
		Where("status IN ?", []string{
			"Pending",
			"Processing",
			"Approved",
		}).
		Scan(&pendingWithdraw).Error

	if err != nil {
		return 0, err
	}

	var balance float64

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

	payable := balance - pendingWithdraw

	if payable < 0 {
		payable = 0
	}

	return payable, nil
}

func (r *walletRepository) GetRank(astrologerID uint) (int, error) {

	type Ranking struct {
		AstrologerID uint
		Earning      float64
	}

	var rankings []Ranking

	err := r.db.
		Model(&models.WalletLedger{}).
		Select(`
			astrologer_id,
			COALESCE(SUM(credit),0) AS earning
		`).
		Group("astrologer_id").
		Order("earning DESC").
		Scan(&rankings).Error

	if err != nil {
		return 0, err
	}

	for index, item := range rankings {

		if item.AstrologerID == astrologerID {
			return index + 1, nil
		}
	}

	return 0, nil
}


func (r *walletRepository) GetTransactionByID(id uint, astrologer_id uint) (*models.WalletLedger, error) {

	var transaction models.WalletLedger

	err := r.db.
		Where("id = ?", id).
		Where("astrologer_id = ?", astrologer_id).
		First(&transaction).Error

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

	return &transaction, nil
}

func (r *walletRepository) CreateTransaction(transaction *models.WalletLedger) error {

	return r.db.Transaction(func(tx *gorm.DB) error {

		if err := tx.Create(transaction).Error; err != nil {
			return err
		}

		var wallet models.UserWallet

		err := tx.
			Where("userId = ?", transaction.UserID).
			First(&wallet).Error

		if err != nil {
			return err
		}

		balance := 0.0

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

		balance += transaction.Credit
		balance -= transaction.Debit

		wallet.Amount = &balance

		if err := tx.Save(&wallet).Error; err != nil {
			return err
		}

		transaction.BalanceAfter = balance

		if err := tx.Model(&models.WalletLedger{}).
			Where("id = ?", transaction.ID).
			Update("balance_after", balance).Error; err != nil {
			return err
		}

		return nil
	})
}

func (r *walletRepository) GetWalletLedger(astrologerID uint) ([]models.WalletLedger, error) {

	var ledger []models.WalletLedger

	err := r.db.
		Where("astrologer_id = ?", astrologerID).
		Order("created_at DESC").
		Find(&ledger).Error

	if err != nil {
		return nil, err
	}

	return ledger, nil
}

func (r *walletRepository) CreateLedger(ledger *models.WalletLedger) error {

	return r.db.Transaction(func(tx *gorm.DB) error {

		if err := tx.Create(ledger).Error; err != nil {
			return err
		}

		return nil
	})
}

func (r *walletRepository) CreateWithdrawRequest(req *models.WalletWithdrawRequest) error {

	return r.db.Transaction(func(tx *gorm.DB) error {

		if err := tx.Create(req).Error; err != nil {
			return err
		}

		return nil
	})
}

func (r *walletRepository) UpdateWithdrawRequest(req *models.WalletWithdrawRequest) error {

	return r.db.Transaction(func(tx *gorm.DB) error {

		result := tx.
			Model(&models.WalletWithdrawRequest{}).
			Where("id = ?", req.ID).
			Updates(map[string]interface{}{
				"status":            req.Status,
				"paymentMethod":     req.PaymentMethod,
				"upiId":             req.UPIID,
				"accountNumber":     req.AccountNumber,
				"ifscCode":          req.IFSCCode,
				"accountHolderName": req.AccountHolderName,
				"updated_at":        gorm.Expr("NOW()"),
			})

		if result.Error != nil {
			return result.Error
		}

		if result.RowsAffected == 0 {
			return errors.New("withdraw request not found")
		}

		return nil
	})
}

func (r *walletRepository) GetWithdrawRequest(id uint) (*models.WalletWithdrawRequest, error) {

	var request models.WalletWithdrawRequest

	err := r.db.
		Where("id = ?", id).
		First(&request).Error

	if err != nil {

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

		return nil, err
	}

	return &request, nil
}

func (r *walletRepository) GetWithdrawHistory(astrologerID uint) ([]models.WalletWithdrawRequest, error) {

	var history []models.WalletWithdrawRequest

	err := r.db.
		Where("astrologerId = ?", astrologerID).
		Order("created_at DESC").
		Find(&history).Error

	if err != nil {
		return nil, err
	}

	return history, nil
}

func (r *walletRepository) GetSettlementHistory(astrologerID uint) ([]models.WalletSettlement, error) {

	var settlements []models.WalletSettlement

	err := r.db.
		Where("astrologer_id = ?", astrologerID).
		Where("settlement_type = ?", constants.SettlementTypeConsultationBatch).
		Order("created_at DESC").
		Find(&settlements).Error

	if err != nil {
		return nil, err
	}

	return settlements, nil
}

func (r *walletRepository) GetSettlementBatches(
	astrologerID uint,
	page int,
	limit int,
) ([]models.WalletSettlement, int64, float64, error) {

	build := func() *gorm.DB {

		return r.db.
			Model(&models.WalletSettlement{}).
			Where("astrologer_id = ?", astrologerID).
			Where("settlement_type = ?", constants.SettlementTypeConsultationBatch)
	}

	var total int64

	if err := build().Count(&total).Error; err != nil {
		return nil, 0, 0, err
	}

	var totalSettled float64

	err := build().
		Where("settlement_status = ?", constants.BatchStatusCompleted).
		Select("COALESCE(SUM(amount),0)").
		Scan(&totalSettled).Error

	if err != nil {
		return nil, 0, 0, err
	}

	if page <= 0 {
		page = 1
	}

	if limit <= 0 {
		limit = 10
	}

	var settlements []models.WalletSettlement

	err = build().
		Order("id DESC").
		Offset((page - 1) * limit).
		Limit(limit).
		Find(&settlements).Error

	if err != nil {
		return nil, 0, 0, err
	}

	return settlements, total, totalSettled, nil
}

func (r *walletRepository) GetSettlementBatch(
	astrologerID uint,
	settlementID uint,
) (*models.WalletSettlement, error) {

	var settlement models.WalletSettlement

	err := r.db.
		Where("id = ?", settlementID).
		Where("astrologer_id = ?", astrologerID).
		First(&settlement).Error

	if err != nil {

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

		return nil, err
	}

	return &settlement, nil
}

func (r *walletRepository) GetSettlementConsultations(
	astrologerID uint,
	settlementID uint,
) ([]models.Consultation, error) {

	var records []models.Consultation

	err := r.db.
		Where("astrologerId = ?", astrologerID).
		Where("settlementId = ?", settlementID).
		Where("isDelete = ?", false).
		Order("id ASC").
		Find(&records).Error

	if err != nil {
		return nil, err
	}

	return records, nil
}

func (r *walletRepository) GetUnsettledConsultations(
	astrologerID uint,
) ([]models.Consultation, error) {

	var records []models.Consultation

	err := r.db.
		Where("astrologerId = ?", astrologerID).
		Where("settlementStatus IN ?", []string{
			constants.SettlementStatusPending,
			constants.SettlementStatusToBeSettled,
			constants.SettlementStatusOnHold,
		}).
		Where("isDelete = ?", false).
		Order("id DESC").
		Find(&records).Error

	if err != nil {
		return nil, err
	}

	return records, nil
}

func (r *walletRepository) CreateSettlement(settlement *models.WalletSettlement) error {

	return r.db.Transaction(func(tx *gorm.DB) error {

		if err := tx.Create(settlement).Error; err != nil {
			return err
		}

		return nil
	})
}

// GetPendingWithdrawRequest finds the newest request still awaiting money
// movement. An empty withdrawType matches either type.
func (r *walletRepository) GetPendingWithdrawRequest(
	astrologerID uint,
	withdrawType string,
) (*models.WalletWithdrawRequest, error) {

	var request models.WalletWithdrawRequest

	query := r.db.
		Where("astrologerId = ?", astrologerID).
		Where("status IN ?", []string{
			models.WithdrawStatusPending,
			models.WithdrawStatusProcessing,
		})

	if withdrawType != "" {
		query = query.Where("withdrawType = ?", withdrawType)
	}

	err := query.
		Order("created_at DESC").
		First(&request).Error

	if err != nil {

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

		return nil, err
	}

	return &request, nil
}

// CreateWithdrawWithLedger writes the request, its ledger entry, the optional
// settlement row and the debited wallet balance together. Doing them in one
// transaction is what stops a failure halfway through leaving a request that
// was never funded, or a wallet that was debited for a request that does not
// exist.
func (r *walletRepository) CreateWithdrawWithLedger(
	withdraw *models.WalletWithdrawRequest,
	ledger *models.WalletLedger,
	settlement *models.WalletSettlement,
	walletID uint,
	debit float64,
	modifiedBy uint,
) error {

	return r.db.Transaction(func(tx *gorm.DB) error {

		//------------------------------------------------
		// Debit the Wallet First
		//------------------------------------------------
		//
		// The balance check is part of the UPDATE rather than a read followed
		// by a write, so two requests submitted at the same time cannot both
		// pass a check against the same balance and overdraw the wallet. The
		// second one matches no row and is rejected here.

		result := tx.
			Model(&models.UserWallet{}).
			Where("id = ?", walletID).
			Where("amount >= ?", debit).
			Updates(map[string]interface{}{
				"amount":     gorm.Expr("amount - ?", debit),
				"modifiedBy": modifiedBy,
				"updated_at": gorm.Expr("NOW()"),
			})

		if result.Error != nil {
			return result.Error
		}

		if result.RowsAffected == 0 {
			return errors.New("insufficient wallet balance")
		}

		//------------------------------------------------
		// Withdraw Request
		//------------------------------------------------

		if err := tx.Create(withdraw).Error; err != nil {
			return err
		}

		//------------------------------------------------
		// Request Number
		//------------------------------------------------
		//
		// Derived from the row id, so it is only available after the insert.

		if withdraw.RequestNo == "" {

			withdraw.RequestNo = fmt.Sprintf("WDR%08d", withdraw.ID)

			if err := tx.
				Model(&models.WalletWithdrawRequest{}).
				Where("id = ?", withdraw.ID).
				Update("requestNo", withdraw.RequestNo).Error; err != nil {

				return err
			}
		}

		//------------------------------------------------
		// Ledger Entry
		//------------------------------------------------

		if ledger != nil {

			ledger.ReferenceID = withdraw.ID

			if err := tx.Create(ledger).Error; err != nil {
				return err
			}
		}

		//------------------------------------------------
		// Settlement
		//------------------------------------------------
		//
		// Instant withdrawals open a settlement immediately, because the
		// payout is meant to happen now. A general withdrawal gets one only
		// when an admin approves it.

		if settlement != nil {

			settlement.WithdrawRequestID = withdraw.ID

			if settlement.SettlementNo == "" {
				settlement.SettlementNo = fmt.Sprintf("STL%08d", withdraw.ID)
			}

			if err := tx.Create(settlement).Error; err != nil {
				return err
			}
		}

		return nil
	})
}

func (r *walletRepository) GetBankAccountByAstrologerID(
	astrologerID uint,
) (*models.AstrologerBankAccount, error) {

	var bank models.AstrologerBankAccount

	err := r.db.
		Where("astrologer_id = ?", astrologerID).
		Order("id DESC").
		First(&bank).Error

	if err != nil {

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

		return nil, err
	}

	return &bank, nil
}

// GetSystemFlagFloat reads a numeric flag out of systemflag. A missing row,
// an inactive row or a value that will not parse all fall back to the caller's
// default: the withdraw screen has to work on a server where the
// configuration migration has not been applied yet, and failing the whole
// request over a missing rate row would be worse than using the default.
func (r *walletRepository) GetSystemFlagFloat(
	name string,
	fallback float64,
) float64 {

	var flag usermodel.SystemFlag

	err := r.db.
		Where("name = ?", name).
		Where("isDelete = ?", false).
		First(&flag).Error

	if err != nil {
		return fallback
	}

	value, convErr := strconv.ParseFloat(strings.TrimSpace(flag.Value), 64)

	if convErr != nil || value < 0 {
		return fallback
	}

	return value
}

// GetSystemFlagString reads a textual systemflag — the settlement frequency
// and run time, which are not numbers. A missing or blank row yields the
// fallback.
func (r *walletRepository) GetSystemFlagString(
	name string,
	fallback string,
) string {

	var flag usermodel.SystemFlag

	err := r.db.
		Where("name = ?", name).
		Where("isDelete = ?", false).
		First(&flag).Error

	if err != nil {
		return fallback
	}

	value := strings.TrimSpace(flag.Value)

	if value == "" {
		return fallback
	}

	return value
}

// ListConsultationEarnings is every billed session for this astrologer, at any
// settlement status, newest first.
//
// Deliberately not restricted to unsettled rows the way GetUnsettledConsultations
// is: this backs the screen an astrologer opens to see what they have earned,
// which has to keep showing a session after it has been paid.
//
// The totals are computed across the whole filtered set rather than the page,
// because a per-page sum is a number nobody can use.
func (r *walletRepository) ListConsultationEarnings(
	astrologerID uint,
	status string,
	medium string,
	page int,
	limit int,
) ([]models.Consultation, int64, map[string]float64, error) {

	if page <= 0 {
		page = 1
	}

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

	build := func() *gorm.DB {

		query := r.db.
			Model(&models.Consultation{}).
			Where("astrologerId = ?", astrologerID).
			Where("isDelete = ?", false).
			// Sessions that were never billed — rejected, missed, cancelled,
			// or too short to charge — have no money attached and would be
			// noise on an earnings screen.
			Where("settlementStatus <> ?", constants.SettlementStatusNA)

		if status = strings.ToUpper(strings.TrimSpace(status)); status != "" && status != "ALL" {
			query = query.Where("settlementStatus = ?", status)
		}

		if medium = strings.ToUpper(strings.TrimSpace(medium)); medium != "" && medium != "ALL" {
			query = query.Where("medium = ?", medium)
		}

		return query
	}

	var total int64

	if err := build().Count(&total).Error; err != nil {
		return nil, 0, nil, err
	}

	//------------------------------------------------
	// Totals by status
	//------------------------------------------------

	type bucket struct {
		SettlementStatus string  `gorm:"column:settlementStatus"`
		Amount           float64 `gorm:"column:amount"`
	}

	var buckets []bucket

	err := build().
		Select("settlementStatus, COALESCE(SUM(astrologerEarning),0) AS amount").
		Group("settlementStatus").
		Scan(&buckets).Error

	if err != nil {
		return nil, 0, nil, err
	}

	totals := map[string]float64{}

	for _, item := range buckets {
		totals[item.SettlementStatus] = item.Amount
	}

	//------------------------------------------------
	// Page
	//------------------------------------------------

	var records []models.Consultation

	err = build().
		Order("id DESC").
		Offset((page - 1) * limit).
		Limit(limit).
		Find(&records).Error

	if err != nil {
		return nil, 0, nil, err
	}

	return records, total, totals, nil
}
