<?php

declare(strict_types=1);

namespace PhpWA\Client;

use PhpWA\Exception\ProtocolException;
use PhpWA\Protocol\Binary\BinaryCodec;
use PhpWA\Protocol\Binary\BinaryNode;
use PhpWA\Protocol\Proto\Message;
use PhpWA\Storage\AccountStoreInterface;
use PhpWA\Storage\AccountTransactionInterface;

final class OutgoingStanzaStore
{
    private const COLLECTION = 'outgoing_stanza';
    private const RECENT = 'outgoing_recent';
    private const MESSAGE = 'outgoing_message';
    /** @var AccountStoreInterface */ private $accounts;
    /** @var BinaryCodec */ private $binary;
    public function __construct(AccountStoreInterface $accounts, ?BinaryCodec $binary = null) { $this->accounts = $accounts; $this->binary = $binary ?? new BinaryCodec(); }
    public function stageInTransaction(AccountTransactionInterface $transaction, string $messageId, BinaryNode $stanza): void
    {
        if ($stanza->tag() !== 'message' || $stanza->attribute('id') !== $messageId) {
            throw new ProtocolException('Only a matching message stanza can be staged for replay.');
        }
        $transaction->put(self::COLLECTION, $messageId, $this->binary->encode($stanza), 0);
    }

