package wallets

import (
	"context"
	"errors"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/niels/banking-app/backend/internal/domain"
)

type Repository struct {
	db *pgxpool.Pool
}

type CreateWalletParams struct {
	UserID string
	Name   string
}

type AddCurrencyParams struct {
	UserID   string
	WalletID string
	Currency string
}

func NewRepository(db *pgxpool.Pool) *Repository {
	return &Repository{db: db}
}

func (r *Repository) ListCurrencies(ctx context.Context) ([]domain.Currency, error) {
	rows, err := r.db.Query(ctx, `
		SELECT code, name, minor_unit::int, enabled, created_at
		FROM currencies
		WHERE enabled = true
		ORDER BY code
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	currencies := []domain.Currency{}
	for rows.Next() {
		currency, err := scanCurrency(rows)
		if err != nil {
			return nil, err
		}
		currencies = append(currencies, currency)
	}

	return currencies, rows.Err()
}

func (r *Repository) CreateWallet(ctx context.Context, params CreateWalletParams) (domain.Wallet, error) {
	row := r.db.QueryRow(ctx, `
		INSERT INTO wallets (user_id, name)
		VALUES ($1, $2)
		RETURNING id::text, user_id::text, name, status, created_at, updated_at
	`, params.UserID, params.Name)

	wallet, err := scanWallet(row)
	if err != nil {
		return domain.Wallet{}, err
	}
	return wallet, nil
}

func (r *Repository) ListWallets(ctx context.Context, userID string) ([]domain.Wallet, error) {
	rows, err := r.db.Query(ctx, `
		SELECT id::text, user_id::text, name, status, created_at, updated_at
		FROM wallets
		WHERE user_id = $1
		ORDER BY created_at DESC
	`, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	wallets := []domain.Wallet{}
	for rows.Next() {
		wallet, err := scanWallet(rows)
		if err != nil {
			return nil, err
		}
		wallets = append(wallets, wallet)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	for i := range wallets {
		balances, err := r.ListBalances(ctx, userID, wallets[i].ID)
		if err != nil {
			return nil, err
		}
		wallets[i].Balances = balances
	}

	return wallets, nil
}

func (r *Repository) FindOwned(ctx context.Context, userID, walletID string) (domain.Wallet, error) {
	row := r.db.QueryRow(ctx, `
		SELECT id::text, user_id::text, name, status, created_at, updated_at
		FROM wallets
		WHERE id = $1 AND user_id = $2
	`, walletID, userID)

	wallet, err := scanWallet(row)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.Wallet{}, domain.ErrNotFound
	}
	if err != nil {
		return domain.Wallet{}, err
	}

	balances, err := r.ListBalances(ctx, userID, wallet.ID)
	if err != nil {
		return domain.Wallet{}, err
	}
	wallet.Balances = balances

	return wallet, nil
}

func (r *Repository) AddCurrency(ctx context.Context, params AddCurrencyParams) (domain.WalletBalance, error) {
	row := r.db.QueryRow(ctx, `
		INSERT INTO wallet_balances (wallet_id, currency)
		SELECT w.id, c.code
		FROM wallets w
		JOIN currencies c ON c.code = $3 AND c.enabled = true
		WHERE w.id = $2 AND w.user_id = $1 AND w.status = 'active'
		ON CONFLICT (wallet_id, currency) DO UPDATE
		SET updated_at = wallet_balances.updated_at
		RETURNING id::text, wallet_id::text, currency, available_balance_cents, reserved_balance_cents, created_at, updated_at
	`, params.UserID, params.WalletID, params.Currency)

	balance, err := scanBalance(row)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.WalletBalance{}, domain.ErrNotFound
	}
	if err != nil {
		if isForeignKeyViolation(err) {
			return domain.WalletBalance{}, domain.ErrValidation
		}
		return domain.WalletBalance{}, err
	}

	return balance, nil
}

func (r *Repository) ListBalances(ctx context.Context, userID, walletID string) ([]domain.WalletBalance, error) {
	rows, err := r.db.Query(ctx, `
		SELECT wb.id::text, wb.wallet_id::text, wb.currency, wb.available_balance_cents, wb.reserved_balance_cents, wb.created_at, wb.updated_at
		FROM wallet_balances wb
		JOIN wallets w ON w.id = wb.wallet_id
		WHERE wb.wallet_id = $1 AND w.user_id = $2
		ORDER BY wb.currency
	`, walletID, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	balances := []domain.WalletBalance{}
	for rows.Next() {
		balance, err := scanBalance(rows)
		if err != nil {
			return nil, err
		}
		balances = append(balances, balance)
	}

	return balances, rows.Err()
}

type scanner interface {
	Scan(dest ...any) error
}

func scanCurrency(row scanner) (domain.Currency, error) {
	var currency domain.Currency
	err := row.Scan(&currency.Code, &currency.Name, &currency.MinorUnit, &currency.Enabled, &currency.CreatedAt)
	return currency, err
}

func scanWallet(row scanner) (domain.Wallet, error) {
	var wallet domain.Wallet
	err := row.Scan(&wallet.ID, &wallet.UserID, &wallet.Name, &wallet.Status, &wallet.CreatedAt, &wallet.UpdatedAt)
	return wallet, err
}

func scanBalance(row scanner) (domain.WalletBalance, error) {
	var balance domain.WalletBalance
	err := row.Scan(
		&balance.ID,
		&balance.WalletID,
		&balance.Currency,
		&balance.AvailableBalanceCents,
		&balance.ReservedBalanceCents,
		&balance.CreatedAt,
		&balance.UpdatedAt,
	)
	return balance, err
}

func isForeignKeyViolation(err error) bool {
	var pgErr *pgconn.PgError
	return errors.As(err, &pgErr) && pgErr.Code == "23503"
}
