package payments

import (
	"context"
	"log"
	"log/slog"
	"math"
	"time"

	"crussell/db"
	"crussell/clock"
	"crussell/internal/square"

	"github.com/jackc/pgx/v5"
)

type RefundCalculationResult struct {
	TotalPrePaid          float64 `json:"total_pre_paid"`
	ProtectedDeposit      float64 `json:"protected_deposit"`
	RefundableAmount      float64 `json:"refundable_amount"`
	KeptAmount            float64 `json:"kept_amount"`
	HoursUntilAppointment float64 `json:"hours_until_appointment"`
	Tier                  string  `json:"tier"`
}

// CalculateRefundForCancellation computes the refund amounts for a cancelled booking.
//
// Track A (universal — same rules for all bookings):
//   - >72 hours notice:        Full refund of all pre-payments
//   - 24-72 hours notice:      Keep protected deposit (up to 50%), refund the rest
//   - <24 hours or no-show:    Keep all pre-payments
//
// The "protected deposit" is defined as min(totalPrePaid, subtotal * 0.50).
// This means up to 50% of the subtotal is always treated as a deposit for
// refund purposes, regardless of whether deposit_required was set on the booking.
func CalculateRefundForCancellation(
	subtotal float64,
	totalPrePaid float64,
	cancellationTime time.Time,
	startTime time.Time,
) RefundCalculationResult {
	hoursUntilAppointment := startTime.Sub(cancellationTime).Hours()

	protectedDeposit := math.Min(totalPrePaid, subtotal*ProtectedDepositMaxPct)

	// Round to 2 decimal places
	protectedDeposit = math.Round(protectedDeposit*100) / 100
	totalPrePaidRounded := math.Round(totalPrePaid*100) / 100

	var refundableAmount, keptAmount float64
	var tier string

	fullHrs := FullRefundThreshold.Hours()
	partHrs := PartialRefundThreshold.Hours()

	switch {
	case hoursUntilAppointment > fullHrs:
		refundableAmount = totalPrePaidRounded
		keptAmount = 0
		tier = FullRefundTier

	case hoursUntilAppointment >= partHrs:
		keptAmount = protectedDeposit
		refundableAmount = totalPrePaidRounded - keptAmount
		if refundableAmount < 0 {
			refundableAmount = 0
		}
		tier = PartialRefundTier

	default:
		keptAmount = totalPrePaidRounded
		refundableAmount = 0
		tier = NoRefundTier
	}

	return RefundCalculationResult{
		TotalPrePaid:          totalPrePaidRounded,
		ProtectedDeposit:      protectedDeposit,
		RefundableAmount:      refundableAmount,
		KeptAmount:            keptAmount,
		HoursUntilAppointment: hoursUntilAppointment,
		Tier:                  tier,
	}
}

