package admin

import (
	"context"
	"errors"
	"fmt"
	"math"
	"strings"

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

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

type WalletAdjustmentParams struct {
	AdminUserID    string
	WalletID       string
	AccountID      string
	Direction      string
	AmountCents    int64
	Currency       string
	Reason         string
	IdempotencyKey string
}

type adjustmentAccount struct {
	ID           string
	UserID       string
	BalanceCents int64
	Status       string
}

func NormalizeWalletAdjustment(direction, currency, reason, idempotencyKey string, amountCents int64) (WalletAdjustmentParams, error) {
	params := WalletAdjustmentParams{
		Direction:      strings.ToLower(strings.TrimSpace(direction)),
		Currency:       domain.NormalizeCurrency(currency),
		Reason:         strings.TrimSpace(reason),
		IdempotencyKey: strings.TrimSpace(idempotencyKey),
		AmountCents:    amountCents,
	}
	if params.Direction != "add" && params.Direction != "subtract" {
		return WalletAdjustmentParams{}, fmt.Errorf("%w: direction must be add or subtract", domain.ErrValidation)
	}
	if err := domain.ValidateAmount(params.AmountCents); err != nil {
		return WalletAdjustmentParams{}, err
	}
	if err := domain.ValidateCurrency(params.Currency); err != nil {
		return WalletAdjustmentParams{}, err
	}
	if len(params.Reason) < 8 || len(params.Reason) > 500 {
		return WalletAdjustmentParams{}, fmt.Errorf("%w: reason must be between 8 and 500 characters", domain.ErrValidation)
	}
	if params.IdempotencyKey == "" || len(params.IdempotencyKey) > 128 {
		return WalletAdjustmentParams{}, fmt.Errorf("%w: Idempotency-Key is required and must be 128 characters or fewer", domain.ErrValidation)
	}
	return params, nil
}

func (r *Repository) adjustWalletBalanceInTx(ctx context.Context, tx pgx.Tx, params WalletAdjustmentParams) (domain.WalletBalanceAdjustment, error) {
	existing, err := findWalletAdjustmentByIdempotencyKey(ctx, tx, params.AdminUserID, params.IdempotencyKey)
	if err == nil {
		if existing.WalletID != params.WalletID ||
			existing.Direction != params.Direction ||
			existing.AmountCents != params.AmountCents ||
			existing.Currency != params.Currency ||
			existing.Reason != params.Reason ||
			(params.AccountID != "" && existing.AccountID != params.AccountID) {
			return domain.WalletBalanceAdjustment{}, fmt.Errorf("%w: Idempotency-Key was already used for a different adjustment", domain.ErrValidation)
		}
		return existing, nil
	}
	if !errors.Is(err, pgx.ErrNoRows) {
		return domain.WalletBalanceAdjustment{}, err
	}

	var adminRole string
	err = tx.QueryRow(ctx, `SELECT role FROM users WHERE id = $1 FOR SHARE`, params.AdminUserID).Scan(&adminRole)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.WalletBalanceAdjustment{}, domain.ErrUnauthorized
	}
	if err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}
	if adminRole != "admin" {
		return domain.WalletBalanceAdjustment{}, domain.ErrForbidden
	}

	var targetUserID, walletStatus string
	err = tx.QueryRow(ctx, `
		SELECT user_id::text, status
		FROM wallets
		WHERE id = $1
		FOR UPDATE
	`, params.WalletID).Scan(&targetUserID, &walletStatus)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.WalletBalanceAdjustment{}, domain.ErrNotFound
	}
	if err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}
	if walletStatus != "active" {
		return domain.WalletBalanceAdjustment{}, fmt.Errorf("%w: wallet must be active", domain.ErrValidation)
	}

	rows, err := tx.Query(ctx, `
		SELECT id::text, user_id::text, balance_cents, status
		FROM accounts
		WHERE wallet_id = $1 AND currency = $2
		ORDER BY id
		FOR UPDATE
	`, params.WalletID, params.Currency)
	if err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}

	accounts := []adjustmentAccount{}
	var totalAccountBalance int64
	for rows.Next() {
		var account adjustmentAccount
		if err := rows.Scan(&account.ID, &account.UserID, &account.BalanceCents, &account.Status); err != nil {
			rows.Close()
			return domain.WalletBalanceAdjustment{}, err
		}
		if account.BalanceCents > math.MaxInt64-totalAccountBalance {
			rows.Close()
			return domain.WalletBalanceAdjustment{}, fmt.Errorf("%w: linked account balance total exceeds supported maximum", domain.ErrValidation)
		}
		totalAccountBalance += account.BalanceCents
		accounts = append(accounts, account)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return domain.WalletBalanceAdjustment{}, err
	}
	rows.Close()

	account, err := selectAdjustmentAccount(accounts, params.AccountID)
	if err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}
	if account.UserID != targetUserID {
		return domain.WalletBalanceAdjustment{}, fmt.Errorf("%w: linked account owner does not match wallet owner", domain.ErrValidation)
	}

	if _, err := tx.Exec(ctx, `
		INSERT INTO wallet_balances (wallet_id, currency)
		SELECT $1, code FROM currencies WHERE code = $2 AND enabled = true
		ON CONFLICT (wallet_id, currency) DO NOTHING
	`, params.WalletID, params.Currency); err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}

	var walletBalanceID string
	var walletAvailable int64
	err = tx.QueryRow(ctx, `
		SELECT id::text, available_balance_cents
		FROM wallet_balances
		WHERE wallet_id = $1 AND currency = $2
		FOR UPDATE
	`, params.WalletID, params.Currency).Scan(&walletBalanceID, &walletAvailable)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.WalletBalanceAdjustment{}, fmt.Errorf("%w: currency is not enabled", domain.ErrValidation)
	}
	if err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}
	if walletAvailable != totalAccountBalance {
		return domain.WalletBalanceAdjustment{}, fmt.Errorf(
			"%w: wallet available balance does not match linked account balances; reconcile before adjusting",
			domain.ErrValidation,
		)
	}

	walletAfter, accountAfter, err := calculateAdjustedBalances(
		walletAvailable,
		account.BalanceCents,
		params.Direction,
		params.AmountCents,
	)
	if err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}

	if _, err := tx.Exec(ctx, `
		UPDATE accounts SET balance_cents = $1, updated_at = now() WHERE id = $2
	`, accountAfter, account.ID); err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}
	if _, err := tx.Exec(ctx, `
		UPDATE wallet_balances
		SET available_balance_cents = $1, updated_at = now()
		WHERE id = $2
	`, walletAfter, walletBalanceID); err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}

	adjustment, err := scanWalletAdjustment(tx.QueryRow(ctx, `
		INSERT INTO wallet_balance_adjustments (
			admin_user_id, target_user_id, wallet_id, wallet_balance_id, account_id, direction,
			amount_cents, currency, reason, idempotency_key, wallet_balance_before_cents,
			wallet_balance_after_cents, account_balance_before_cents, account_balance_after_cents
		)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
		RETURNING id::text, admin_user_id::text, target_user_id::text, wallet_id::text,
			wallet_balance_id::text, account_id::text, direction, amount_cents, currency, reason,
			idempotency_key, wallet_balance_before_cents, wallet_balance_after_cents,
			account_balance_before_cents, account_balance_after_cents, created_at
	`, params.AdminUserID, targetUserID, params.WalletID, walletBalanceID, account.ID, params.Direction,
		params.AmountCents, params.Currency, params.Reason, params.IdempotencyKey, walletAvailable,
		walletAfter, account.BalanceCents, accountAfter))
	if err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}

	if err := r.postWalletAdjustmentLedger(ctx, tx, adjustment); err != nil {
		return domain.WalletBalanceAdjustment{}, err
	}
	return adjustment, nil
}

