<?php

declare(strict_types=1);

namespace PhpWA\Runtime;

use InvalidArgumentException;
use PhpWA\Client\ConnectionSession;
use PhpWA\Client\OutgoingReplayWorker;
use PhpWA\Client\OutgoingStanzaStore;
use PhpWA\Client\ProtocolConnectionInterface;
use PhpWA\Client\SessionEvent;
use PhpWA\Client\StanzaRouter;
use PhpWA\Exception\TimeoutException;
use PhpWA\Storage\CredentialRecord;

/** One bounded connect/receive/persist/disconnect cycle for a single account. */
final class AccountWorker
{
    /** @var string */ private $accountId;
    /** @var AccountLockProviderInterface */ private $locks;
    /** @var ProtocolConnectionInterface */ private $connection;
    /** @var ConnectionSession */ private $session;
    /** @var OutgoingStanzaStore */ private $outgoing;
    /** @var callable */ private $routerFactory;

    /** @param callable(CredentialRecord):StanzaRouter $routerFactory */
    public function __construct(
        string $accountId,
        AccountLockProviderInterface $locks,
        ProtocolConnectionInterface $connection,
        ConnectionSession $session,
        OutgoingStanzaStore $outgoing,
        callable $routerFactory
    ) {
        if ($accountId === '' || strlen($accountId) > 512 || preg_match('//u', $accountId) !== 1
            || preg_match('/[\x00-\x1f\x7f]/', $accountId) === 1) {
            throw new InvalidArgumentException('Account worker ID is invalid.');
        }
        $this->accountId = $accountId; $this->locks = $locks; $this->connection = $connection;
        $this->session = $session; $this->outgoing = $outgoing; $this->routerFactory = $routerFactory;
    }

    public function run(float $maximumSeconds = 20.0, int $maximumNodes = 500): WorkerRunResult
    {
        if (!is_finite($maximumSeconds) || $maximumSeconds < 0.1 || $maximumSeconds > 300.0
            || $maximumNodes < 1 || $maximumNodes > 10000) {
            throw new InvalidArgumentException('Account worker run budget is invalid.');
        }
        $lock = $this->locks->acquire($this->accountId);
        if (!$lock instanceof AccountLockInterface) { return WorkerRunResult::busy(); }
        $deadline = Deadline::after($maximumSeconds); $nodes = 0; $replayed = 0; $events = []; $router = null;
        try {
            $this->connection->open($this->session->credentialRecord()->credentials(), $deadline);
            while ($nodes < $maximumNodes && !$deadline->isExpired() && $this->connection->isOpen()) {
                try { $node = $this->connection->receive($deadline); }
                catch (TimeoutException $timeout) { break; }
                $nodes++;
                $sessionEvent = $this->session->process($node, $deadline);
                if ($sessionEvent->type() !== SessionEvent::IGNORED) {
                    $events[] = $sessionEvent;
                    if ($sessionEvent->type() === SessionEvent::ONLINE) {
                        $candidate = ($this->routerFactory)($this->session->credentialRecord());
                        if (!$candidate instanceof StanzaRouter) { throw new \LogicException('Account worker router factory returned an invalid router.'); }
                        $router = $candidate;
                        $replayed += count((new OutgoingReplayWorker($this->accountId, $this->connection, $this->outgoing))->replay($deadline, 100));
                    }
                    if ($sessionEvent->type() === SessionEvent::PAIRING_SUCCESS || $sessionEvent->type() === SessionEvent::DISCONNECTED) { break; }
                    continue;
                }
                if ($router instanceof StanzaRouter) { $router->route($node, $deadline); }
            }
            return new WorkerRunResult(true, $nodes, $replayed, $this->session->state(), $events);
        } finally {
            try { $this->connection->close(); } finally { $lock->release(); }
        }
    }
}