// ProcessCancellationRefund calculates and records refunds for a cancelled booking.
// It processes refunds against completed payments on the booking up to the
// calculated refundable amount, creating refund records in the database.
// Returns the refund calculation and whether any refunds were processed.
// ProcessCancellationRefundTx is like ProcessCancellationRefund but uses an
// externally-provided transaction. The caller owns the transaction lifecycle
// (commit/rollback). Pass a non-nil pgx.Tx to share an existing transaction.
func ProcessCancellationRefundTx(
	ctx context.Context,
	tx pgx.Tx,
	bookingID string,
	subtotal float64,
	totalPrePaid float64,
	startTime time.Time,
	cancellationTime time.Time,
	reason string,
	actorID *string,
) (*RefundCalculationResult, error) {
	calc := CalculateRefundForCancellation(subtotal, totalPrePaid, cancellationTime, startTime)

	if calc.RefundableAmount <= 0 {
		return &calc, nil
	}

	// Get the booking's user info for refund routing.
	var bookingUserID string
	var isGuest bool
	if err := tx.QueryRow(ctx, `
		SELECT b.user_id, COALESCE(u.account_role = 'guest', false)
		FROM bookings b
		LEFT JOIN users u ON b.user_id = u.id
		WHERE b.id = $1
	`, bookingID).Scan(&bookingUserID, &isGuest); err != nil {
		log.Printf("Failed to get booking user info for refund: %v", err)
		// Non-fatal — we'll still process Square refunds but skip balance credits.
	}

	rows, err := tx.Query(ctx, `
		SELECT id, amount, payment_method, square_payment_id, gift_card_id
		FROM payments
		WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house')
		ORDER BY created_at ASC
	`, bookingID)
	if err != nil {
		log.Printf("Failed to fetch payments for refund: %v", err)
		return &calc, nil
	}
	defer rows.Close()

	// Read all payments into a slice, then close rows immediately.
	type paymentRow struct {
		ID              string
		Amount          float64
		PaymentMethod   string
		SquarePaymentID *string
		GiftCardID      *string
	}
	var payments []paymentRow
	for rows.Next() {
		var p paymentRow
		if err := rows.Scan(&p.ID, &p.Amount, &p.PaymentMethod, &p.SquarePaymentID, &p.GiftCardID); err != nil {
			log.Printf("Failed to scan payment row: %v", err)
			continue
		}
		payments = append(payments, p)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Payment row iteration error: %v", err)
	}

	refundRemaining := calc.RefundableAmount

	for _, p := range payments {
		if refundRemaining <= 0 {
			break
		}

		paymentID := p.ID
		paymentMethod := p.PaymentMethod
		amount := p.Amount
		giftCardID := p.GiftCardID

		refundThisPayment := math.Min(amount, refundRemaining)
		var squareRefundID *string

		switch paymentMethod {
		case "online_square", "in_person_card":
			// Square API refund is processed AFTER the transaction commits
			// (see ProcessPendingSquareRefunds). Inside the tx we only record
			// the refund record as "pending" for post-commit processing.
			if isGuest || bookingUserID == "" {
				log.Printf("Guest card refund: booking %s, payment %s, amount £%.2f — will be processed after commit", bookingID, paymentID, refundThisPayment)
			}
			// squareRefundID stays nil — will be set by ProcessPendingSquareRefunds

		case "giftcard":
			if giftCardID == nil || *giftCardID == "" {
				log.Printf("Giftcard payment %s has no gift_card_id — cannot refund to card. Skipping.", paymentID)
				break
			}
			var expired bool
			if err := tx.QueryRow(ctx, `
				SELECT expiry_date IS NOT NULL AND expiry_date < NOW()
				FROM gift_cards WHERE id = $1
			`, *giftCardID).Scan(&expired); err != nil {
				log.Printf("Failed to check gift card %s expiry: %v — proceeding with refund", *giftCardID, err)
			} else if expired {
				log.Printf("Gift card %s has expired — money retained by salon, no refund due for booking %s", *giftCardID, bookingID)
				break
			}
			if _, err := tx.Exec(ctx, `
				UPDATE gift_cards SET amount_remaining = amount_remaining + $1, last_used_at = NOW()
				WHERE id = $2
			`, refundThisPayment, *giftCardID); err != nil {
				log.Printf("Failed to refund £%.2f to gift card %s: %v", refundThisPayment, *giftCardID, err)
				break
			}
			if _, err := tx.Exec(ctx, `
				INSERT INTO gift_card_transactions (gift_card_id, transaction_type, amount, reference_type, reference_id, user_id, notes)
				VALUES ($1, 'refund', $2, 'booking', $3, $4, $5)
			`, *giftCardID, refundThisPayment, bookingID, bookingUserID, "Refund from cancelled booking"); err != nil {
				log.Printf("Failed to create gift card transaction for refund: %v", err)
			}

		case "cash":
			if isGuest || bookingUserID == "" {
				log.Printf("Guest cash refund: booking %s, payment %s, amount £%.2f — admin must process cash refund at till", bookingID, paymentID, refundThisPayment)
			} else {
				log.Printf("Crediting £%.2f to user %s balance for cash payment %s", refundThisPayment, bookingUserID, paymentID)
				if _, balErr := tx.Exec(ctx, `
					INSERT INTO user_giftcard_balances (user_id, balance, updated_at)
					VALUES ($1, $2, NOW())
					ON CONFLICT (user_id) DO UPDATE SET
						balance = user_giftcard_balances.balance + EXCLUDED.balance,
						updated_at = NOW()
				`, bookingUserID, refundThisPayment); balErr != nil {
					log.Printf("Failed to credit user %s balance for refund of booking %s: %v", bookingUserID, bookingID, balErr)
				}
			}

		default:
			log.Printf("Skipping refund for payment %s with method %q (no money exchanged)", paymentID, paymentMethod)
		}

		recordStatus := "completed"
		if paymentMethod == "online_square" || paymentMethod == "in_person_card" {
			recordStatus = "pending"
		}
		record := RefundRecord{
			PaymentID:      paymentID,
			BookingID:      bookingID,
			Amount:         refundThisPayment,
			SquareRefundID: squareRefundID,
			Status:         recordStatus,
			Reason:         reason,
			CreatedBy:      actorID,
			CreatedAt:      clock.Now(),
		}

		_, dbErr := tx.Exec(ctx, `
			INSERT INTO refunds (payment_id, booking_id, amount, square_refund_id, status, reason, created_by, created_at)
			VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
		`, record.PaymentID, record.BookingID, record.Amount, record.SquareRefundID, record.Status, record.Reason, record.CreatedBy, record.CreatedAt)
		if dbErr != nil {
			log.Printf("Failed to create refund record for payment %s: %v", paymentID, dbErr)
			continue
		}

		refundRemaining -= refundThisPayment
	}

	if bookingUserID != "" {
		var loyaltyUsed bool
		if err := tx.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'loyalty')", bookingID).Scan(&loyaltyUsed); err != nil {
			log.Printf("Failed to check loyalty stamp refund for booking %s: %v", bookingID, err)
		} else if loyaltyUsed {
			_, loyaltyErr := tx.Exec(ctx, "UPDATE users SET loyalty_stamps = loyalty_stamps + $1 WHERE id = $2", LoyaltyStampCost, bookingUserID)
			if loyaltyErr != nil {
				log.Printf("Failed to refund loyalty stamps for booking %s: %v", bookingID, loyaltyErr)
			} else {
				log.Printf("Refunded %d loyalty stamps to user %s after cancellation of booking %s", LoyaltyStampCost, bookingUserID, bookingID)
			}
		}
	}

	return &calc, nil
}