func selectAdjustmentAccount(accounts []adjustmentAccount, requestedID string) (adjustmentAccount, error) {
	active := []adjustmentAccount{}
	for _, account := range accounts {
		if requestedID != "" && account.ID == requestedID {
			if account.Status != "active" {
				return adjustmentAccount{}, fmt.Errorf("%w: selected account must be active", domain.ErrValidation)
			}
			return account, nil
		}
		if account.Status == "active" {
			active = append(active, account)
		}
	}
	if requestedID != "" {
		return adjustmentAccount{}, fmt.Errorf("%w: selected account is not linked to this wallet and currency", domain.ErrValidation)
	}
	if len(active) == 0 {
		return adjustmentAccount{}, fmt.Errorf("%w: wallet currency requires an active linked account", domain.ErrValidation)
	}
	if len(active) > 1 {
		return adjustmentAccount{}, fmt.Errorf("%w: account_id is required when multiple active accounts use this wallet currency", domain.ErrValidation)
	}
	return active[0], nil
}

func calculateAdjustedBalances(walletBalance, accountBalance int64, direction string, amountCents int64) (int64, int64, error) {
	if err := domain.ValidateAmount(amountCents); err != nil {
		return 0, 0, err
	}
	switch direction {
	case "add":
		if amountCents > math.MaxInt64-walletBalance || amountCents > math.MaxInt64-accountBalance {
			return 0, 0, fmt.Errorf("%w: resulting balance exceeds supported maximum", domain.ErrValidation)
		}
		return walletBalance + amountCents, accountBalance + amountCents, nil
	case "subtract":
		if walletBalance < amountCents || accountBalance < amountCents {
			return 0, 0, domain.ErrInsufficientFunds
		}
		return walletBalance - amountCents, accountBalance - amountCents, nil
	default:
		return 0, 0, fmt.Errorf("%w: direction must be add or subtract", domain.ErrValidation)
	}
}

