<?php

declare(strict_types=1);

namespace PhpWA\Crypto\Signal;

use PhpWA\Exception\StorageException;

final class SenderKeyRecordCodec
{
    private const VERSION = 1;
    private const MAX_BYTES = 1048576;
    public function encode(SenderKeyRecord $record): string
    {
        $states = [];
        foreach ($record->states() as $state) {
            $keys = []; foreach ($state->messageKeys() as $key) { $keys[] = ['iteration' => $key->iteration(), 'seed' => base64_encode($key->seed())]; }
            $states[] = ['key_id' => $state->keyId(), 'chain_iteration' => $state->chain()->iteration(), 'chain_seed' => base64_encode($state->chain()->seed()), 'signing_public' => base64_encode($state->signingPublic()), 'signing_private' => $state->signingPrivate() === null ? null : base64_encode($state->signingPrivate()), 'message_keys' => $keys];
        }
        try { $json = json_encode(['v' => self::VERSION, 'states' => $states], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); }
        catch (\Throwable $error) { throw new StorageException('Unable to encode Sender Key record.', 0, $error); }
        if (!is_string($json) || strlen($json) > self::MAX_BYTES) { throw new StorageException('Sender Key record exceeds its size limit.'); }
        return $json;
    }
    public function decode(string $json): SenderKeyRecord
    {
        if ($json === '' || strlen($json) > self::MAX_BYTES) { throw new StorageException('Sender Key record is empty or too large.'); }
        try { $data = json_decode($json, true, 64, JSON_THROW_ON_ERROR); }
        catch (\Throwable $error) { throw new StorageException('Sender Key record is malformed.', 0, $error); }
        if (!is_array($data) || array_keys($data) !== ['v', 'states'] || $data['v'] !== self::VERSION || !is_array($data['states']) || !$this->isList($data['states']) || count($data['states']) > SenderKeyRecord::MAX_STATES) { throw new StorageException('Sender Key record shape is invalid.'); }
        $states = [];
        try {
            foreach ($data['states'] as $row) {
                if (!is_array($row) || array_keys($row) !== ['key_id', 'chain_iteration', 'chain_seed', 'signing_public', 'signing_private', 'message_keys'] || !is_array($row['message_keys']) || !$this->isList($row['message_keys']) || count($row['message_keys']) > SenderKeyState::MAX_MESSAGE_KEYS) { throw new StorageException('Sender Key state is malformed.'); }
                $keys = [];
                foreach ($row['message_keys'] as $key) { if (!is_array($key) || array_keys($key) !== ['iteration', 'seed']) { throw new StorageException('Sender message key is malformed.'); } $keys[] = new SenderMessageKey($this->integer($key['iteration']), $this->b64($key['seed'], 32)); }
                $states[] = new SenderKeyState($this->integer($row['key_id'], 0x7fffffff), new SenderChainKey($this->integer($row['chain_iteration']), $this->b64($row['chain_seed'], 32)), $this->b64($row['signing_public'], 33), $row['signing_private'] === null ? null : $this->b64($row['signing_private'], 32), $keys);
            }
        } catch (StorageException $error) { throw $error; }
        catch (\Throwable $error) { throw new StorageException('Sender Key record contains invalid state.', 0, $error); }
        return new SenderKeyRecord($states);
    }
    public function copy(SenderKeyRecord $record): SenderKeyRecord { return $this->decode($this->encode($record)); }
    private function b64($value, int $length): string { if (!is_string($value)) { throw new StorageException('Sender Key base64 field is malformed.'); } $decoded = base64_decode($value, true); if (!is_string($decoded) || strlen($decoded) !== $length || base64_encode($decoded) !== $value) { throw new StorageException('Sender Key base64 field is malformed.'); } return $decoded; }
    private function integer($value, int $maximum = 0xffffffff): int { if (!is_int($value) || $value < 0 || $value > $maximum) { throw new StorageException('Sender Key counter is malformed.'); } return $value; }
    private function isList(array $value): bool { $i = 0; foreach ($value as $key => $_) { if ($key !== $i++) { return false; } } return true; }
}