func ProcessCancellationRefund(
	ctx context.Context,
	bookingID string,
	subtotal float64,
	totalPrePaid float64,
	startTime time.Time,
	cancellationTime time.Time,
	reason string,
	actorID *string,
) (*RefundCalculationResult, error) {
	calc := CalculateRefundForCancellation(subtotal, totalPrePaid, cancellationTime, startTime)

	if calc.RefundableAmount <= 0 {
		return &calc, nil
	}

	tx, err := db.Conn.Begin(ctx)
	if err != nil {
		log.Printf("Failed to begin transaction for cancellation refund: %v", err)
		return &calc, nil
	}
	defer func() {
    if err := tx.Rollback(ctx); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	// Get the booking's user info for refund routing.
	var bookingUserID string
	var isGuest bool
	if err := tx.QueryRow(ctx, `
		SELECT b.user_id, COALESCE(u.account_role = 'guest', false)
		FROM bookings b
		LEFT JOIN users u ON b.user_id = u.id
		WHERE b.id = $1
	`, bookingID).Scan(&bookingUserID, &isGuest); err != nil {
		log.Printf("Failed to get booking user info for refund: %v", err)
		// Non-fatal — we'll still process Square refunds but skip balance credits.
	}

	rows, err := tx.Query(ctx, `
		SELECT id, amount, payment_method, square_payment_id, gift_card_id
		FROM payments
		WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house')
		ORDER BY created_at ASC
	`, bookingID)
	if err != nil {
		log.Printf("Failed to fetch payments for refund: %v", err)
		return &calc, nil
	}
	defer rows.Close()

	// Read all payments into a slice, then close rows immediately.
	// This avoids "conn busy" errors when db.Conn.QueryRow/Exec are called
	// inside the processing loop with a per-test transaction (pgx.Tx does not
	// support concurrent queries on the same connection).
	type paymentRow struct {
		ID              string
		Amount          float64
		PaymentMethod   string
		SquarePaymentID *string
		GiftCardID      *string
	}
	var payments []paymentRow
	for rows.Next() {
		var p paymentRow
		if err := rows.Scan(&p.ID, &p.Amount, &p.PaymentMethod, &p.SquarePaymentID, &p.GiftCardID); err != nil {
			log.Printf("Failed to scan payment row: %v", err)
			continue
		}
		payments = append(payments, p)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Payment row iteration error: %v", err)
	}

	refundRemaining := calc.RefundableAmount

	for _, p := range payments {
		if refundRemaining <= 0 {
			break
		}

		paymentID := p.ID
		paymentMethod := p.PaymentMethod
		amount := p.Amount
		giftCardID := p.GiftCardID

		refundThisPayment := math.Min(amount, refundRemaining)
		var squareRefundID *string

		switch paymentMethod {
		case "online_square", "in_person_card":
			// Square API refund is processed AFTER the transaction commits
			// (see ProcessPendingSquareRefunds). Inside the tx we only record
			// the refund record as "pending" for post-commit processing.
			if isGuest || bookingUserID == "" {
				log.Printf("Guest card refund: booking %s, payment %s, amount £%.2f — will be processed after commit", bookingID, paymentID, refundThisPayment)
			}
			// squareRefundID stays nil — will be set by ProcessPendingSquareRefunds

		case "giftcard":
			if giftCardID == nil || *giftCardID == "" {
				log.Printf("Giftcard payment %s has no gift_card_id — cannot refund to card. Skipping.", paymentID)
				break
			}
			var expired bool
			if err := tx.QueryRow(ctx, `
				SELECT expiry_date IS NOT NULL AND expiry_date < NOW()
				FROM gift_cards WHERE id = $1
			`, *giftCardID).Scan(&expired); err != nil {
				log.Printf("Failed to check gift card %s expiry: %v — proceeding with refund", *giftCardID, err)
			} else if expired {
				log.Printf("Gift card %s has expired — money retained by salon, no refund due for booking %s", *giftCardID, bookingID)
				break
			}
			if _, err := tx.Exec(ctx, `
				UPDATE gift_cards SET amount_remaining = amount_remaining + $1, last_used_at = NOW()
				WHERE id = $2
			`, refundThisPayment, *giftCardID); err != nil {
				log.Printf("Failed to refund £%.2f to gift card %s: %v", refundThisPayment, *giftCardID, err)
				break
			}
			if _, err := tx.Exec(ctx, `
				INSERT INTO gift_card_transactions (gift_card_id, transaction_type, amount, reference_type, reference_id, user_id, notes)
				VALUES ($1, 'refund', $2, 'booking', $3, $4, $5)
			`, *giftCardID, refundThisPayment, bookingID, bookingUserID, "Refund from cancelled booking"); err != nil {
				log.Printf("Failed to create gift card transaction for refund: %v", err)
			}

		case "cash":
			if isGuest || bookingUserID == "" {
				log.Printf("Guest cash refund: booking %s, payment %s, amount £%.2f — admin must process cash refund at till", bookingID, paymentID, refundThisPayment)
			} else {
				log.Printf("Crediting £%.2f to user %s balance for cash payment %s", refundThisPayment, bookingUserID, paymentID)
				if _, balErr := tx.Exec(ctx, `
					INSERT INTO user_giftcard_balances (user_id, balance, updated_at)
					VALUES ($1, $2, NOW())
					ON CONFLICT (user_id) DO UPDATE SET
						balance = user_giftcard_balances.balance + EXCLUDED.balance,
						updated_at = NOW()
				`, bookingUserID, refundThisPayment); balErr != nil {
					log.Printf("Failed to credit user %s balance for refund of booking %s: %v", bookingUserID, bookingID, balErr)
				}
			}

		default:
			// discount, on_the_house — no real money to refund.
			log.Printf("Skipping refund for payment %s with method %q (no money exchanged)", paymentID, paymentMethod)
		}

		recordStatus := "completed"
		if paymentMethod == "online_square" || paymentMethod == "in_person_card" {
			recordStatus = "pending"
		}
		record := RefundRecord{
			PaymentID:      paymentID,
			BookingID:      bookingID,
			Amount:         refundThisPayment,
			SquareRefundID: squareRefundID,
			Status:         recordStatus,
			Reason:         reason,
			CreatedBy:      actorID,
			CreatedAt:      clock.Now(),
		}

		_, dbErr := tx.Exec(ctx, `
			INSERT INTO refunds (payment_id, booking_id, amount, square_refund_id, status, reason, created_by, created_at)
			VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
		`, record.PaymentID, record.BookingID, record.Amount, record.SquareRefundID, record.Status, record.Reason, record.CreatedBy, record.CreatedAt)
		if dbErr != nil {
			log.Printf("Failed to create refund record for payment %s: %v", paymentID, dbErr)
			continue
		}

		refundRemaining -= refundThisPayment
	}

	if bookingUserID != "" {
		var loyaltyUsed bool
		if err := tx.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'loyalty')", bookingID).Scan(&loyaltyUsed); err != nil {
			log.Printf("Failed to check loyalty stamp refund for booking %s: %v", bookingID, err)
		} else if loyaltyUsed {
			_, loyaltyErr := tx.Exec(ctx, "UPDATE users SET loyalty_stamps = loyalty_stamps + $1 WHERE id = $2", LoyaltyStampCost, bookingUserID)
			if loyaltyErr != nil {
				log.Printf("Failed to refund loyalty stamps for booking %s: %v", bookingID, loyaltyErr)
			} else {
				log.Printf("Refunded %d loyalty stamps to user %s after cancellation of booking %s", LoyaltyStampCost, bookingUserID, bookingID)
			}
		}
	}

	if cErr := tx.Commit(ctx); cErr != nil {
		log.Printf("CRITICAL: Failed to commit cancellation refund transaction for booking %s: %v", bookingID, cErr)
		return &calc, nil
	}

	// Process pending Square refunds after the transaction commits successfully.
	// This ensures Square API calls only happen if the DB records persist.
	ProcessPendingSquareRefunds(ctx, bookingID, reason)

	return &calc, nil
}

