<?php

declare(strict_types=1);

namespace PhpWA\Auth;

use PhpWA\Crypto\KeyPair;
use PhpWA\Crypto\PreKeyFactory;
use PhpWA\Crypto\SignedPreKey;
use PhpWA\Crypto\SodiumCompatXEdDsa;
use PhpWA\Crypto\X25519;
use PhpWA\Exception\StorageException;

final class CredentialsSerializer
{
    private const MAX_JSON_BYTES = 4194304;

    public function serialize(AuthenticationCredentials $credentials): string
    {
        try {
            return json_encode($credentials->exportState(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
        } catch (\Throwable $error) {
            throw new StorageException('Unable to serialize WhatsApp credentials.', 0, $error);
        }
    }

    public function deserialize(string $json): AuthenticationCredentials
    {
        if ($json === '' || strlen($json) > self::MAX_JSON_BYTES) {
            throw new StorageException('WhatsApp credential payload is empty or too large.');
        }
        try {
            $state = json_decode($json, true, 128, JSON_THROW_ON_ERROR);
            if (!is_array($state) || ($state['schema_version'] ?? null) !== AuthenticationCredentials::SCHEMA_VERSION) {
                throw new StorageException('Unsupported WhatsApp credential schema.');
            }
            $noise = $this->keyPair($state['noise_key'] ?? null);
            $pairing = $this->keyPair($state['pairing_ephemeral_key'] ?? null);
            $identity = $this->keyPair($state['signed_identity_key'] ?? null);
            $signedState = $state['signed_pre_key'] ?? null;
            if (!is_array($signedState) || !is_int($signedState['id'] ?? null)) {
                throw new StorageException('Malformed signed pre-key.');
            }
            $signedKey = new SignedPreKey(
                $signedState['id'],
                $this->keyPair($signedState['key'] ?? null),
                $this->base64($signedState['signature'] ?? null, 64, 'signed pre-key signature')
            );
            $xeddsa = new SodiumCompatXEdDsa();
            if (!$xeddsa->verify(
                $identity->publicKey(),
                PreKeyFactory::signalPublicKey($signedKey->keyPair()->publicKey()),
                $signedKey->signature()
            )) {
                throw new StorageException('Signed pre-key integrity verification failed.');
            }
            if (!is_int($state['registration_id'] ?? null)) {
                throw new StorageException('Malformed registration ID.');
            }
            $history = $state['processed_history_messages'] ?? [];
            $signalIdentities = $state['signal_identities'] ?? [];
            if (!is_array($history) || !self::isList($history) || !is_array($signalIdentities) || !self::isList($signalIdentities)) {
                throw new StorageException('Malformed credential collections.');
            }
            $decodedSignalIdentities = [];
            foreach ($signalIdentities as $entry) {
                if (!is_array($entry) || !is_string($entry['name'] ?? null) || !is_int($entry['device_id'] ?? null)) {
                    throw new StorageException('Malformed Signal identity entry.');
                }
                $decodedSignalIdentities[] = new SignalIdentity(
                    $entry['name'],
                    $entry['device_id'],
                    $this->base64($entry['identifier_key'] ?? null, 33, 'Signal identity key')
                );
            }
            $me = null;
            if (($state['me'] ?? null) !== null) {
                $meState = $state['me'];
                if (!is_array($meState) || !is_string($meState['id'] ?? null)) {
                    throw new StorageException('Malformed account identity.');
                }
                $me = new AccountIdentity(
                    $meState['id'],
                    self::nullableString($meState['name'] ?? null),
                    self::nullableString($meState['lid'] ?? null)
                );
            }
            foreach (['next_pre_key_id', 'first_unuploaded_pre_key_id', 'account_sync_counter'] as $integerKey) {
                if (!is_int($state[$integerKey] ?? null)) { throw new StorageException('Malformed credential counter.'); }
            }
            foreach (['unarchive_chats', 'registered'] as $boolKey) {
                if (!is_bool($state[$boolKey] ?? null)) { throw new StorageException('Malformed credential boolean.'); }
            }

            return new AuthenticationCredentials(
                $noise,
                $pairing,
                $identity,
                $signedKey,
                $state['registration_id'],
                $this->base64($state['adv_secret_key'] ?? null, 32, 'ADV secret'),
                $history,
                $state['next_pre_key_id'],
                $state['first_unuploaded_pre_key_id'],
                $state['account_sync_counter'],
                $state['unarchive_chats'],
                $me,
                $this->nullableBase64($state['signed_device_identity'] ?? null, 1048576, 'signed device identity'),
                $decodedSignalIdentities,
                self::nullableString($state['my_lid'] ?? null),
                self::nullableString($state['platform'] ?? null),
                self::nullableString($state['device_id'] ?? null),
                self::nullableString($state['phone_number'] ?? null),
                $this->nullableBase64($state['routing_info'] ?? null, 65535, 'routing info'),
                self::nullableString($state['last_property_hash'] ?? null),
                $state['registered']
            );
        } catch (StorageException $error) {
            throw $error;
        } catch (\Throwable $error) {
            throw new StorageException('Unable to deserialize WhatsApp credentials.', 0, $error);
        }
    }

    private function keyPair($state): KeyPair
    {
        if (!is_array($state)) { throw new StorageException('Malformed Curve25519 key pair.'); }
        $private = $this->base64($state['private'] ?? null, 32, 'private key');
        $public = $this->base64($state['public'] ?? null, 32, 'public key');
        $derived = X25519::fromPrivate($private)->publicKey();
        if (!hash_equals($derived, $public)) { throw new StorageException('Curve25519 key-pair integrity verification failed.'); }
        return new KeyPair($private, $public);
    }

    private function base64($value, int $exactLength, string $field): string
    {
        if (!is_string($value)) { throw new StorageException('Malformed base64 ' . $field . '.'); }
        $decoded = base64_decode($value, true);
        if (!is_string($decoded) || strlen($decoded) !== $exactLength || base64_encode($decoded) !== $value) {
            throw new StorageException('Malformed base64 ' . $field . '.');
        }
        return $decoded;
    }

    private function nullableBase64($value, int $maxLength, string $field): ?string
    {
        if ($value === null) { return null; }
        if (!is_string($value)) { throw new StorageException('Malformed base64 ' . $field . '.'); }
        $decoded = base64_decode($value, true);
        if (!is_string($decoded) || strlen($decoded) > $maxLength || base64_encode($decoded) !== $value) {
            throw new StorageException('Malformed base64 ' . $field . '.');
        }
        return $decoded;
    }

    private static function nullableString($value): ?string
    {
        if ($value === null) { return null; }
        if (!is_string($value)) { throw new StorageException('Malformed credential text field.'); }
        return $value;
    }

    private static function isList(array $value): bool
    {
        $index = 0;
        foreach ($value as $key => $_) {
            if ($key !== $index++) { return false; }
        }
        return true;
    }
}
