<?php

declare(strict_types=1);

namespace PhpWA\Client;

use InvalidArgumentException;
use PhpWA\Exception\ProtocolException;
use PhpWA\Protocol\Proto\Message\HistorySyncNotification;

/** Durable, encrypted-at-rest description of one history-sync chunk. */
final class HistorySyncJob
{
    private const SCHEMA_VERSION = 1;

    /** @var string */ private $sourceMessageId;
    /** @var string */ private $notificationBytes;
    /** @var int */ private $cursor;
    /** @var int */ private $receivedAt;

    public function __construct(string $sourceMessageId, string $notificationBytes, int $cursor, int $receivedAt)
    {
        if ($sourceMessageId === '' || strlen($sourceMessageId) > 512
            || preg_match('/[\x00-\x1f\x7f]/', $sourceMessageId) === 1
            || $notificationBytes === '' || strlen($notificationBytes) > 12582912
            || $cursor < 0 || $receivedAt < 0) {
            throw new InvalidArgumentException('History-sync job is invalid.');
        }
        $this->sourceMessageId = $sourceMessageId;
        $this->notificationBytes = $notificationBytes;
        $this->cursor = $cursor;
        $this->receivedAt = $receivedAt;
    }

    public static function fromNotification(string $sourceMessageId, HistorySyncNotification $notification, int $receivedAt): self
    {
        try { $bytes = $notification->serializeToString(); }
        catch (\Throwable $error) { throw new ProtocolException('Unable to serialize the history-sync notification.', 0, $error); }
        return new self($sourceMessageId, $bytes, 0, $receivedAt);
    }

    public static function decode(string $json): self
    {
        try { $data = json_decode($json, true, 32, JSON_THROW_ON_ERROR); }
        catch (\Throwable $error) { throw new ProtocolException('Stored history-sync job is malformed.', 0, $error); }
        if (!is_array($data) || array_keys($data) !== ['v', 'source_message_id', 'notification', 'cursor', 'received_at']
            || $data['v'] !== self::SCHEMA_VERSION || !is_string($data['source_message_id'])
            || !is_string($data['notification']) || !is_int($data['cursor']) || !is_int($data['received_at'])) {
            throw new ProtocolException('Stored history-sync job has an invalid shape.');
        }
        $bytes = base64_decode($data['notification'], true);
        if (!is_string($bytes)) { throw new ProtocolException('Stored history-sync notification is not valid base64.'); }
        try { return new self($data['source_message_id'], $bytes, $data['cursor'], $data['received_at']); }
        catch (InvalidArgumentException $error) { throw new ProtocolException('Stored history-sync job violates its bounds.', 0, $error); }
    }

    public function encode(): string
    {
        try {
            $json = json_encode([
                'v' => self::SCHEMA_VERSION,
                'source_message_id' => $this->sourceMessageId,
                'notification' => base64_encode($this->notificationBytes),
                'cursor' => $this->cursor,
                'received_at' => $this->receivedAt,
            ], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
        } catch (\Throwable $error) {
            throw new ProtocolException('Unable to serialize the history-sync job.', 0, $error);
        }
        if (!is_string($json)) { throw new ProtocolException('Unable to serialize the history-sync job.'); }
        return $json;
    }

    public function notification(): HistorySyncNotification
    {
        $notification = new HistorySyncNotification();
        try { $notification->mergeFromString($this->notificationBytes); }
        catch (\Throwable $error) { throw new ProtocolException('Stored history-sync notification protobuf is malformed.', 0, $error); }
        return $notification;
    }

    public function withCursor(int $cursor): self
    {
        return new self($this->sourceMessageId, $this->notificationBytes, $cursor, $this->receivedAt);
    }

    public function key(): string { return hash('sha256', $this->sourceMessageId); }
    public function sourceMessageId(): string { return $this->sourceMessageId; }
    public function cursor(): int { return $this->cursor; }
    public function receivedAt(): int { return $this->receivedAt; }
}
