<?php
namespace Bot\Services;

use Bot\Database;
use PDO;

/**
 * Centralized balance manager that keeps user_balances and balance_ledger
 * in sync. All monetary movements (deposits, withdrawals, order locks)
 * should flow through this service to maintain an auditable history.
 */
class BalanceService
{
    private PDO $pdo;

    public function __construct()
    {
        $this->pdo = Database::connection();
    }

    /**
     * @return array{available:string,locked:string}
     */
    public function getBalance(int $userId, string $currency): array
    {
        $currency = strtoupper($currency);
        $stmt = $this->pdo->prepare(
            'SELECT available, locked FROM user_balances WHERE user_id = ? AND currency = ?'
        );
        $stmt->execute([$userId, $currency]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return [
            'available' => (string) ($row['available'] ?? '0'),
            'locked'    => (string) ($row['locked'] ?? '0'),
        ];
    }

    /**
     * @return array<int,array{currency:string,available:string,locked:string}>
     */
    public function listBalances(int $userId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT currency, available, locked FROM user_balances WHERE user_id = ? ORDER BY currency ASC'
        );
        $stmt->execute([$userId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return array_map(static function ($row) {
            return [
                'currency'  => strtoupper($row['currency']),
                'available' => (string) $row['available'],
                'locked'    => (string) $row['locked'],
            ];
        }, $rows);
    }

    public function credit(
        int $userId,
        string $currency,
        string $amount,
        string $category,
        ?string $reference = null,
        array $meta = [],
        ?int $walletId = null
    ): void {
        $this->moveFunds($userId, strtoupper($currency), $amount, 'credit', $category, $reference, $meta, $walletId);
    }

    public function debit(
        int $userId,
        string $currency,
        string $amount,
        string $category,
        ?string $reference = null,
        array $meta = [],
        ?int $walletId = null
    ): void {
        $this->moveFunds($userId, strtoupper($currency), $amount, 'debit', $category, $reference, $meta, $walletId);
    }

    public function lock(int $userId, string $currency, string $amount): void
    {
        $currency = strtoupper($currency);
        $this->pdo->beginTransaction();
        $this->ensureBalanceRow($userId, $currency);
        $stmt = $this->pdo->prepare(
            'UPDATE user_balances SET available = available - :amount, locked = locked + :amount
             WHERE user_id = :user AND currency = :currency AND available >= :amount'
        );
        $stmt->execute([
            ':amount'   => $amount,
            ':user'     => $userId,
            ':currency' => $currency,
        ]);
        if ($stmt->rowCount() === 0) {
            $this->pdo->rollBack();
            throw new \RuntimeException('Insufficient balance.');
        }
        $this->pdo->commit();
    }

    public function unlock(int $userId, string $currency, string $amount): void
    {
        $currency = strtoupper($currency);
        $stmt = $this->pdo->prepare(
            'UPDATE user_balances SET available = available + :amount, locked = locked - :amount
             WHERE user_id = :user AND currency = :currency AND locked >= :amount'
        );
        $stmt->execute([
            ':amount'   => $amount,
            ':user'     => $userId,
            ':currency' => $currency,
        ]);
    }

    public function consumeLocked(int $userId, string $currency, string $amount): void
    {
        $currency = strtoupper($currency);
        $stmt = $this->pdo->prepare(
            'UPDATE user_balances SET locked = locked - :amount
             WHERE user_id = :user AND currency = :currency AND locked >= :amount'
        );
        $stmt->execute([
            ':amount'   => $amount,
            ':user'     => $userId,
            ':currency' => $currency,
        ]);
    }

    private function moveFunds(
        int $userId,
        string $currency,
        string $amount,
        string $direction,
        string $category,
        ?string $reference,
        array $meta,
        ?int $walletId
    ): void {
        if ((float) $amount <= 0) {
            throw new \InvalidArgumentException('Amount must be positive.');
        }
        $this->pdo->beginTransaction();
        $this->ensureBalanceRow($userId, $currency);
        if ($direction === 'credit') {
            $stmt = $this->pdo->prepare(
                'UPDATE user_balances SET available = available + :amount WHERE user_id = :user AND currency = :currency'
            );
        } else {
            $stmt = $this->pdo->prepare(
                'UPDATE user_balances SET available = available - :amount
                 WHERE user_id = :user AND currency = :currency AND available >= :amount'
            );
        }
        $stmt->execute([
            ':amount'   => $amount,
            ':user'     => $userId,
            ':currency' => $currency,
        ]);
        if ($stmt->rowCount() === 0) {
            $this->pdo->rollBack();
            throw new \RuntimeException('Balance update failed.');
        }
        $this->recordLedger($userId, $walletId, $currency, $amount, $direction, $category, $reference, $meta);
        $this->pdo->commit();
    }

    private function ensureBalanceRow(int $userId, string $currency): void
    {
        $stmt = $this->pdo->prepare(
            'INSERT IGNORE INTO user_balances (user_id, currency, available, locked, created_at)
             VALUES (:user, :currency, 0, 0, NOW())'
        );
        $stmt->execute([
            ':user'     => $userId,
            ':currency' => $currency,
        ]);
    }

    private function recordLedger(
        int $userId,
        ?int $walletId,
        string $currency,
        string $amount,
        string $direction,
        string $category,
        ?string $reference,
        array $meta
    ): void {
        $stmt = $this->pdo->prepare(
            'INSERT INTO balance_ledger (user_id, wallet_id, currency, amount, direction, category, reference_id, meta, created_at)
             VALUES (:user, :wallet, :currency, :amount, :direction, :category, :reference, :meta, NOW())'
        );
        $stmt->execute([
            ':user'      => $userId,
            ':wallet'    => $walletId,
            ':currency'  => $currency,
            ':amount'    => $amount,
            ':direction' => $direction,
            ':category'  => $category,
            ':reference' => $reference,
            ':meta'      => $meta ? json_encode($meta, JSON_THROW_ON_ERROR) : null,
        ]);
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function getLedger(int $userId, int $limit = 10): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT currency, amount, direction, category, reference_id, meta, created_at
             FROM balance_ledger
             WHERE user_id = ?
             ORDER BY id DESC
             LIMIT ?'
        );
        $stmt->bindValue(1, $userId, PDO::PARAM_INT);
        $stmt->bindValue(2, $limit, PDO::PARAM_INT);
        $stmt->execute();
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return array_map(static function ($row) {
            return [
                'currency'     => strtoupper($row['currency']),
                'amount'       => $row['amount'],
                'direction'    => $row['direction'],
                'category'     => $row['category'],
                'reference_id' => $row['reference_id'],
                'meta'         => $row['meta'] ? json_decode($row['meta'], true) : null,
                'created_at'   => $row['created_at'],
            ];
        }, $rows);
    }
}
