<?php

declare(strict_types=1);

namespace PhpWA\Crypto\Signal;

use PhpWA\Crypto\CryptoPrimitives;
use PhpWA\Exception\SignalException;
use PhpWA\Protocol\ProtobufCodec;
use PhpWA\Protocol\Proto\SenderKeyMessage;

final class GroupCipher
{
    private const VERSION_BYTE = 0x33;
    private const SIGNATURE_BYTES = 64;
    private const MAX_WIRE_BYTES = 4194304;
    /** @var SignalProtocolStoreInterface */ private $store;
    /** @var string */ private $name;
    /** @var SignalCurve */ private $curve;
    /** @var SenderKeyRecordCodec */ private $records;
    /** @var ProtobufCodec */ private $protobuf;
    public function __construct(SignalProtocolStoreInterface $store, string $name, ?SignalCurve $curve = null) { if ($name === '') { throw new \InvalidArgumentException('Sender Key name cannot be empty.'); } $this->store = $store; $this->name = $name; $this->curve = $curve ?? new SignalCurve(); $this->records = new SenderKeyRecordCodec(); $this->protobuf = new ProtobufCodec(self::MAX_WIRE_BYTES); }

    public function encrypt(string $plaintext): string
    {
        if (strlen($plaintext) > self::MAX_WIRE_BYTES) { throw new SignalException('Group plaintext exceeds its size limit.'); }
        $stored = $this->store->loadSenderKey($this->name); $record = $this->records->copy($stored); $state = $record->currentState();
        if ($state === null || $state->signingPrivate() === null) { throw new SignalException('Local Sender Key state is unavailable for encryption.'); }
        $iteration = $state->chain()->iteration(); $messageKey = $this->getMessageKey($state, $iteration === 0 ? 0 : $iteration + 1);
        $ciphertext = CryptoPrimitives::aesCbcEncrypt($messageKey->cipherKey(), $plaintext, $messageKey->iv());
        $message = new SenderKeyMessage(); $message->setId($state->keyId()); $message->setIteration($messageKey->iteration()); $message->setCiphertext($ciphertext);
        $signed = chr(self::VERSION_BYTE) . $this->protobuf->encode($message);
        $result = $signed . $this->curve->sign($state->signingPrivate(), $signed);
        $this->store->storeSenderKey($this->name, $record);
        return $result;
    }

    public function decrypt(string $serialized): string
    {
        GroupSessionBuilder::assertVersioned($serialized, 'ciphertext');
        if (strlen($serialized) <= self::SIGNATURE_BYTES + 1) { throw new SignalException('Sender Key ciphertext is truncated.'); }
        $signed = substr($serialized, 0, -self::SIGNATURE_BYTES); $signature = substr($serialized, -self::SIGNATURE_BYTES);
        $message = $this->protobuf->decode(SenderKeyMessage::class, substr($signed, 1));
        if (!$message instanceof SenderKeyMessage || !$message->hasId() || !$message->hasIteration() || !$message->hasCiphertext() || $message->getCiphertext() === '' || (strlen($message->getCiphertext()) % 16) !== 0) { throw new SignalException('Sender Key ciphertext is missing required fields.'); }
        $stored = $this->store->loadSenderKey($this->name); $record = $this->records->copy($stored); $state = $record->state($message->getId());
        if ($state === null) { throw new SignalException('No Sender Key state matches this message.'); }
        if (!$this->curve->verify($state->signingPublic(), $signed, $signature)) { throw new SignalException('Sender Key signature verification failed.'); }
        $messageKey = $this->getMessageKey($state, $message->getIteration());
        try { $plaintext = CryptoPrimitives::aesCbcDecrypt($messageKey->cipherKey(), $message->getCiphertext(), $messageKey->iv()); }
        catch (\Throwable $error) { throw new SignalException('Sender Key ciphertext decryption failed.', 0, $error); }
        $this->store->storeSenderKey($this->name, $record);
        return $plaintext;
    }

    private function getMessageKey(SenderKeyState $state, int $iteration): SenderMessageKey
    {
        $chain = $state->chain();
        if ($chain->iteration() > $iteration) { $cached = $state->removeMessageKey($iteration); if ($cached === null) { throw new SignalException('Sender Key message counter is old or already used.'); } return $cached; }
        if ($iteration - $chain->iteration() > SenderKeyState::MAX_MESSAGE_KEYS) { throw new SignalException('Sender Key message counter is too far in the future.'); }
        while ($chain->iteration() < $iteration) { $state->addMessageKey($chain->messageKey()); $chain = $chain->next(); }
        $state->setChain($chain->next());
        return $chain->messageKey();
    }
}
