package admin

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/jackc/pgx/v5"

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

const adminWalletAdjustmentLimitCents int64 = 10_000_000

type WalletAdjustmentDecisionParams struct {
	RequestID    string
	ActorAdminID string
	Action       string
	DecisionNote string
}

func (r *Repository) CreateWalletAdjustmentRequest(ctx context.Context, params WalletAdjustmentParams) (domain.WalletBalanceAdjustmentRequest, error) {
	var request domain.WalletBalanceAdjustmentRequest
	var err error
	for attempt := 0; attempt < 3; attempt++ {
		request, err = r.createWalletAdjustmentRequestOnce(ctx, params)
		if !isAdjustmentRetryable(err) {
			return request, err
		}
		time.Sleep(time.Duration(attempt+1) * 25 * time.Millisecond)
	}
	return request, err
}

func (r *Repository) createWalletAdjustmentRequestOnce(ctx context.Context, params WalletAdjustmentParams) (domain.WalletBalanceAdjustmentRequest, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}
	defer tx.Rollback(ctx)

	existing, err := findWalletAdjustmentRequestByIdempotencyKey(ctx, tx, params.AdminUserID, params.IdempotencyKey)
	if err == nil {
		if !requestMatchesParams(existing, params) {
			return domain.WalletBalanceAdjustmentRequest{}, fmt.Errorf("%w: Idempotency-Key was already used for a different request", domain.ErrValidation)
		}
		return existing, tx.Commit(ctx)
	}
	if !errors.Is(err, pgx.ErrNoRows) {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}
	if err := requireAdminWithinAdjustmentLimit(ctx, tx, params.AdminUserID, params.AmountCents); err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}

	targetUserID, accountID, err := validateWalletAdjustmentTarget(ctx, tx, params)
	if err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}

	var requestID string
	err = tx.QueryRow(ctx, `
		INSERT INTO wallet_balance_adjustment_requests (
			requester_admin_user_id, target_user_id, wallet_id, account_id, direction,
			amount_cents, currency, reason, idempotency_key
		)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
		RETURNING id::text
	`, params.AdminUserID, targetUserID, params.WalletID, accountID, params.Direction,
		params.AmountCents, params.Currency, params.Reason, params.IdempotencyKey).Scan(&requestID)
	if err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}

	request, err := findWalletAdjustmentRequest(ctx, tx, requestID, false)
	if err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}
	return request, nil
}

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

	var balanceExists bool
	if err := tx.QueryRow(ctx, `
		SELECT EXISTS (
			SELECT 1 FROM wallet_balances WHERE wallet_id = $1 AND currency = $2
		)
	`, params.WalletID, params.Currency).Scan(&balanceExists); err != nil {
		return "", "", err
	}
	if !balanceExists {
		return "", "", fmt.Errorf("%w: wallet currency balance does not exist", 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
	`, params.WalletID, params.Currency)
	if err != nil {
		return "", "", err
	}
	defer rows.Close()

	accounts := []adjustmentAccount{}
	for rows.Next() {
		var account adjustmentAccount
		if err := rows.Scan(&account.ID, &account.UserID, &account.BalanceCents, &account.Status); err != nil {
			return "", "", err
		}
		accounts = append(accounts, account)
	}
	if err := rows.Err(); err != nil {
		return "", "", err
	}
	account, err := selectAdjustmentAccount(accounts, params.AccountID)
	if err != nil {
		return "", "", err
	}
	if account.UserID != targetUserID {
		return "", "", fmt.Errorf("%w: linked account owner does not match wallet owner", domain.ErrValidation)
	}
	return targetUserID, account.ID, nil
}

func (r *Repository) ListWalletAdjustmentRequests(ctx context.Context, status string, limit int) ([]domain.WalletBalanceAdjustmentRequest, error) {
	status = strings.ToLower(strings.TrimSpace(status))
	if status != "" && status != "pending" && status != "approved" && status != "rejected" && status != "canceled" {
		return nil, fmt.Errorf("%w: invalid request status", domain.ErrValidation)
	}
	limit = normalizeLimit(limit)

	rows, err := r.db.Query(ctx, walletAdjustmentRequestSelect+`
		WHERE (NULLIF($1, '') IS NULL OR req.status = $1)
		ORDER BY
			CASE WHEN req.status = 'pending' THEN 0 ELSE 1 END,
			req.created_at DESC
		LIMIT $2
	`, status, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	requests := []domain.WalletBalanceAdjustmentRequest{}
	for rows.Next() {
		request, err := scanWalletAdjustmentRequest(rows)
		if err != nil {
			return nil, err
		}
		requests = append(requests, request)
	}
	return requests, rows.Err()
}

func (r *Repository) ListWalletAdjustmentRequestsForUser(ctx context.Context, userID string, limit int) ([]domain.WalletBalanceAdjustmentRequest, error) {
	limit = normalizeLimit(limit)
	rows, err := r.db.Query(ctx, walletAdjustmentRequestSelect+`
		WHERE req.target_user_id = $1
		ORDER BY req.created_at DESC
		LIMIT $2
	`, userID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	requests := []domain.WalletBalanceAdjustmentRequest{}
	for rows.Next() {
		request, err := scanWalletAdjustmentRequest(rows)
		if err != nil {
			return nil, err
		}
		requests = append(requests, request)
	}
	return requests, rows.Err()
}

func (r *Repository) DecideWalletAdjustmentRequest(ctx context.Context, params WalletAdjustmentDecisionParams) (domain.WalletBalanceAdjustmentRequest, error) {
	var request domain.WalletBalanceAdjustmentRequest
	var err error
	for attempt := 0; attempt < 3; attempt++ {
		request, err = r.decideWalletAdjustmentRequestOnce(ctx, params)
		if !isAdjustmentRetryable(err) {
			return request, err
		}
		time.Sleep(time.Duration(attempt+1) * 25 * time.Millisecond)
	}
	return request, err
}

func (r *Repository) decideWalletAdjustmentRequestOnce(ctx context.Context, params WalletAdjustmentDecisionParams) (domain.WalletBalanceAdjustmentRequest, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}
	defer tx.Rollback(ctx)

	request, err := findWalletAdjustmentRequest(ctx, tx, params.RequestID, true)
	if err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}
	note, err := validateWalletAdjustmentDecision(request, params.ActorAdminID, params.Action, params.DecisionNote)
	if err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}

	switch params.Action {
	case "approve":
		if err := requireAdminWithinAdjustmentLimit(ctx, tx, params.ActorAdminID, request.AmountCents); err != nil {
			return domain.WalletBalanceAdjustmentRequest{}, err
		}
		if err := requireAdminWithinAdjustmentLimit(ctx, tx, request.RequesterAdminUserID, request.AmountCents); err != nil {
			return domain.WalletBalanceAdjustmentRequest{}, fmt.Errorf("%w: requester is no longer authorized for this amount", err)
		}
		adjustment, err := r.adjustWalletBalanceInTx(ctx, tx, WalletAdjustmentParams{
			AdminUserID:    params.ActorAdminID,
			WalletID:       request.WalletID,
			AccountID:      request.AccountID,
			Direction:      request.Direction,
			AmountCents:    request.AmountCents,
			Currency:       request.Currency,
			Reason:         request.Reason,
			IdempotencyKey: "approval:" + request.ID,
		})
		if err != nil {
			return domain.WalletBalanceAdjustmentRequest{}, err
		}
		if _, err := tx.Exec(ctx, `
			UPDATE wallet_balance_adjustment_requests
			SET status = 'approved', reviewer_admin_user_id = $1, decision_note = NULLIF($2, ''),
				adjustment_id = $3, decided_at = now()
			WHERE id = $4
		`, params.ActorAdminID, note, adjustment.ID, request.ID); err != nil {
			return domain.WalletBalanceAdjustmentRequest{}, err
		}
	case "reject":
		if err := requireLiveAdmin(ctx, tx, params.ActorAdminID); err != nil {
			return domain.WalletBalanceAdjustmentRequest{}, err
		}
		if _, err := tx.Exec(ctx, `
			UPDATE wallet_balance_adjustment_requests
			SET status = 'rejected', reviewer_admin_user_id = $1, decision_note = $2, decided_at = now()
			WHERE id = $3
		`, params.ActorAdminID, note, request.ID); err != nil {
			return domain.WalletBalanceAdjustmentRequest{}, err
		}
	case "cancel":
		if err := requireLiveAdmin(ctx, tx, params.ActorAdminID); err != nil {
			return domain.WalletBalanceAdjustmentRequest{}, err
		}
		if _, err := tx.Exec(ctx, `
			UPDATE wallet_balance_adjustment_requests
			SET status = 'canceled', decision_note = $1, decided_at = now()
			WHERE id = $2
		`, note, request.ID); err != nil {
			return domain.WalletBalanceAdjustmentRequest{}, err
		}
	default:
		return domain.WalletBalanceAdjustmentRequest{}, fmt.Errorf("%w: invalid decision action", domain.ErrValidation)
	}

	updated, err := findWalletAdjustmentRequest(ctx, tx, request.ID, false)
	if err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return domain.WalletBalanceAdjustmentRequest{}, err
	}
	return updated, nil
}

func validateWalletAdjustmentDecision(request domain.WalletBalanceAdjustmentRequest, actorID, action, rawNote string) (string, error) {
	action = strings.ToLower(strings.TrimSpace(action))
	note := strings.TrimSpace(rawNote)
	if request.Status != "pending" {
		return "", fmt.Errorf("%w: only pending requests can be decided", domain.ErrValidation)
	}
	if action == "approve" || action == "reject" {
		if actorID == request.RequesterAdminUserID {
			return "", fmt.Errorf("%w: requester cannot approve or reject their own request", domain.ErrForbidden)
		}
	}
	if action == "cancel" && actorID != request.RequesterAdminUserID {
		return "", fmt.Errorf("%w: only the requester can cancel this request", domain.ErrForbidden)
	}
	if action != "approve" && action != "reject" && action != "cancel" {
		return "", fmt.Errorf("%w: action must be approve, reject or cancel", domain.ErrValidation)
	}
	if (action == "reject" || action == "cancel") && (len(note) < 8 || len(note) > 500) {
		return "", fmt.Errorf("%w: decision_note must be between 8 and 500 characters", domain.ErrValidation)
	}
	if action == "approve" && len(note) > 500 {
		return "", fmt.Errorf("%w: decision_note must be 500 characters or fewer", domain.ErrValidation)
	}
	return note, nil
}

func requireAdminWithinAdjustmentLimit(ctx context.Context, tx pgx.Tx, adminUserID string, amountCents int64) error {
	role, err := liveAdminRole(ctx, tx, adminUserID)
	if err != nil {
		return err
	}
	limit, ok := walletAdjustmentLimitForRole(role)
	if !ok {
		return domain.ErrForbidden
	}
	if amountCents > limit {
		return fmt.Errorf("%w: amount exceeds the %s role limit of %d cents", domain.ErrValidation, role, limit)
	}
	return nil
}

func requireLiveAdmin(ctx context.Context, tx pgx.Tx, adminUserID string) error {
	_, err := liveAdminRole(ctx, tx, adminUserID)
	return err
}

func liveAdminRole(ctx context.Context, tx pgx.Tx, adminUserID string) (string, error) {
	var role string
	err := tx.QueryRow(ctx, `SELECT role FROM users WHERE id = $1 FOR SHARE`, adminUserID).Scan(&role)
	if errors.Is(err, pgx.ErrNoRows) {
		return "", domain.ErrUnauthorized
	}
	if err != nil {
		return "", err
	}
	if role != "admin" {
		return "", domain.ErrForbidden
	}
	return role, nil
}

func walletAdjustmentLimitForRole(role string) (int64, bool) {
	switch role {
	case "admin":
		return adminWalletAdjustmentLimitCents, true
	default:
		return 0, false
	}
}

func requestMatchesParams(request domain.WalletBalanceAdjustmentRequest, params WalletAdjustmentParams) bool {
	return request.WalletID == params.WalletID &&
		(params.AccountID == "" || request.AccountID == params.AccountID) &&
		request.Direction == params.Direction &&
		request.AmountCents == params.AmountCents &&
		request.Currency == params.Currency &&
		request.Reason == params.Reason
}

const walletAdjustmentRequestSelect = `
	SELECT req.id::text, req.requester_admin_user_id::text, requester.full_name,
		COALESCE(req.reviewer_admin_user_id::text, ''), COALESCE(reviewer.full_name, ''),
		req.target_user_id::text, target.full_name, req.wallet_id::text, req.account_id::text,
		req.direction, req.amount_cents, req.currency, req.reason, req.status,
		COALESCE(req.decision_note, ''), COALESCE(req.adjustment_id::text, ''), req.idempotency_key,
		req.decided_at, req.created_at, req.updated_at
	FROM wallet_balance_adjustment_requests req
	JOIN users requester ON requester.id = req.requester_admin_user_id
	LEFT JOIN users reviewer ON reviewer.id = req.reviewer_admin_user_id
	JOIN users target ON target.id = req.target_user_id
`

func findWalletAdjustmentRequest(ctx context.Context, tx pgx.Tx, requestID string, forUpdate bool) (domain.WalletBalanceAdjustmentRequest, error) {
	query := walletAdjustmentRequestSelect + ` WHERE req.id = $1`
	if forUpdate {
		query += ` FOR UPDATE OF req`
	}
	request, err := scanWalletAdjustmentRequest(tx.QueryRow(ctx, query, requestID))
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.WalletBalanceAdjustmentRequest{}, domain.ErrNotFound
	}
	return request, err
}

func findWalletAdjustmentRequestByIdempotencyKey(ctx context.Context, tx pgx.Tx, requesterID, key string) (domain.WalletBalanceAdjustmentRequest, error) {
	return scanWalletAdjustmentRequest(tx.QueryRow(ctx, walletAdjustmentRequestSelect+`
		WHERE req.requester_admin_user_id = $1 AND req.idempotency_key = $2
	`, requesterID, key))
}

func scanWalletAdjustmentRequest(row scanner) (domain.WalletBalanceAdjustmentRequest, error) {
	var request domain.WalletBalanceAdjustmentRequest
	var decidedAt sql.NullTime
	err := row.Scan(
		&request.ID,
		&request.RequesterAdminUserID,
		&request.RequesterName,
		&request.ReviewerAdminUserID,
		&request.ReviewerName,
		&request.TargetUserID,
		&request.TargetUserName,
		&request.WalletID,
		&request.AccountID,
		&request.Direction,
		&request.AmountCents,
		&request.Currency,
		&request.Reason,
		&request.Status,
		&request.DecisionNote,
		&request.AdjustmentID,
		&request.IdempotencyKey,
		&decidedAt,
		&request.CreatedAt,
		&request.UpdatedAt,
	)
	if decidedAt.Valid {
		request.DecidedAt = &decidedAt.Time
	}
	return request, err
}
