package services

import (
	"errors"
	"fmt"
	"log"
	"strings"
	"time"

	configs "astrology-api/configs"
	usermodel "astrology-api/models/usermodel"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

//////////////////////////////////////////////////////////////
// Astrology Service Pricing
//////////////////////////////////////////////////////////////
//
// Section 3 of astrology-admin/docs/api-changes/astrology-services-api-changes.txt:
// the admin's access_mode, price, free_quota and quota_period decide what a
// customer pays for each astrology service.
//
//   FREE        nothing
//   FREE_QUOTA  free for the first free_quota successful calls per
//               quota_period (DAY / MONTH / LIFETIME, server time), then price
//   PAID        price every time
//
// middleware.TrackAstrologyService applies it:
//
//   1. before the handler  quote the price; a wallet that cannot cover it gets
//                          HTTP 402 and the recharge message, and the provider
//                          is never called
//   2. after a success     debit the wallet and write a wallettransaction row,
//                          in one transaction with the wallet row locked
//   3. after a failure     nothing is debited - a failed call is never billed
//
// While the catalogue is readable it is the only price: the systemflag prices
// (HoroscopePrice, KundaliMatchingPrice, ...) that astrologyBase charged are
// switched off, so a call is never charged twice. If the catalogue cannot be
// read, the old systemflag charging comes back on. A service that has no
// catalogue row is served free.
//
// Astrologers and guests are never charged, and neither is a public route
// (requires_login = 0); the admin panel refuses to price one anyway.

// AstroPriceContextKey is set on the gin context, before the handler runs, to
// the amount this call will cost. Only the horoscope controller reads it, so
// its own horoscope_requests log can record the charge.
const AstroPriceContextKey = "astro_service_price"

// InsufficientBalanceError is returned when the wallet cannot cover the price.
type InsufficientBalanceError struct {
	Required float64
	Balance  float64
}

func (e *InsufficientBalanceError) Error() string {
	return fmt.Sprintf("insufficient wallet balance, %.2f required", e.Required)
}

// CatalogueGovernsPricing reports whether the admin catalogue is readable, in
// which case it is the only source of astrology prices.
func CatalogueGovernsPricing() bool {

	catalogue.get("")

	catalogue.mu.RLock()
	defer catalogue.mu.RUnlock()

	return len(catalogue.byCode) > 0
}

// QuoteAstrologyService returns what this customer pays for one call now.
// Anything it cannot work out is free: charging on a guess is worse than not
// charging.
func QuoteAstrologyService(row usermodel.AstrologyService, userID uint) float64 {

	if userID == 0 || !row.RequiresLogin || row.Price <= 0 {
		return 0
	}

	switch strings.ToUpper(row.AccessMode) {

	case "PAID":
		return row.Price

	case "FREE_QUOTA":

		used, err := freeCallsUsed(userID, row.Code, row.QuotaPeriod)

		if err != nil {
			log.Printf("astrology pricing: quota lookup failed for %s, serving free: %v", row.Code, err)
			return 0
		}

		if used < int64(row.FreeQuota) {
			return 0
		}

		return row.Price
	}

	return 0
}

// freeCallsUsed counts this customer's free, successful calls in the current
// quota window. Only was_free rows count, so paying calls never use up a
// quota, and a failed call gives its free use back.
func freeCallsUsed(userID uint, code string, period string) (int64, error) {

	query := configs.DB.
		Model(&usermodel.AstrologyServiceUsage{}).
		Where("user_id = ? AND service_code = ? AND was_free = 1 AND succeeded = 1", userID, code)

	if start, bounded := quotaWindowStart(period, time.Now()); bounded {
		query = query.Where("created_at >= ?", start)
	}

	var used int64

	err := query.Count(&used).Error

	return used, err
}

// quotaWindowStart is the start of the current DAY or calendar MONTH in server
// time; LIFETIME has no lower bound.
func quotaWindowStart(period string, now time.Time) (time.Time, bool) {

	switch strings.ToUpper(period) {
	case "DAY":
		return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()), true
	case "LIFETIME":
		return time.Time{}, false
	}

	// MONTH, and the default for anything unrecognised.
	return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()), true
}

// CustomerWalletBalance is the customer's wallet balance, 0 without a wallet.
func CustomerWalletBalance(userID uint) (float64, error) {

	var wallet usermodel.UserWallet

	err := configs.DB.
		Where("userId = ? AND isDelete = ?", userID, false).
		Order("id ASC").
		First(&wallet).Error

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

	if err != nil {
		return 0, err
	}

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

	return *wallet.Amount, nil
}

// ChargeAstrologyService debits the customer's wallet and records the
// wallettransaction row, the same shape every other astrology debit uses. The
// wallet row is locked for the duration, so two calls at once cannot both
// spend the same balance. It returns the balance left.
func ChargeAstrologyService(userID uint, amount float64, row usermodel.AstrologyService) (float64, error) {

	var remaining float64

	err := configs.DB.Transaction(func(tx *gorm.DB) error {

		var wallet usermodel.UserWallet

		err := tx.
			Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("userId = ? AND isDelete = ?", userID, false).
			Order("id ASC").
			First(&wallet).Error

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return &InsufficientBalanceError{Required: amount, Balance: 0}
		}

		if err != nil {
			return err
		}

		balance := float64(0)

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

		if balance < amount {
			return &InsufficientBalanceError{Required: amount, Balance: balance}
		}

		remaining = balance - amount

		if err := tx.
			Model(&usermodel.UserWallet{}).
			Where("id = ?", wallet.ID).
			Updates(map[string]interface{}{"amount": remaining, "updated_at": time.Now()}).Error; err != nil {
			return err
		}

		now := time.Now()

		return tx.Create(&usermodel.WalletTransaction{
			UserID:          userID,
			Amount:          amount,
			IsCredit:        false,
			TransactionType: astrologyTxnType(row.Family),
			CreatedBy:       &userID,
			ModifiedBy:      &userID,
			CreatedAt:       now,
			UpdatedAt:       now,
		}).Error
	})

	return remaining, err
}

// astrologyTxnType is wallettransaction.transactionType for a service family,
// reusing the values the astrology debits have always written.
func astrologyTxnType(family string) string {

	switch strings.ToUpper(family) {
	case "KUNDALI":
		return "KundliView"
	case "MATCHING":
		return TxnKundaliMatching
	case "HOROSCOPE":
		return TxnHoroscope
	case "PANCHANG":
		return TxnPanchang
	case "NUMEROLOGY":
		return TxnNumerology
	case "TAROT":
		return "TarotView"
	case "DOSHA":
		return "DoshaView"
	case "REPORT":
		return "ReportView"
	}

	return "AstrologyService"
}
