<?php

declare(strict_types=1);

namespace PhpWA\Crypto\Signal;

use PhpWA\Exception\SignalException;
use PhpWA\Protocol\Proto\PreKeySignalMessage;

final class SessionBuilder
{
    /** @var SignalProtocolStoreInterface */ private $store;
    /** @var string */ private $address;
    /** @var SignalCurve */ private $curve;

    public function __construct(SignalProtocolStoreInterface $store, string $address, ?SignalCurve $curve = null)
    {
        if ($address === '') { throw new \InvalidArgumentException('Signal address cannot be empty.'); }
        $this->store = $store; $this->address = $address; $this->curve = $curve ?? new SignalCurve();
    }

    public function initializeOutgoing(PreKeyBundle $bundle): void
    {
        $bundle->assertSignature($this->curve);
        $this->store->assertTrustedIdentity($this->address, $bundle->identityKey());
        $baseKey = $this->curve->generateKeyPair();
        $session = $this->initializeSession(
            true,
            $baseKey,
            null,
            $bundle->identityKey(),
            $bundle->preKey(),
            $bundle->signedPreKey(),
            $bundle->registrationId()
        );
        $pending = ['signed_pre_key_id' => $bundle->signedPreKeyId(), 'base_key' => $baseKey->publicKey()];
        if ($bundle->preKeyId() !== null) { $pending['pre_key_id'] = $bundle->preKeyId(); }
        $session->setPendingPreKey($pending);
        $record = $this->store->loadSession($this->address) ?? new SignalSessionRecord();
        $record->closeOpenSession(); $record->prepend($session);
        $this->store->storeSession($this->address, $record);
    }

    public function initializeIncoming(SignalSessionRecord $record, PreKeySignalMessage $message): ?int
    {
        $baseKey = $message->getBaseKey(); $identity = $message->getIdentityKey();
        SignalCurve::assertPublicKey($baseKey); SignalCurve::assertPublicKey($identity);
        if ($record->sessionByBaseKey($baseKey) !== null) { return null; }
        $this->store->assertTrustedIdentity($this->address, $identity);
        $preKeyId = $message->hasPreKeyId() ? $message->getPreKeyId() : null;
        $preKey = $preKeyId === null ? null : $this->store->loadPreKey($preKeyId);
        if ($preKeyId !== null && $preKey === null) { throw new SignalException('Inbound Signal message references an unavailable one-time pre-key.'); }
        if (!$message->hasSignedPreKeyId()) { throw new SignalException('Inbound Signal pre-key message has no signed pre-key ID.'); }
        $signed = $this->store->loadSignedPreKey($message->getSignedPreKeyId());
        if ($signed === null) { throw new SignalException('Inbound Signal message references an unavailable signed pre-key.'); }
        $record->closeOpenSession();
        $record->prepend($this->initializeSession(false, $preKey, $signed, $identity, $baseKey, null, $message->getRegistrationId()));
        return $preKeyId;
    }

    private function initializeSession(bool $initiator, ?SignalKeyPair $ourEphemeral, ?SignalKeyPair $ourSigned, string $theirIdentity, ?string $theirEphemeral, ?string $theirSigned, int $registrationId): SignalSession
    {
        if ($initiator) { $ourSigned = $ourEphemeral; }
        else { $theirSigned = $theirEphemeral; }
        if ($ourSigned === null || $theirSigned === null) { throw new SignalException('Signal X3DH keys are incomplete.'); }
        SignalCurve::assertPublicKey($theirIdentity); SignalCurve::assertPublicKey($theirSigned);
        $hasOneTime = $ourEphemeral !== null && $theirEphemeral !== null;
        $shared = str_repeat("\0", $hasOneTime ? 160 : 128);
        $shared = substr_replace($shared, str_repeat("\xff", 32), 0, 32);
        $identity = $this->store->identityKeyPair();
        $a1 = $this->curve->agreement($theirSigned, $identity->privateKey());
        $a2 = $this->curve->agreement($theirIdentity, $ourSigned->privateKey());
        $a3 = $this->curve->agreement($theirSigned, $ourSigned->privateKey());
        if ($initiator) { $shared = substr_replace($shared, $a1, 32, 32); $shared = substr_replace($shared, $a2, 64, 32); }
        else { $shared = substr_replace($shared, $a2, 32, 32); $shared = substr_replace($shared, $a1, 64, 32); }
        $shared = substr_replace($shared, $a3, 96, 32);
        if ($hasOneTime) { $shared = substr_replace($shared, $this->curve->agreement((string) $theirEphemeral, $ourEphemeral->privateKey()), 128, 32); }
        $master = SignalKdf::derive($shared, str_repeat("\0", 32), 'WhisperText', 3);
        $ratchetPair = $initiator ? $this->curve->generateKeyPair() : $ourSigned;
        $ratchet = new SignalRatchet($master[0], $ratchetPair, $theirSigned, 0);
        $baseKey = $initiator ? ($ourEphemeral === null ? null : $ourEphemeral->publicKey()) : $theirEphemeral;
        if ($baseKey === null) { throw new SignalException('Signal base key is unavailable.'); }
        $session = new SignalSession($registrationId, $ratchet, new SignalSessionIndex($baseKey, $initiator ? SignalSessionIndex::BASE_OURS : SignalSessionIndex::BASE_THEIRS, $theirIdentity));
        if ($initiator) { self::calculateSendingRatchet($session, $theirSigned, $this->curve); }
        return $session;
    }

    public static function calculateSendingRatchet(SignalSession $session, string $remoteKey, SignalCurve $curve): void
    {
        $ratchet = $session->ratchet();
        $master = SignalKdf::derive($curve->agreement($remoteKey, $ratchet->ephemeralKeyPair()->privateKey()), $ratchet->rootKey(), 'WhisperRatchet', 2);
        $session->putChain($ratchet->ephemeralKeyPair()->publicKey(), new SignalChain(-1, $master[1], SignalChain::SENDING));
        $ratchet->setRootKey($master[0]);
    }
}
