<?php

declare(strict_types=1);

namespace PhpWA\Auth;

use PhpWA\Exception\ProtocolException;
use PhpWA\Storage\AccountStoreInterface;
use PhpWA\Storage\AccountTransactionInterface;
use PhpWA\Storage\CredentialRecord;
use PhpWA\Storage\TransactionalCredentialStore;

final class PairingCommitter
{
    private const SIGNAL_IDENTITY = 'signal_identity';
    private const LID_TO_PHONE = 'lid_mapping';
    private const PHONE_TO_LID = 'pn_mapping';

    /** @var AccountStoreInterface */ private $accounts;
    /** @var TransactionalCredentialStore */ private $credentials;

    public function __construct(AccountStoreInterface $accounts, ?TransactionalCredentialStore $credentials = null)
    {
        $this->accounts = $accounts;
        $this->credentials = $credentials ?? new TransactionalCredentialStore($accounts);
    }

    public function commit(string $accountId, PairingResult $result, int $expectedRevision): CredentialRecord
    {
        return $this->accounts->transaction(
            $accountId,
            function (AccountTransactionInterface $transaction) use ($result, $expectedRevision): CredentialRecord {
                $credentials = $result->credentials();
                $identity = $credentials->me();
                if (!$identity instanceof AccountIdentity || $identity->lid() === null) {
                    throw new \LogicException('Pairing result has no account LID mapping.');
                }
                $record = $this->credentials->saveInTransaction($transaction, $credentials, $expectedRevision);
                foreach ($credentials->signalIdentities() as $signalIdentity) {
                    $this->pin(
                        $transaction,
                        self::SIGNAL_IDENTITY,
                        $signalIdentity->name(),
                        $signalIdentity->identifierKey(),
                        'Signal identity'
                    );
                }
                $this->pin($transaction, self::LID_TO_PHONE, $identity->lid(), $identity->id(), 'LID mapping');
                $this->pin($transaction, self::PHONE_TO_LID, $identity->id(), $identity->lid(), 'phone mapping');
                $transaction->put('maintenance_job', 'prekey_upload', json_encode([
                    'v' => 1, 'requested_at' => time(), 'remaining' => 0, 'initial' => true,
                ], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
                return $record;
            }
        );
    }

    /**
     * Account identities and address mappings are trust-on-first-use records.
     * A different value must never be silently accepted during a replay or a
     * stale pairing flow: the caller has to explicitly reset that account.
     */
    private function pin(
        AccountTransactionInterface $transaction,
        string $collection,
        string $key,
        string $value,
        string $label
    ): void {
        $known = $transaction->get($collection, $key);
        if ($known === null) {
            $transaction->put($collection, $key, $value, 0);
            return;
        }
        if (!hash_equals($known->value(), $value)) {
            throw new ProtocolException($label . ' conflicts with the pinned account state; explicit reset is required.');
        }
    }
}
