<?php

declare(strict_types=1);

namespace PhpWA\Crypto\Signal;

use InvalidArgumentException;
use PhpWA\Auth\AuthenticationCredentials;
use PhpWA\Exception\SignalException;
use PhpWA\Exception\StorageException;
use PhpWA\Storage\AccountTransactionInterface;

/**
 * Binds Signal state to the same transaction used for dedupe and EasyChat
 * outbox insertion. No ratchet mutation can escape a rolled-back receive.
 */
final class TransactionalSignalStore implements SignalProtocolStoreInterface
{
    private const SESSION = 'signal_session';
    private const PREKEY = 'signal_prekey';
    private const SIGNED_PREKEY = 'signal_signed_prekey';
    private const IDENTITY = 'signal_identity';
    private const SENDER_KEY = 'signal_sender_key';

    /** @var AccountTransactionInterface */ private $transaction;
    /** @var AuthenticationCredentials */ private $credentials;
    /** @var SignalCurve */ private $curve;
    /** @var SignalSessionRecordCodec */ private $sessions;
    /** @var SenderKeyRecordCodec */ private $senderKeys;

    public function __construct(AccountTransactionInterface $transaction, AuthenticationCredentials $credentials, ?SignalCurve $curve = null)
    {
        $this->transaction = $transaction;
        $this->credentials = $credentials;
        $this->curve = $curve ?? new SignalCurve();
        $this->sessions = new SignalSessionRecordCodec();
        $this->senderKeys = new SenderKeyRecordCodec();
    }

    public function identityKeyPair(): SignalKeyPair
    {
        return $this->curve->keyPairFromPrivate($this->credentials->signedIdentityKey()->privateKey());
    }

    public function registrationId(): int { return $this->credentials->registrationId(); }

    public function assertTrustedIdentity(string $address, string $identityKey): void
    {
        self::assertAddress($address);
        SignalCurve::assertPublicKey($identityKey);
        $known = $this->transaction->get(self::IDENTITY, $address);
        if ($known === null) {
            $this->transaction->put(self::IDENTITY, $address, $identityKey, 0);
            return;
        }
        if (!hash_equals($known->value(), $identityKey)) {
            throw new SignalException('Remote Signal identity changed; explicit trust reset is required.');
        }
    }

    public function loadSession(string $address): ?SignalSessionRecord
    {
        self::assertAddress($address);
        $stored = $this->transaction->get(self::SESSION, $address);
        return $stored === null ? null : $this->sessions->decode($stored->value());
    }

    public function storeSession(string $address, SignalSessionRecord $record): void
    {
        self::assertAddress($address);
        $this->transaction->put(self::SESSION, $address, $this->sessions->encode($record));
    }

    public function loadPreKey(int $id): ?SignalKeyPair { return $this->loadKeyPair(self::PREKEY, $id); }
    public function storePreKey(int $id, SignalKeyPair $keyPair): void { $this->storeKeyPair(self::PREKEY, $id, $keyPair); }
    public function removePreKey(int $id): void { self::assertPreKeyId($id); $this->transaction->delete(self::PREKEY, (string) $id); }

    public function loadSignedPreKey(int $id): ?SignalKeyPair
    {
        self::assertPreKeyId($id);
        if ($id === $this->credentials->signedPreKey()->id()) {
            return $this->curve->keyPairFromPrivate($this->credentials->signedPreKey()->keyPair()->privateKey());
        }
        return $this->loadKeyPair(self::SIGNED_PREKEY, $id);
    }
    public function storeSignedPreKey(int $id, SignalKeyPair $keyPair): void { $this->storeKeyPair(self::SIGNED_PREKEY, $id, $keyPair); }

    public function loadSenderKey(string $name): SenderKeyRecord
    {
        self::assertAddress($name);
        $stored = $this->transaction->get(self::SENDER_KEY, $name);
        return $stored === null ? new SenderKeyRecord() : $this->senderKeys->decode($stored->value());
    }

    public function storeSenderKey(string $name, SenderKeyRecord $record): void
    {
        self::assertAddress($name);
        $this->transaction->put(self::SENDER_KEY, $name, $this->senderKeys->encode($record));
    }

    private function loadKeyPair(string $collection, int $id): ?SignalKeyPair
    {
        self::assertPreKeyId($id);
        $stored = $this->transaction->get($collection, (string) $id);
        if ($stored === null) { return null; }
        if (strlen($stored->value()) !== 65) { throw new StorageException('Stored Signal key pair is malformed.'); }
        return new SignalKeyPair(substr($stored->value(), 0, 33), substr($stored->value(), 33, 32));
    }

    private function storeKeyPair(string $collection, int $id, SignalKeyPair $keyPair): void
    {
        self::assertPreKeyId($id);
        $this->transaction->put($collection, (string) $id, $keyPair->publicKey() . $keyPair->privateKey());
    }

    private static function assertAddress(string $address): void
    {
        if ($address === '' || strlen($address) > 512 || preg_match('//u', $address) !== 1 || preg_match('/[\x00-\x1f\x7f]/', $address) === 1) {
            throw new InvalidArgumentException('Signal address is invalid.');
        }
    }

    private static function assertPreKeyId(int $id): void
    {
        if ($id < 0 || $id > 0xffffff) { throw new InvalidArgumentException('Signal pre-key ID must fit 24 bits.'); }
    }
}