func (r *Repository) postWalletAdjustmentLedger(ctx context.Context, tx pgx.Tx, adjustment domain.WalletBalanceAdjustment) error {
	accountLedger, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		OwnerUserID:   adjustment.TargetUserID,
		ReferenceType: "account",
		ReferenceID:   adjustment.AccountID,
		Currency:      adjustment.Currency,
		NormalBalance: "credit",
	})
	if err != nil {
		return err
	}
	clearingLedger, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		ReferenceType: "admin_adjustment_clearing",
		ReferenceID:   "wallet_balance_adjustments",
		Currency:      adjustment.Currency,
		NormalBalance: "debit",
	})
	if err != nil {
		return err
	}

	accountDirection, clearingDirection := "credit", "debit"
	if adjustment.Direction == "subtract" {
		accountDirection, clearingDirection = "debit", "credit"
	}
	_, err = r.ledger.Post(ctx, tx, ledger.PostParams{
		EventType:      "wallet.balance_adjusted",
		SourceType:     "wallet_balance_adjustment",
		SourceID:       adjustment.ID,
		IdempotencyKey: adjustment.IdempotencyKey,
		Description:    adjustment.Reason,
		Metadata: map[string]any{
			"admin_user_id":  adjustment.AdminUserID,
			"target_user_id": adjustment.TargetUserID,
			"wallet_id":      adjustment.WalletID,
			"account_id":     adjustment.AccountID,
			"direction":      adjustment.Direction,
		},
		Lines: []ledger.LineParams{
			{
				LedgerAccountID: accountLedger.ID,
				Direction:       accountDirection,
				AmountCents:     adjustment.AmountCents,
				Currency:        adjustment.Currency,
			},
			{
				LedgerAccountID: clearingLedger.ID,
				Direction:       clearingDirection,
				AmountCents:     adjustment.AmountCents,
				Currency:        adjustment.Currency,
			},
		},
	})
	return err
}

func (r *Repository) ListWalletAdjustments(ctx context.Context, walletID string, limit int) ([]domain.WalletBalanceAdjustment, error) {
	return r.listWalletAdjustments(ctx, "wallet_id", walletID, limit)
}

func (r *Repository) ListWalletAdjustmentsForUser(ctx context.Context, userID string, limit int) ([]domain.WalletBalanceAdjustment, error) {
	return r.listWalletAdjustments(ctx, "target_user_id", userID, limit)
}

func (r *Repository) listWalletAdjustments(ctx context.Context, filterColumn, filterID string, limit int) ([]domain.WalletBalanceAdjustment, error) {
	limit = normalizeLimit(limit)
	query := `
		SELECT id::text, admin_user_id::text, target_user_id::text, wallet_id::text,
			wallet_balance_id::text, account_id::text, direction, amount_cents, currency, reason,
			idempotency_key, wallet_balance_before_cents, wallet_balance_after_cents,
			account_balance_before_cents, account_balance_after_cents, created_at
		FROM wallet_balance_adjustments
		WHERE ` + filterColumn + ` = $1
		ORDER BY created_at DESC
		LIMIT $2
	`
	rows, err := r.db.Query(ctx, query, filterID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	adjustments := []domain.WalletBalanceAdjustment{}
	for rows.Next() {
		adjustment, err := scanWalletAdjustment(rows)
		if err != nil {
			return nil, err
		}
		adjustments = append(adjustments, adjustment)
	}
	return adjustments, rows.Err()
}

func findWalletAdjustmentByIdempotencyKey(ctx context.Context, tx pgx.Tx, adminUserID, key string) (domain.WalletBalanceAdjustment, error) {
	return scanWalletAdjustment(tx.QueryRow(ctx, `
		SELECT id::text, admin_user_id::text, target_user_id::text, wallet_id::text,
			wallet_balance_id::text, account_id::text, direction, amount_cents, currency, reason,
			idempotency_key, wallet_balance_before_cents, wallet_balance_after_cents,
			account_balance_before_cents, account_balance_after_cents, created_at
		FROM wallet_balance_adjustments
		WHERE admin_user_id = $1 AND idempotency_key = $2
	`, adminUserID, key))
}

func scanWalletAdjustment(row scanner) (domain.WalletBalanceAdjustment, error) {
	var adjustment domain.WalletBalanceAdjustment
	err := row.Scan(
		&adjustment.ID,
		&adjustment.AdminUserID,
		&adjustment.TargetUserID,
		&adjustment.WalletID,
		&adjustment.WalletBalanceID,
		&adjustment.AccountID,
		&adjustment.Direction,
		&adjustment.AmountCents,
		&adjustment.Currency,
		&adjustment.Reason,
		&adjustment.IdempotencyKey,
		&adjustment.WalletBalanceBeforeCents,
		&adjustment.WalletBalanceAfterCents,
		&adjustment.AccountBalanceBeforeCents,
		&adjustment.AccountBalanceAfterCents,
		&adjustment.CreatedAt,
	)
	return adjustment, err
}

func isAdjustmentRetryable(err error) bool {
	var pgErr *pgconn.PgError
	return errors.As(err, &pgErr) && (pgErr.Code == "40001" || pgErr.Code == "40P01" || pgErr.Code == "23505")
}