// ProcessPendingSquareRefunds queries for refund records where the refund was
// inserted as "pending" (Square refund not yet processed) and calls the Square
// API to process them. This ensures Square API calls happen AFTER the DB
// transaction commits — if the commit fails, no Square money is lost.
//
// Call this AFTER the enclosing transaction (if any) has been committed.
func ProcessPendingSquareRefunds(ctx context.Context, bookingID string, reason string) {
	rows, err := db.Conn.Query(ctx, `
		SELECT r.id, r.amount, p.square_payment_id
		FROM refunds r
		JOIN payments p ON r.payment_id = p.id
		WHERE r.booking_id = $1
		  AND r.status = 'pending'
		  AND p.payment_method IN ('online_square', 'in_person_card')
		  AND r.square_refund_id IS NULL
	`, bookingID)
	if err != nil {
		log.Printf("Failed to query pending Square refunds for booking %s: %v", bookingID, err)
		return
	}
	defer rows.Close()

	type pendingRefund struct {
		ID              string
		Amount          float64
		SquarePaymentID *string
	}
	var pending []pendingRefund
	for rows.Next() {
		var pr pendingRefund
		if err := rows.Scan(&pr.ID, &pr.Amount, &pr.SquarePaymentID); err != nil {
			log.Printf("Failed to scan pending refund row: %v", err)
			continue
		}
		pending = append(pending, pr)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Pending refund row iteration error: %v", err)
	}

	// Deduplicate: multiple split payment records can share the same
	// square_payment_id — only refund each Square payment once.
	refundedSquareIDs := make(map[string]bool)

	for _, pr := range pending {
		if pr.SquarePaymentID == nil || *pr.SquarePaymentID == "" {
			// No Square payment ID — mark as completed (no API call needed)
			_, upErr := db.Conn.Exec(ctx, `UPDATE refunds SET status = 'completed' WHERE id = $1`, pr.ID)
			if upErr != nil {
				log.Printf("Failed to update refund %s to completed: %v", pr.ID, upErr)
			}
			continue
		}

		if refundedSquareIDs[*pr.SquarePaymentID] {
			// Already refunded this Square payment via a previous split record.
			// Mark this refund as completed since the money is already returned.
			if _, upErr := db.Conn.Exec(ctx, `UPDATE refunds SET status = 'completed' WHERE id = $1`, pr.ID); upErr != nil {
				log.Printf("Failed to update refund %s to completed (deduped): %v", pr.ID, upErr)
			}
			continue
		}

		refundCents := int64(math.Round(pr.Amount * 100))
		refundReq := square.RefundPaymentReq{
			PaymentID:      *pr.SquarePaymentID,
			Amount:         refundCents,
			IdempotencyKey: pr.ID + "-square-" + clock.Now().Format("20060102150405"),
			Reason:         reason,
		}
		sqResult, sqErr := SquareClient.RefundPayment(ctx, refundReq)
		if sqErr != nil {
			log.Printf("Square refund failed for pending refund %s (payment %s): %v — record left as 'pending' for manual retry", pr.ID, *pr.SquarePaymentID, sqErr)
			// Leave status as 'pending' — can be retried manually or via admin tool.
			continue
		}

		refundedSquareIDs[*pr.SquarePaymentID] = true

		_, upErr := db.Conn.Exec(ctx, `
			UPDATE refunds SET square_refund_id = $1, status = 'completed' WHERE id = $2
		`, sqResult.ID, pr.ID)
		if upErr != nil {
			log.Printf("CRITICAL: Square refund succeeded (ID=%s) but DB record %s update failed: %v — manual reconciliation required", sqResult.ID, pr.ID, upErr)
		}
	}
}


