package payments

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"time"

	"crussell/clock"
	"crussell/db"
	"crussell/internal/square"
	"crussell/internal/validators"
	"crussell/mw"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5"
)

// --- Types ---

type GiftCard struct {
	ID              string     `json:"id"`
	TotalFundsAdded float64    `json:"total_funds_added"`
	AmountRemaining float64    `json:"amount_remaining"`
	CreatedBy       *string    `json:"created_by,omitempty"`
	CreatedAt       time.Time  `json:"created_at"`
	RedeemedAt      *time.Time `json:"redeemed_at,omitempty"`
	RedeemedBy      *string    `json:"redeemed_by,omitempty"`
	IsInventory     bool       `json:"is_inventory"`
	LastUsedAt      *time.Time `json:"last_used_at,omitempty"`
}

type UserBalance struct {
	UserID    string    `json:"user_id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	Balance   float64   `json:"balance"`
	UpdatedAt time.Time `json:"updated_at"`
}

type GiftCardListResponse struct {
	TotalUnclaimed    float64       `json:"total_unclaimed"`
	TotalUserBalances float64       `json:"total_user_balances"`
	GiftCards         []GiftCard    `json:"gift_cards"`
	UserBalances      []UserBalance `json:"user_balances"`
	Total             int           `json:"total"`
	UBTotal           int           `json:"ub_total"`
	Page              int           `json:"page"`
	PerPage           int           `json:"perPage"`
	TotalPages        int           `json:"totalPages"`
	NextCursor        *string       `json:"next_cursor,omitempty"`
}

type CreateGiftCardRequest struct {
	Amount      float64 `json:"amount"`
	IsInventory bool    `json:"is_inventory,omitempty"`
}

type TopUpGiftCardRequest struct {
	Amount        float64 `json:"amount"`
	PaymentMethod string  `json:"payment_method"`
}

type TransferGiftCardRequest struct {
	ToCardID string  `json:"to_card_id"`
	Amount   float64 `json:"amount"`
}

type BuyGiftCardRequest struct {
	Amount         int64   `json:"amount"`
	RecipientType  string  `json:"recipient_type"`
	RecipientEmail string  `json:"recipient_email,omitempty"`
	CardID         *string `json:"card_id,omitempty"`
	NewCardToken   *string `json:"new_card_token,omitempty"`
	SaveCard       bool    `json:"save_card"`
	IdempotencyKey string  `json:"idempotency_key"`
}

type RedeemGiftCardRequest struct {
	Code string `json:"code"`
}

// --- Admin Handlers ---

func GetGiftCards(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Parse query parameters
	query := r.URL.Query()
	searchTerm := query.Get("q")

	// Pagination parameters
	perPage := 10
	cursorStr := query.Get("cursor")

	if perPageStr := query.Get("per_page"); perPageStr != "" {
		if pp, err := strconv.Atoi(perPageStr); err == nil && pp > 0 && pp <= 100 {
			perPage = pp
		}
	}

	// Accept page param for backward compat (deprecated)
	page := 1
	if pageStr := query.Get("page"); pageStr != "" {
		if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
			page = p
		}
	}

	var resp GiftCardListResponse
	resp.GiftCards = []GiftCard{}
	resp.UserBalances = []UserBalance{}
	resp.Page = page
	resp.PerPage = perPage

	// --- Aggregate totals (unfiltered, unpaginated) ---

	err := db.Conn.QueryRow(ctx, `
		SELECT COALESCE(SUM(amount_remaining), 0)
		FROM gift_cards
		WHERE redeemed_by IS NULL
	`).Scan(&resp.TotalUnclaimed)
	if err != nil {
		log.Printf("Failed to calculate total unclaimed gift cards: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	err = db.Conn.QueryRow(ctx, `
		SELECT COALESCE(SUM(balance), 0)
		FROM user_giftcard_balances
	`).Scan(&resp.TotalUserBalances)
	if err != nil {
		log.Printf("Failed to calculate total user balances: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// --- Gift cards (paginated, optionally filtered) ---

	filterType := query.Get("type")
	whereClauses := []string{}
	if searchTerm != "" {
		whereClauses = append(whereClauses, "id ILIKE $1")
	}
	if filterType == "customer" {
		whereClauses = append(whereClauses, "is_inventory = FALSE")
	} else if filterType == "inventory" {
		whereClauses = append(whereClauses, "is_inventory = TRUE")
	}

	whereSQL := ""
	if len(whereClauses) > 0 {
		whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
	}

	var gcTotal int
	var gcListArgs []any

	gcListQuery := fmt.Sprintf(`
		SELECT id, total_funds_added, amount_remaining, created_at, is_inventory
		FROM gift_cards
		%s
	`, whereSQL)

	if searchTerm != "" {
		searchPattern := "%" + searchTerm + "%"
		gcListArgs = []any{searchPattern}

		if cursorStr != "" {
			cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
			if err != nil {
				http.Error(w, "invalid cursor: "+err.Error(), http.StatusBadRequest)
				return
			}
			gcListQuery += " AND (created_at, id) < ($2, $3)"
			gcListArgs = append(gcListArgs, cursorCreatedAt, cursorID)
		}
		gcListQuery += " ORDER BY created_at DESC, id DESC LIMIT $" + strconv.Itoa(len(gcListArgs)+1)
		gcListArgs = append(gcListArgs, perPage+1)
	} else {
		if cursorStr != "" {
			cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
			if err != nil {
				http.Error(w, "invalid cursor: "+err.Error(), http.StatusBadRequest)
				return
			}
			gcListQuery += " WHERE (created_at, id) < ($1, $2)"
			gcListArgs = append(gcListArgs, cursorCreatedAt, cursorID)
		}
		gcListQuery += " ORDER BY created_at DESC, id DESC LIMIT $" + strconv.Itoa(len(gcListArgs)+1)
		gcListArgs = append(gcListArgs, perPage+1)
	}

	// Count query: total matching gift cards (same WHERE, without cursor/ORDER BY/LIMIT).
	// Run BEFORE the data query to avoid "conn busy" when using a per-test
	// transaction (single connection).
	gcTotal = 0
	if whereSQL != "" {
		countArgs := []any{}
		if searchTerm != "" {
			countArgs = append(countArgs, "%"+searchTerm+"%")
		}
//nolint:errcheck // zero value is acceptable fallback on scan failure
		_ = db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards "+whereSQL, countArgs...).Scan(&gcTotal)
	} else {
//nolint:errcheck // zero value is acceptable fallback on scan failure
		_ = db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards").Scan(&gcTotal)
	}

	gcRows, err := db.Conn.Query(ctx, gcListQuery, gcListArgs...)

	if err != nil {
		log.Printf("Failed to query gift cards: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer gcRows.Close()

	for gcRows.Next() {
		var gc GiftCard

		err = gcRows.Scan(
			&gc.ID,
			&gc.TotalFundsAdded,
			&gc.AmountRemaining,
			&gc.CreatedAt,
			&gc.IsInventory,
		)
		if err != nil {
			log.Printf("Failed to scan gift card: %v", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		resp.GiftCards = append(resp.GiftCards, gc)
	}

	// --- User balances (all, optionally filtered) ---

	var ubTotal int
	var ubListQuery string
	var ubListArgs []any

	if searchTerm != "" {
		searchPattern := "%" + searchTerm + "%"

		ubListQuery = `
			SELECT
			       b.user_id, u.n_first_name || ' ' || u.n_last_name AS name, u.email, b.balance, b.updated_at
			FROM user_giftcard_balances b
			JOIN users u ON b.user_id = u.id
			WHERE u.n_first_name ILIKE $1
			   OR u.n_last_name ILIKE $1
			   OR u.email ILIKE $1
			ORDER BY b.updated_at DESC
		`
		ubListArgs = []any{searchPattern}
	} else {
		ubListQuery = `
			SELECT
			       b.user_id, u.n_first_name || ' ' || u.n_last_name AS name, u.email, b.balance, b.updated_at
			FROM user_giftcard_balances b
			JOIN users u ON b.user_id = u.id
			ORDER BY b.updated_at DESC
		`
		ubListArgs = []any{}
	}

	ubRows, err := db.Conn.Query(ctx, ubListQuery, ubListArgs...)
	if err != nil {
		log.Printf("Failed to query user balances: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer ubRows.Close()

	// Count query for user balances.
	if searchTerm != "" {
//nolint:errcheck // zero value is acceptable fallback on scan failure
		_ = db.Conn.QueryRow(ctx, `SELECT COUNT(*) FROM user_giftcard_balances b
			JOIN users u ON b.user_id = u.id
			WHERE u.n_first_name ILIKE $1 OR u.n_last_name ILIKE $1 OR u.email ILIKE $1`, "%"+searchTerm+"%").Scan(&ubTotal)
	} else {
//nolint:errcheck // zero value is acceptable fallback on scan failure
		_ = db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM user_giftcard_balances").Scan(&ubTotal)
	}

	for ubRows.Next() {
		var ub UserBalance
		err = ubRows.Scan(
			&ub.UserID,
			&ub.Name,
			&ub.Email,
			&ub.Balance,
			&ub.UpdatedAt,
		)
		if err != nil {
			log.Printf("Failed to scan user balance: %v", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		resp.UserBalances = append(resp.UserBalances, ub)
	}

	resp.UBTotal = ubTotal
	resp.Total = gcTotal

	var nextCursor *string
	if len(resp.GiftCards) > perPage {
		resp.GiftCards = resp.GiftCards[:perPage]
		last := resp.GiftCards[len(resp.GiftCards)-1]
		cursor := last.CreatedAt.Format(time.RFC3339) + "|" + last.ID
		nextCursor = &cursor
	}
	resp.NextCursor = nextCursor

	totalPages := (gcTotal + perPage - 1) / perPage
	if totalPages == 0 {
		totalPages = 1
	}
	resp.TotalPages = totalPages

	_ = json.NewEncoder(w).Encode(resp)
}

func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	adminID, _ := ctx.Value(mw.UserIDKey).(string)

	var req CreateGiftCardRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	if req.Amount < 0 {
		http.Error(w, "Amount must not be negative", http.StatusBadRequest)
		return
	}
	if req.Amount == 0 && !req.IsInventory {
		http.Error(w, "Amount must be greater than zero", http.StatusBadRequest)
		return
	}

	tx, err := db.Conn.Begin(ctx)
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(ctx); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	var gc GiftCard
	var lastUsedAt sql.NullTime
	var purchaseVoucherType string
	err = tx.QueryRow(ctx, `SELECT COALESCE(voucher_type, 'SPV') FROM business_settings LIMIT 1`).Scan(&purchaseVoucherType)
	if err != nil {
		log.Printf("Failed to query voucher type: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	if purchaseVoucherType == "" {
		purchaseVoucherType = "SPV"
	}
	err = tx.QueryRow(ctx, `
		INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, last_used_at, voucher_type_at_purchase)
		VALUES ($1, $1, $2, $3, NOW(), $4)
		RETURNING id, total_funds_added, amount_remaining, created_by, created_at, is_inventory, last_used_at
	`, req.Amount, adminID, req.IsInventory, purchaseVoucherType).Scan(
		&gc.ID,
		&gc.TotalFundsAdded,
		&gc.AmountRemaining,
		&gc.CreatedBy,
		&gc.CreatedAt,
		&gc.IsInventory,
		&lastUsedAt,
	)
	if err != nil {
		log.Printf("Failed to create gift card: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if lastUsedAt.Valid {
		gc.LastUsedAt = &lastUsedAt.Time
	}

	var notes *string
	if req.IsInventory {
		s := "inventory card"
		notes = &s
	} else {
		s := "giveaway"
		notes = &s
	}
	_, err = tx.Exec(ctx, `
		INSERT INTO gift_card_transactions (gift_card_id, transaction_type, amount, reference_type, reference_id, user_id, notes)
		VALUES ($1, 'purchase', $2, 'api', NULL, $3, $4)
	`, gc.ID, req.Amount, adminID, notes)
	if err != nil {
		log.Printf("Failed to record gift card transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(ctx); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(gc)
}

func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	adminID, _ := ctx.Value(mw.UserIDKey).(string)
	cardID := chi.URLParam(r, "id")
	if cardID == "" || !validators.IsValidID(cardID) {
		http.Error(w, "Invalid gift card ID", http.StatusBadRequest)
		return
	}

	var req TopUpGiftCardRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	if req.Amount <= 0 {
		http.Error(w, "Amount must be greater than zero", http.StatusBadRequest)
		return
	}

	validMethods := map[string]string{
		"cash":          "cash",
		"card_machine":  "in_person_card",
		"online_square": "online_square",
		"on_the_house":  "on_the_house",
	}
	_, ok := validMethods[req.PaymentMethod]
	if !ok {
		http.Error(w, "Invalid payment method. Must be one of: cash, card_machine, online_square, on_the_house", http.StatusBadRequest)
		return
	}

	tx, err := db.Conn.Begin(ctx)
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(ctx); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	var redeemedBy sql.NullString
	var isInventory bool
	var currentTotalFunds float64
	err = tx.QueryRow(ctx, "SELECT redeemed_by, is_inventory, total_funds_added FROM gift_cards WHERE id = $1", cardID).Scan(&redeemedBy, &isInventory, &currentTotalFunds)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.Error(w, "Gift card not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to check gift card: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if redeemedBy.Valid {
		http.Error(w, "Cannot top up a card that has already been redeemed to an account", http.StatusBadRequest)
		return
	}

	txType := "topup"
	var notes *string
	if isInventory && currentTotalFunds == 0 {
		txType = "purchase"
		s := "first top-up on inventory card"
		notes = &s
	}

	var gc GiftCard
	var createdBy sql.NullString
	err = tx.QueryRow(ctx, `
		UPDATE gift_cards
		SET total_funds_added = total_funds_added + $1,
		    amount_remaining = amount_remaining + $1,
		    last_used_at = NOW()
		WHERE id = $2
		RETURNING id, total_funds_added, amount_remaining, created_by, created_at
	`, req.Amount, cardID).Scan(
		&gc.ID,
		&gc.TotalFundsAdded,
		&gc.AmountRemaining,
		&createdBy,
		&gc.CreatedAt,
	)
	if err != nil {
		log.Printf("Failed to top up gift card: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if createdBy.Valid {
		gc.CreatedBy = &createdBy.String
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO gift_card_transactions (gift_card_id, transaction_type, amount, reference_type, reference_id, user_id, notes)
		VALUES ($1, $2, $3, 'api', NULL, $4, $5)
	`, cardID, txType, req.Amount, adminID, notes)
	if err != nil {
		log.Printf("Failed to record gift card transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(ctx); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(gc)
}

func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	fromCardID := chi.URLParam(r, "from")
	if fromCardID == "" || !validators.IsValidID(fromCardID) {
		http.Error(w, "Invalid source gift card ID", http.StatusBadRequest)
		return
	}

	var req TransferGiftCardRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	req.ToCardID = validators.NormalizeGiftCardCode(req.ToCardID)
	if !validators.IsValidID(req.ToCardID) {
		http.Error(w, "Invalid destination gift card ID", http.StatusBadRequest)
		return
	}

	if fromCardID == req.ToCardID {
		http.Error(w, "Source and destination cards must be different", http.StatusBadRequest)
		return
	}

	if req.Amount <= 0 {
		http.Error(w, "Amount must be greater than zero", http.StatusBadRequest)
		return
	}

	tx, err := db.Conn.Begin(ctx)
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(ctx); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	var fromRedeemedBy, toRedeemedBy sql.NullString
	var fromRemaining, toRemaining float64

	err = tx.QueryRow(ctx, "SELECT redeemed_by, amount_remaining FROM gift_cards WHERE id = $1", fromCardID).Scan(&fromRedeemedBy, &fromRemaining)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.Error(w, "Source gift card not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to check source gift card: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	err = tx.QueryRow(ctx, "SELECT redeemed_by, amount_remaining FROM gift_cards WHERE id = $1", req.ToCardID).Scan(&toRedeemedBy, &toRemaining)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.Error(w, "Destination gift card not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to check destination gift card: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if fromRedeemedBy.Valid || toRedeemedBy.Valid {
		http.Error(w, "Cannot transfer balance to/from cards redeemed to accounts", http.StatusBadRequest)
		return
	}

	if fromRemaining < req.Amount {
		http.Error(w, "Insufficient balance on source gift card", http.StatusBadRequest)
		return
	}

	_, err = tx.Exec(ctx, `
		UPDATE gift_cards
		SET amount_remaining = amount_remaining - $1,
		    last_used_at = NOW()
		WHERE id = $2
	`, req.Amount, fromCardID)
	if err != nil {
		log.Printf("Failed to deduct from source: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	_, err = tx.Exec(ctx, `
		UPDATE gift_cards
		SET amount_remaining = amount_remaining + $1,
		    total_funds_added = total_funds_added + $1,
		    last_used_at = NOW()
		WHERE id = $2
	`, req.Amount, req.ToCardID)
	if err != nil {
		log.Printf("Failed to add to destination: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(ctx); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
	_ = json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}

// --- User Handlers ---

func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	userID, ok := ctx.Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	var req RedeemGiftCardRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	code := validators.NormalizeGiftCardCode(req.Code)
	if !validators.IsValidID(code) {
		http.Error(w, "Invalid gift card code format", http.StatusBadRequest)
		return
	}

	tx, err := db.Conn.Begin(ctx)
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(ctx); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	var amountRemaining float64
	var redeemedBy sql.NullString
	err = tx.QueryRow(ctx, `
		SELECT amount_remaining, redeemed_by
		FROM gift_cards
		WHERE id = $1
		FOR UPDATE
	`, code).Scan(&amountRemaining, &redeemedBy)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.Error(w, "Invalid or expired gift card code", http.StatusNotFound)
			return
		}
		log.Printf("Failed to query gift card: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if redeemedBy.Valid {
		http.Error(w, "This gift card has already been redeemed", http.StatusBadRequest)
		return
	}

	if amountRemaining <= 0 {
		http.Error(w, "This gift card has no remaining balance", http.StatusBadRequest)
		return
	}

	_, err = tx.Exec(ctx, `
		UPDATE gift_cards
		SET amount_remaining = 0,
		    redeemed_at = NOW(),
		    redeemed_by = $1
		WHERE id = $2
	`, userID, code)
	if err != nil {
		log.Printf("Failed to update gift card: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	_, err = 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()
	`, userID, amountRemaining)
	if err != nil {
		log.Printf("Failed to update user gift card balance: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO gift_card_transactions (gift_card_id, transaction_type, amount, reference_type, reference_id, user_id, notes)
		VALUES ($1, 'redeem_to_balance', $2, 'api', NULL, $3, NULL)
	`, code, amountRemaining, userID)
	if err != nil {
		log.Printf("Failed to record gift card transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(ctx); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
	_ = json.NewEncoder(w).Encode(map[string]any{
		"status":          "success",
		"amount_redeemed": amountRemaining,
	})
}

func GetGiftCardBalance(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	userID, ok := ctx.Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	var balance float64
	err := db.Conn.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			_ = json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00})
			return
		}
		log.Printf("Failed to query user balance: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(map[string]float64{"balance": balance})
}

// GetUserGiftCardBalanceAdmin Handler returns any user's balance for the admin.
func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	userID := chi.URLParam(r, "id")
	if userID == "" || !validators.IsValidID(userID) {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	adminID, _ := ctx.Value(mw.UserIDKey).(string)

	var balance float64
	err := db.Conn.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			_ = json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00})
			return
		}
		log.Printf("Failed to query user balance: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	detailsJSON := fmt.Sprintf(`{"balance": %.2f}`, balance)

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

		if _, err := tx.Exec(ctx, `
			INSERT INTO admin_audit_log (admin_id, action_type, target_user_id, details)
			VALUES ($1, 'balance_check', $2, $3::jsonb)
		`, adminID, userID, detailsJSON); err != nil {
			log.Printf("Failed to record admin_audit_log (non-critical): %v", err)
		}

		if err := tx.Commit(ctx); err != nil {
			log.Printf("Failed to commit transaction: %v", err)
		}
	}

	_ = json.NewEncoder(w).Encode(map[string]float64{"balance": balance})
}

func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	userID, ok := ctx.Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	var req BuyGiftCardRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	allowedAmounts := map[int64]bool{1000: true, 2000: true, 5000: true}
	if !allowedAmounts[req.Amount] {
		http.Error(w, "Invalid amount. Must be £10, £20, or £50.", http.StatusBadRequest)
		return
	}

	if req.RecipientType != "self" && req.RecipientType != "friend" {
		http.Error(w, "Invalid recipient type", http.StatusBadRequest)
		return
	}

	if err := ValidateCardInfo(req.CardID, req.NewCardToken); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	paymentService := NewPaymentService()

	if req.IdempotencyKey != "" {
		existing, err := paymentService.CheckIdempotencyByKey(ctx, req.IdempotencyKey)
		if err != nil {
			log.Printf("Failed to check idempotency: %v", err)
		}
		if existing != nil {
			_ = json.NewEncoder(w).Encode(existing)
			return
		}
	}

	var sourceID string
	var savedCardID *string

	if req.NewCardToken != nil && *req.NewCardToken != "" {
		cardOnFile, err := SquareClient.CreateCardOnFile(ctx, userID, *req.NewCardToken)
		if err != nil {
			log.Printf("Failed to create card on file: %v", err)
			http.Error(w, "Failed to process card", http.StatusInternalServerError)
			return
		}

		sourceID = cardOnFile.CardID

		if req.SaveCard {
			cardID, err := paymentService.SaveCardForUser(ctx, userID, cardOnFile.CardID, cardOnFile.Brand, cardOnFile.Last4, cardOnFile.ExpMonth, cardOnFile.ExpYear, cardOnFile.Fingerprint)
			if err != nil {
				log.Printf("Failed to save card: %v", err)
			} else {
				savedCardID = &cardID
			}
		}
	} else if req.CardID != nil {
		card, err := paymentService.GetCardByID(ctx, *req.CardID, userID)
		if err != nil {
			if errors.Is(err, pgx.ErrNoRows) {
				http.Error(w, "Card not found", http.StatusNotFound)
				return
			}
			log.Printf("Failed to get card: %v", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		sourceID = card.SquareCardID
		savedCardID = req.CardID
	}

	amountPounds := float64(req.Amount) / 100.0

	// Step 1: Insert payment with status='pending' inside a DB transaction.
	// Square is NOT called yet — if the tx fails, no harm done.
	// Gift card and balance are created AFTER payment succeeds (below).
	tx, err := db.Conn.Begin(ctx)
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(ctx); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	var buyPaymentID string
	fees := paymentService.CalculateFees(req.Amount, "online")
	record := PaymentRecord{
		PaymentType:     "full",
		PaymentMethod:   "online_square",
		Status:          "pending",
		Amount:          amountPounds,
		SquarePaymentID: nil,
		IdempotencyKey:  &req.IdempotencyKey,
		Fees:            float64(fees) / 100.0,
		UserSavedCardID: savedCardID,
		CreatedAt:       clock.Now(),
		UpdatedAt:       clock.Now(),
		CreatedBy:       &userID,
	}
	err = tx.QueryRow(ctx, `
		INSERT INTO payments (payment_type, payment_method, status, amount, square_payment_id, idempotency_key, fees, user_saved_card_id, created_by, created_at, updated_at)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
		RETURNING id
	`, record.PaymentType, record.PaymentMethod, record.Status, record.Amount, record.SquarePaymentID, record.IdempotencyKey, record.Fees, record.UserSavedCardID, record.CreatedBy, record.CreatedAt, record.UpdatedAt).Scan(&buyPaymentID)
	if err != nil {
		log.Printf("Failed to insert payment record: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Apply VAT to the pending payment
	vatCfg, vatErr := GetVATConfig(ctx, tx)
	if vatErr == nil && vatCfg.IsVATRegistered && vatCfg.VoucherType == "SPV" {
		if _, vatExecErr := tx.Exec(ctx, "SELECT apply_vat_to_payment($1, $2)", buyPaymentID, vatCfg.DefaultVATRate); vatExecErr != nil {
			log.Printf("Failed to apply VAT to buy gift card payment %s: %v", buyPaymentID, vatExecErr)
		}
	}

	if err := tx.Commit(ctx); err != nil {
		log.Printf("Failed to commit buy transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Step 2: DB transaction committed — safe to call Square now.
	// If Square fails, the payment record stays 'pending' for manual retry.
	paymentReq := square.CreatePaymentReq{
		Amount:         req.Amount,
		Currency:       "GBP",
		SourceID:       sourceID,
		IdempotencyKey: req.IdempotencyKey,
		Note:           "Gift Card Purchase",
	}

	paymentResult, err := SquareClient.CreatePayment(ctx, paymentReq)
	if err != nil {
		log.Printf("Failed to process gift card purchase payment: %v", err)
		// Payment record intentionally left as 'pending' for manual retry.
		http.Error(w, "Payment failed", http.StatusPaymentRequired)
		return
	}

	// Step 3: Square succeeded — update payment, create gift card.
	_, upErr := db.Conn.Exec(ctx,
		`UPDATE payments SET status = 'completed', square_payment_id = $1 WHERE id = $2`,
		paymentResult.SquarePayID, buyPaymentID,
	)
	if upErr != nil {
		log.Printf("CRITICAL: Square payment succeeded (ID=%s) but payment %s update failed: %v — manual reconciliation required", paymentResult.SquarePayID, buyPaymentID, upErr)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	var cardID string

	if req.RecipientType == "self" {
		var purchaseVoucherType string
		err = db.Conn.QueryRow(ctx, `SELECT COALESCE(voucher_type, 'SPV') FROM business_settings LIMIT 1`).Scan(&purchaseVoucherType)
		if err != nil {
			log.Printf("Failed to query voucher type: %v", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		if purchaseVoucherType == "" {
			purchaseVoucherType = "SPV"
		}
		err = db.Conn.QueryRow(ctx, `
			INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by, is_inventory, voucher_type_at_purchase)
			VALUES ($1, 0, $2, NOW(), $2, FALSE, $3)
			RETURNING id
		`, amountPounds, userID, purchaseVoucherType).Scan(&cardID)
		if err != nil {
			log.Printf("CRITICAL: Square payment succeeded (ID=%s) but gift card creation failed: %v — manual reconciliation required", paymentResult.SquarePayID, err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		_, err = db.Conn.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()
		`, userID, amountPounds)
		if err != nil {
			log.Printf("CRITICAL: Square payment succeeded (ID=%s) but balance update for user %s failed: %v — manual reconciliation required", paymentResult.SquarePayID, userID, err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		_, err = db.Conn.Exec(ctx, `
			INSERT INTO gift_card_transactions (gift_card_id, transaction_type, amount, reference_type, reference_id, user_id, notes)
			VALUES ($1, 'purchase', $2, 'api', NULL, $3, 'self-purchase, auto-redeemed')
		`, cardID, amountPounds, userID)
		if err != nil {
			log.Printf("CRITICAL: Square payment succeeded (ID=%s) but gift card transaction record failed: %v — manual reconciliation required", paymentResult.SquarePayID, err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
	} else {
		var purchaseVoucherType string
		err = db.Conn.QueryRow(ctx, `SELECT COALESCE(voucher_type, 'SPV') FROM business_settings LIMIT 1`).Scan(&purchaseVoucherType)
		if err != nil {
			log.Printf("Failed to query voucher type: %v", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		if purchaseVoucherType == "" {
			purchaseVoucherType = "SPV"
		}
		err = db.Conn.QueryRow(ctx, `
			INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, voucher_type_at_purchase)
			VALUES ($1, $1, $2, FALSE, $3)
			RETURNING id
		`, amountPounds, userID, purchaseVoucherType).Scan(&cardID)
		if err != nil {
			log.Printf("CRITICAL: Square payment succeeded (ID=%s) but gift card creation failed: %v — manual reconciliation required", paymentResult.SquarePayID, err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		recipient := req.RecipientEmail
		if recipient == "" {
			var userEmail string
			err = db.Conn.QueryRow(ctx, "SELECT email FROM users WHERE id = $1", userID).Scan(&userEmail)
			if err != nil {
				log.Printf("Failed to query user email: %v", err)
				http.Error(w, "internal server error", http.StatusInternalServerError)
				return
			}
			recipient = userEmail
		}
		log.Printf("[TODO EMAIL] Send gift card code %s (Value: £%.2f) to %s", cardID, amountPounds, recipient)

		_, err = db.Conn.Exec(ctx, `
			INSERT INTO gift_card_transactions (gift_card_id, transaction_type, amount, reference_type, reference_id, user_id, notes)
			VALUES ($1, 'purchase', $2, 'api', NULL, $3, 'purchased for friend')
		`, cardID, amountPounds, userID)
		if err != nil {
			log.Printf("CRITICAL: Square payment succeeded (ID=%s) but gift card transaction record failed: %v — manual reconciliation required", paymentResult.SquarePayID, err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
	}

	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(map[string]any{
		"status": "success",
		"code":   cardID,
		"amount": amountPounds,
	})
}

// --- Helpers ---

type ExpiredBalance struct {
	ID              string     `json:"id"`
	AccountID       *string    `json:"account_id,omitempty"`
	OriginalBalance float64    `json:"original_balance"`
	ExpiredAt       time.Time  `json:"expired_at"`
	ClaimedAt       *time.Time `json:"claimed_at,omitempty"`
	ClaimedByAdmin  *string    `json:"claimed_by_admin,omitempty"`
	Notes           *string    `json:"notes,omitempty"`
}

func GetExpiredBalances(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	rows, err := db.Conn.Query(ctx, `
		SELECT id, account_id, original_balance, expired_at, claimed_at, claimed_by_admin, notes
		FROM gift_card_expired_balances
		ORDER BY expired_at DESC
	`)
	if err != nil {
		log.Printf("Failed to query expired balances: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var balances []ExpiredBalance
	for rows.Next() {
		var b ExpiredBalance
		var accountID, claimedByAdmin, notes sql.NullString
		var claimedAt sql.NullTime

		err = rows.Scan(&b.ID, &accountID, &b.OriginalBalance, &b.ExpiredAt, &claimedAt, &claimedByAdmin, &notes)
		if err != nil {
			log.Printf("Failed to scan expired balance: %v", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		if accountID.Valid {
			b.AccountID = &accountID.String
		}
		if claimedAt.Valid {
			b.ClaimedAt = &claimedAt.Time
		}
		if claimedByAdmin.Valid {
			b.ClaimedByAdmin = &claimedByAdmin.String
		}
		if notes.Valid {
			b.Notes = &notes.String
		}

		balances = append(balances, b)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if balances == nil {
		balances = []ExpiredBalance{}
	}

	_ = json.NewEncoder(w).Encode(map[string]any{
		"expired_balances": balances,
		"total":            len(balances),
	})
}

type ClaimExpiredBalanceRequest struct {
	BalanceID string  `json:"balance_id"`
	Notes     *string `json:"notes,omitempty"`
}

func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	adminID, ok := ctx.Value(mw.UserIDKey).(string)
	if !ok || adminID == "" {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	var req ClaimExpiredBalanceRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	if req.BalanceID == "" {
		http.Error(w, "balance_id is required", http.StatusBadRequest)
		return
	}

	tx, err := db.Conn.Begin(ctx)
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(ctx); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	var existingClaimedAt sql.NullTime
	err = tx.QueryRow(ctx, `
		SELECT claimed_at FROM gift_card_expired_balances WHERE id = $1 FOR UPDATE
	`, req.BalanceID).Scan(&existingClaimedAt)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.Error(w, "Expired balance not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to check expired balance: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if existingClaimedAt.Valid {
		http.Error(w, "Balance already claimed", http.StatusConflict)
		return
	}

	_, err = tx.Exec(ctx, `
		UPDATE gift_card_expired_balances
		SET claimed_at = NOW(), claimed_by_admin = $1, notes = $2
		WHERE id = $3
	`, adminID, req.Notes, req.BalanceID)
	if err != nil {
		log.Printf("Failed to claim expired balance: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(ctx); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
	_ = json.NewEncoder(w).Encode(map[string]string{"status": "claimed"})
}
