<?php

declare(strict_types=1);

namespace PhpWA\Crypto\Signal;

use PhpWA\Crypto\RandomSourceInterface;
use PhpWA\Crypto\SystemRandomSource;
use PhpWA\Exception\SignalException;
use PhpWA\Protocol\ProtobufCodec;
use PhpWA\Protocol\Proto\SenderKeyDistributionMessage;

final class GroupSessionBuilder
{
    private const VERSION_BYTE = 0x33;
    private const MAX_WIRE_BYTES = 1048576;
    /** @var SignalProtocolStoreInterface */ private $store;
    /** @var RandomSourceInterface */ private $random;
    /** @var SignalCurve */ private $curve;
    /** @var ProtobufCodec */ private $protobuf;

    public function __construct(SignalProtocolStoreInterface $store, ?RandomSourceInterface $random = null, ?SignalCurve $curve = null)
    {
        $this->store = $store; $this->random = $random ?? new SystemRandomSource(); $this->curve = $curve ?? new SignalCurve($this->random); $this->protobuf = new ProtobufCodec(self::MAX_WIRE_BYTES);
    }

    public function create(string $name): string
    {
        $record = $this->store->loadSenderKey($name);
        if ($record->isEmpty()) {
            $idBytes = $this->random->bytes(4); $seed = $this->random->bytes(32);
            if (strlen($idBytes) !== 4 || strlen($seed) !== 32) { throw new SignalException('Unable to generate Sender Key material.'); }
            $parts = unpack('Nid', $idBytes); $keyId = ((int) $parts['id']) & 0x7fffffff;
            $signing = $this->curve->generateKeyPair();
            $record->replace(new SenderKeyState($keyId, new SenderChainKey(0, $seed), $signing->publicKey(), $signing->privateKey()));
            $this->store->storeSenderKey($name, $record);
        }
        $state = $record->currentState();
        if ($state === null) { throw new SignalException('Sender Key state is unavailable.'); }
        $message = new SenderKeyDistributionMessage();
        $message->setId($state->keyId()); $message->setIteration($state->chain()->iteration()); $message->setChainKey($state->chain()->seed()); $message->setSigningKey($state->signingPublic());
        return chr(self::VERSION_BYTE) . $this->protobuf->encode($message);
    }

    public function process(string $name, string $serialized): void
    {
        self::assertVersioned($serialized, 'distribution');
        $message = $this->protobuf->decode(SenderKeyDistributionMessage::class, substr($serialized, 1));
        if (!$message instanceof SenderKeyDistributionMessage || !$message->hasId() || !$message->hasIteration() || !$message->hasChainKey() || !$message->hasSigningKey() || strlen($message->getChainKey()) !== 32) { throw new SignalException('Sender Key distribution message is missing required fields.'); }
        SignalCurve::assertPublicKey($message->getSigningKey());
        $record = $this->store->loadSenderKey($name);
        $record->add(new SenderKeyState($message->getId(), new SenderChainKey($message->getIteration(), $message->getChainKey()), $message->getSigningKey()));
        $this->store->storeSenderKey($name, $record);
    }

    public static function assertVersioned(string $serialized, string $kind): void
    {
        if ($serialized === '' || strlen($serialized) > self::MAX_WIRE_BYTES) { throw new SignalException('Sender Key ' . $kind . ' message is empty or too large.'); }
        $version = ord($serialized[0]); if (($version >> 4) < 3 || ($version & 0x0f) > 3) { throw new SignalException('Sender Key ' . $kind . ' message version is incompatible.'); }
    }
}