    public function stageMessageInTransaction(
        AccountTransactionInterface $transaction,
        string $messageId,
        string $to,
        bool $group,
        Message $message,
        int $createdAt
    ): void {
        $stored = new StoredOutgoingMessage($messageId, $to, $group, $message, $createdAt);
        try {
            $json = json_encode([
                'v' => 1, 'id' => $stored->id(), 'to' => $stored->to(), 'group' => $stored->isGroup(),
                'message' => base64_encode($stored->message()->serializeToString()), 'created_at' => $stored->createdAt(),
            ], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
        } catch (\Throwable $error) { throw new ProtocolException('Unable to serialize an outgoing retry message.', 0, $error); }
        if (!is_string($json)) { throw new ProtocolException('Unable to serialize an outgoing retry message.'); }
        $transaction->put(self::MESSAGE, $messageId, $json, 0);
    }

    public function stageRetryInTransaction(AccountTransactionInterface $transaction, string $messageId, BinaryNode $stanza): void
    {
        if ($stanza->tag() !== 'message' || $stanza->attribute('id') !== $messageId) {
            throw new ProtocolException('Only a matching retry message stanza can be staged.');
        }
        $known = $transaction->get(self::COLLECTION, $messageId);
        $transaction->put(self::COLLECTION, $messageId, $this->binary->encode($stanza), $known === null ? 0 : $known->revision());
    }
    /** @return array<string,BinaryNode> */
    public function pending(string $accountId, int $limit = 100): array
    {
        return $this->accounts->transaction($accountId, function (AccountTransactionInterface $tx) use ($limit): array {
            $result = [];
            foreach ($tx->list(self::COLLECTION, $limit) as $id => $stored) {
                $node = $this->binary->decode($stored->value());
                if ($node->tag() !== 'message' || $node->attribute('id') !== $id) {
                    throw new ProtocolException('Stored outgoing stanza is malformed or belongs to another ID.');
                }
                $result[$id] = $node;
            }
            return $result;
        });
    }
    public function complete(string $accountId, string $messageId): void
    {
        $this->accounts->transaction($accountId, static function (AccountTransactionInterface $tx) use ($messageId): void {
            $stored = $tx->get(self::COLLECTION, $messageId);
            if ($stored !== null) {
                // Keep the exact ciphertext available for bounded retry-receipt
                // handling after the server ACK removes it from the live queue.
                $tx->put(self::RECENT, $messageId, $stored->value());
                $tx->delete(self::COLLECTION, $messageId, $stored->revision());
            }
        });
    }

    /**
     * WhatsApp прислал окончательный отрицательный ACK. Такой stanza нельзя
     * replay-ить: сервер уже сообщил, что не примет именно эту отправку.
     */
    public function reject(string $accountId, string $messageId): void
    {
        $this->accounts->transaction($accountId, static function (AccountTransactionInterface $tx) use ($messageId): void {
            $stanza = $tx->get(self::COLLECTION, $messageId);
            if ($stanza !== null) { $tx->delete(self::COLLECTION, $messageId, $stanza->revision()); }
            $message = $tx->get(self::MESSAGE, $messageId);
            if ($message !== null) { $tx->delete(self::MESSAGE, $messageId, $message->revision()); }
            $recent = $tx->get(self::RECENT, $messageId);
            if ($recent !== null) { $tx->delete(self::RECENT, $messageId, $recent->revision()); }
        });
    }

    public function hasPending(string $accountId): bool
    {
        return $this->accounts->transaction($accountId, static function (AccountTransactionInterface $tx): bool {
            return $tx->list(self::COLLECTION, 1) !== [];
        });
    }

    public function findForRetry(string $accountId, string $messageId): ?BinaryNode
    {
        return $this->accounts->transaction($accountId, function (AccountTransactionInterface $tx) use ($messageId): ?BinaryNode {
            $stored = $tx->get(self::COLLECTION, $messageId) ?? $tx->get(self::RECENT, $messageId);
            if ($stored === null) { return null; }
            $node = $this->binary->decode($stored->value());
            if ($node->tag() !== 'message' || $node->attribute('id') !== $messageId) {
                throw new ProtocolException('Stored retry stanza is malformed or belongs to another ID.');
            }
            return $node;
        });
    }

    public function retryMessage(string $accountId, string $messageId): ?StoredOutgoingMessage
    {
        return $this->accounts->transaction($accountId, static function (AccountTransactionInterface $tx) use ($messageId): ?StoredOutgoingMessage {
            $stored = $tx->get(self::MESSAGE, $messageId);
            if ($stored === null) { return null; }
            try { $data = json_decode($stored->value(), true, 16, JSON_THROW_ON_ERROR); }
            catch (\Throwable $error) { throw new ProtocolException('Stored outgoing retry message is malformed.', 0, $error); }
            if (!is_array($data) || array_keys($data) !== ['v', 'id', 'to', 'group', 'message', 'created_at']
                || $data['v'] !== 1 || !is_string($data['id']) || !is_string($data['to']) || !is_bool($data['group'])
                || !is_string($data['message']) || !is_int($data['created_at']) || !hash_equals($messageId, $data['id'])) {
                throw new ProtocolException('Stored outgoing retry message has an invalid shape.');
            }
            $bytes = base64_decode($data['message'], true);
            if (!is_string($bytes) || $bytes === '') { throw new ProtocolException('Stored outgoing retry protobuf is invalid.'); }
            $message = new Message();
            try { $message->mergeFromString($bytes); }
            catch (\Throwable $error) { throw new ProtocolException('Stored outgoing retry protobuf is malformed.', 0, $error); }
            try { return new StoredOutgoingMessage($data['id'], $data['to'], $data['group'], $message, $data['created_at']); }
            catch (\Throwable $error) { throw new ProtocolException('Stored outgoing retry message violates its bounds.', 0, $error); }
        });
    }

    public function forgetRecent(string $accountId, string $messageId): void
    {
        $this->accounts->transaction($accountId, static function (AccountTransactionInterface $tx) use ($messageId): void {
            $stored = $tx->get(self::RECENT, $messageId);
            if ($stored !== null) { $tx->delete(self::RECENT, $messageId, $stored->revision()); }
            $message = $tx->get(self::MESSAGE, $messageId);
            if ($message !== null) { $tx->delete(self::MESSAGE, $messageId, $message->revision()); }
        });
    }

    /** Deletes bounded old retry material while retaining pending stanzas. */
    public function pruneRecent(string $accountId, int $createdBefore, int $limit = 1000): int
    {
        if ($createdBefore < 0 || $limit < 1 || $limit > 10000) { throw new \InvalidArgumentException('Outgoing retry prune bounds are invalid.'); }
        return $this->accounts->transaction($accountId, static function (AccountTransactionInterface $tx) use ($createdBefore, $limit): int {
            $removed = 0;
            foreach ($tx->list(self::MESSAGE, $limit) as $id => $stored) {
                try { $data = json_decode($stored->value(), true, 8, JSON_THROW_ON_ERROR); }
                catch (\Throwable $error) { throw new ProtocolException('Stored outgoing retry metadata is malformed.', 0, $error); }
                if (!is_array($data) || !isset($data['created_at']) || !is_int($data['created_at'])) {
                    throw new ProtocolException('Stored outgoing retry timestamp is malformed.');
                }
                if ($data['created_at'] >= $createdBefore || $tx->get(self::COLLECTION, $id) !== null) { continue; }
                $tx->delete(self::MESSAGE, $id, $stored->revision());
                $recent = $tx->get(self::RECENT, $id);
                if ($recent !== null) { $tx->delete(self::RECENT, $id, $recent->revision()); }
                $removed++;
            }
            return $removed;
        });
    }
}
