<?php

declare(strict_types=1);

namespace PhpWA\Client;

use InvalidArgumentException;
use PhpWA\Exception\ProtocolException;
use PhpWA\Integration\EventType;
use PhpWA\Protocol\Binary\BinaryNode;
use PhpWA\Protocol\Jid\Jid;
use PhpWA\Runtime\Deadline;
use PhpWA\Storage\AccountStoreInterface;
use PhpWA\Storage\AccountTransactionInterface;

/** Durably hands delivery/read state to the application before ACKing it. */
final class ReceiptProcessor
{
    /** @var string */ private $accountId;
    /** @var ProtocolConnectionInterface */ private $connection;
    /** @var AccountStoreInterface */ private $accounts;
    /** @var callable */ private $clock;

    public function __construct(string $accountId, ProtocolConnectionInterface $connection, AccountStoreInterface $accounts, ?callable $clock = null)
    {
        if ($accountId === '' || strlen($accountId) > 512 || preg_match('//u', $accountId) !== 1) {
            throw new InvalidArgumentException('Receipt processor account ID is invalid.');
        }
        $this->accountId = $accountId; $this->connection = $connection; $this->accounts = $accounts;
        $this->clock = $clock ?? static function (): int { return time(); };
    }

    public function process(BinaryNode $node, Deadline $deadline): bool
    {
        if ($node->tag() !== 'receipt') { return false; }
        if ($node->attribute('type') === 'retry') { return false; }
        $parsed = $this->parse($node); $now = ($this->clock)();
        if (!is_int($now) || $now < 0) { throw new ProtocolException('Receipt processor clock returned an invalid timestamp.'); }
        $dedupe = 'receipt:' . hash('sha256', json_encode($parsed, JSON_UNESCAPED_SLASHES));
        $payload = json_encode($parsed + ['schema_version' => 1], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
        if (!is_string($payload)) { throw new ProtocolException('Unable to serialize receipt event.'); }
        $this->accounts->transaction($this->accountId, static function (AccountTransactionInterface $tx) use ($dedupe, $payload, $now): void {
            $tx->stageIncoming($dedupe, EventType::MESSAGE_RECEIPT, $payload, $now);
        });
        $attrs = ['class' => 'receipt', 'id' => $parsed['message_ids'][0], 'to' => $parsed['from_jid']];
        foreach (['participant', 'recipient', 'type'] as $name) {
            $value = $node->attribute($name); if ($value !== null) { $attrs[$name] = $value; }
        }
        $this->connection->send(new BinaryNode('ack', $attrs), $deadline);
        return true;
    }

    /** @return array<string,mixed> */
    private function parse(BinaryNode $node): array
    {
        $from = Jid::parse($node->attribute('from'));
        if (!$from instanceof Jid) { throw new ProtocolException('Receipt source JID is invalid.'); }
        $type = $node->attribute('type') ?? '';
        $types = ['', 'sender', 'read', 'read-self', 'played', 'played-self', 'inactive', 'server-error', 'peer_msg', 'hist_sync'];
        if (!in_array($type, $types, true)) {
            throw new ProtocolException('Receipt type is unsupported.');
        }
        $ids = [$this->messageId($node->attribute('id'))];
        $list = $node->child('list');
        if ($list instanceof BinaryNode) {
            foreach ($list->childrenNamed('item') as $item) {
                $id = $this->messageId($item->attribute('id'));
                if (in_array($id, $ids, true)) { throw new ProtocolException('Receipt contains duplicate message IDs.'); }
                $ids[] = $id;
                if (count($ids) > 1000) { throw new ProtocolException('Receipt message list exceeds its limit.'); }
            }
        }
        $timestamp = $this->timestamp($node->attribute('t'));
        $participant = $this->optionalJid($node->attribute('participant'), 'participant');
        $recipient = $this->optionalJid($node->attribute('recipient'), 'recipient');
        return [
            'message_ids' => $ids, 'type' => $type === '' ? 'delivery' : $type,
            'from_jid' => (string) $from, 'participant_jid' => $participant,
            'recipient_jid' => $recipient, 'timestamp' => $timestamp,
        ];
    }

    private function messageId(?string $id): string
    {
        if ($id === null || preg_match('/\A[A-Za-z0-9._:-]{1,256}\z/D', $id) !== 1) { throw new ProtocolException('Receipt message ID is invalid.'); }
        return $id;
    }

    private function timestamp(?string $value): int
    {
        if ($value === null) { return 0; }
        if (preg_match('/^(0|[1-9][0-9]{0,18})$/D', $value) !== 1 || (float) $value > PHP_INT_MAX) {
            throw new ProtocolException('Receipt timestamp is invalid.');
        }
        return (int) $value;
    }

    private function optionalJid(?string $value, string $field): ?string
    {
        if ($value === null) { return null; }
        $jid = Jid::parse($value);
        if (!$jid instanceof Jid) { throw new ProtocolException('Receipt ' . $field . ' JID is invalid.'); }
        return (string) $jid;
    }
}
