<?php

declare(strict_types=1);

namespace Australcode\Rewards\Service;

use Australcode\Rewards\Domain\Wallet\Balance;
use Australcode\Rewards\Domain\Wallet\BalanceRepository;
use Australcode\Rewards\Domain\Wallet\Transaction;
use Australcode\Rewards\Domain\Wallet\TransactionRepository;
use Australcode\Rewards\Domain\Wallet\TransactionType;

defined( 'ABSPATH' ) || exit;

/**
 * The single entry point for points movements. Every credit/debit writes one
 * ledger entry AND updates the denormalized balance, keeping them in sync.
 *
 * lifetime_earned / lifetime_redeemed are gross running totals (never decreased
 * by refunds); points_balance is the net spendable balance.
 */
final class WalletService {

	public function __construct(
		private readonly BalanceRepository $balances,
		private readonly TransactionRepository $transactions,
	) {
	}

	/**
	 * Records a points movement. `$points` is signed (positive to grant, negative
	 * to deduct). Returns the persisted ledger entry (with its new id).
	 */
	public function credit(
		int $user_id,
		int $points,
		TransactionType $type,
		string $reason = '',
		?int $order_id = null,
		?string $expires_at = null
	): Transaction {
		$balance     = $this->balances->find( $user_id );
		$new_balance = $balance->points_balance + $points;

		$id = $this->transactions->insert(
			new Transaction(
				id: null,
				user_id: $user_id,
				type: $type,
				points_delta: $points,
				balance_after: $new_balance,
				order_id: $order_id,
				reason: $reason,
				expires_at: $expires_at,
			)
		);

		$earned   = $balance->lifetime_earned + ( TransactionType::Earn === $type && $points > 0 ? $points : 0 );
		$redeemed = $balance->lifetime_redeemed + ( TransactionType::Redeem === $type && $points < 0 ? -$points : 0 );

		$this->balances->save(
			new Balance(
				user_id: $user_id,
				points_balance: $new_balance,
				lifetime_earned: $earned,
				lifetime_redeemed: $redeemed,
				tier_id: $balance->tier_id,
			)
		);

		return new Transaction(
			id: $id,
			user_id: $user_id,
			type: $type,
			points_delta: $points,
			balance_after: $new_balance,
			order_id: $order_id,
			reason: $reason,
			expires_at: $expires_at,
		);
	}

	public function get_balance( int $user_id ): Balance {
		return $this->balances->find( $user_id );
	}

	/**
	 * @return Transaction[]
	 */
	public function get_transactions( int $user_id, int $limit = 20, int $offset = 0 ): array {
		return $this->transactions->for_user( $user_id, $limit, $offset );
	}

	public function count_transactions( int $user_id ): int {
		return $this->transactions->count_for_user( $user_id );
	}

	/**
	 * Total active points earned for an order — the authoritative awarded amount.
	 * Sums the active Earn entries; 0 before award or once the order is reverted.
	 */
	public function points_earned_for_order( int $order_id ): int {
		$total = 0;
		foreach ( $this->transactions->active_for_order( $order_id ) as $entry ) {
			if ( TransactionType::Earn === $entry->type && $entry->points_delta > 0 ) {
				$total += $entry->points_delta;
			}
		}
		return $total;
	}

	/**
	 * Reverts the points earned for an order (on refund/cancel): marks the active
	 * earn entries as reverted and adds a compensating Refund entry. Idempotent at
	 * the data level — if there are no active earn entries, it does nothing.
	 *
	 * @return int Points reverted (0 if none).
	 */
	public function revert_order( int $order_id, int $user_id ): int {
		$total = 0;
		$ids   = array();

		foreach ( $this->transactions->active_for_order( $order_id ) as $entry ) {
			if ( TransactionType::Earn === $entry->type && $entry->points_delta > 0 ) {
				$total += $entry->points_delta;
				if ( null !== $entry->id ) {
					$ids[] = $entry->id;
				}
			}
		}

		if ( $total <= 0 ) {
			return 0;
		}

		$this->transactions->mark_reverted( $ids );
		$this->credit(
			$user_id,
			-$total,
			TransactionType::Refund,
			/* translators: %d: order number. */
			sprintf( 'Reverted points from order #%d', $order_id ),
			$order_id
		);

		return $total;
	}
}
