<?php

declare(strict_types=1);

namespace PhpWA\Auth;

use InvalidArgumentException;
use PhpWA\Crypto\KeyPair;
use PhpWA\Crypto\SignedPreKey;

final class AuthenticationCredentials
{
    public const SCHEMA_VERSION = 2;

    /** @var KeyPair */ private $noiseKey;
    /** @var KeyPair */ private $pairingEphemeralKey;
    /** @var KeyPair */ private $signedIdentityKey;
    /** @var SignedPreKey */ private $signedPreKey;
    /** @var int */ private $registrationId;
    /** @var string */ private $advSecretKey;
    /** @var list<string> */ private $processedHistoryMessages;
    /** @var int */ private $nextPreKeyId;
    /** @var int */ private $firstUnuploadedPreKeyId;
    /** @var int */ private $accountSyncCounter;
    /** @var bool */ private $unarchiveChats;
    /** @var AccountIdentity|null */ private $me;
    /** @var string|null */ private $signedDeviceIdentity;
    /** @var list<SignalIdentity> */ private $signalIdentities;
    /** @var string|null */ private $myLid;
    /** @var string|null */ private $platform;
    /** @var string|null */ private $deviceId;
    /** @var string|null */ private $phoneNumber;
    /** @var string|null */ private $routingInfo;
    /** @var string|null */ private $lastPropertyHash;
    /** @var bool */ private $registered;

    /**
     * @param list<string> $processedHistoryMessages
     * @param list<SignalIdentity> $signalIdentities
     */
    public function __construct(
        KeyPair $noiseKey,
        KeyPair $pairingEphemeralKey,
        KeyPair $signedIdentityKey,
        SignedPreKey $signedPreKey,
        int $registrationId,
        string $advSecretKey,
        array $processedHistoryMessages = [],
        int $nextPreKeyId = 1,
        int $firstUnuploadedPreKeyId = 1,
        int $accountSyncCounter = 0,
        bool $unarchiveChats = false,
        ?AccountIdentity $me = null,
        ?string $signedDeviceIdentity = null,
        array $signalIdentities = [],
        ?string $myLid = null,
        ?string $platform = null,
        ?string $deviceId = null,
        ?string $phoneNumber = null,
        ?string $routingInfo = null,
        ?string $lastPropertyHash = null,
        bool $registered = false
    ) {
        if ($registrationId < 0 || $registrationId > 16383 || strlen($advSecretKey) !== 32) {
            throw new InvalidArgumentException('Invalid WhatsApp registration ID or ADV secret.');
        }
        foreach ([$nextPreKeyId, $firstUnuploadedPreKeyId] as $id) {
            if ($id < 0 || $id > 16777215) { throw new InvalidArgumentException('Pre-key cursor must fit 24 bits.'); }
        }
        if ($accountSyncCounter < 0 || count($processedHistoryMessages) > 10000 || count($signalIdentities) > 10000) {
            throw new InvalidArgumentException('Credential collection/counter limit exceeded.');
        }
        foreach ($processedHistoryMessages as $messageId) {
            if (!is_string($messageId) || $messageId === '' || strlen($messageId) > 256) {
                throw new InvalidArgumentException('Invalid processed history message ID.');
            }
        }
        foreach ($signalIdentities as $identity) {
            if (!$identity instanceof SignalIdentity) {
                throw new InvalidArgumentException('Signal identity collection is invalid.');
            }
        }
        if ($signedDeviceIdentity !== null && strlen($signedDeviceIdentity) > 1048576) {
            throw new InvalidArgumentException('Signed device identity exceeds limit.');
        }
        if ($routingInfo !== null && strlen($routingInfo) > 65535) {
            throw new InvalidArgumentException('Routing info exceeds limit.');
        }
        if ($myLid !== null) {
            $parsedLid = \PhpWA\Protocol\Jid\Jid::parse($myLid);
            if (!$parsedLid instanceof \PhpWA\Protocol\Jid\Jid || (!$parsedLid->isLid() && !$parsedLid->isHostedLid())) {
                throw new InvalidArgumentException('Account LID is invalid.');
            }
        }
        foreach ([$myLid, $platform, $deviceId, $phoneNumber, $lastPropertyHash] as $text) {
            if ($text !== null && (strlen($text) > 1024 || preg_match('//u', $text) !== 1)) {
                throw new InvalidArgumentException('Credential text field is invalid.');
            }
        }
        $this->noiseKey = $noiseKey;
        $this->pairingEphemeralKey = $pairingEphemeralKey;
        $this->signedIdentityKey = $signedIdentityKey;
        $this->signedPreKey = $signedPreKey;
        $this->registrationId = $registrationId;
        $this->advSecretKey = $advSecretKey;
        $this->processedHistoryMessages = array_values($processedHistoryMessages);
        $this->nextPreKeyId = $nextPreKeyId;
        $this->firstUnuploadedPreKeyId = $firstUnuploadedPreKeyId;
        $this->accountSyncCounter = $accountSyncCounter;
        $this->unarchiveChats = $unarchiveChats;
        $this->me = $me;
        $this->signedDeviceIdentity = $signedDeviceIdentity;
        $this->signalIdentities = array_values($signalIdentities);
        $this->myLid = $myLid;
        $this->platform = $platform;
        $this->deviceId = $deviceId;
        $this->phoneNumber = $phoneNumber;
        $this->routingInfo = $routingInfo;
        $this->lastPropertyHash = $lastPropertyHash;
        $this->registered = $registered;
    }

    public function noiseKey(): KeyPair { return $this->noiseKey; }
    public function pairingEphemeralKey(): KeyPair { return $this->pairingEphemeralKey; }
    public function signedIdentityKey(): KeyPair { return $this->signedIdentityKey; }
    public function signedPreKey(): SignedPreKey { return $this->signedPreKey; }
    public function registrationId(): int { return $this->registrationId; }
    public function advSecretKey(): string { return $this->advSecretKey; }
    /** @return list<string> */ public function processedHistoryMessages(): array { return $this->processedHistoryMessages; }
    public function nextPreKeyId(): int { return $this->nextPreKeyId; }
    public function firstUnuploadedPreKeyId(): int { return $this->firstUnuploadedPreKeyId; }
    public function accountSyncCounter(): int { return $this->accountSyncCounter; }
    public function unarchiveChats(): bool { return $this->unarchiveChats; }
    public function me(): ?AccountIdentity { return $this->me; }
    public function signedDeviceIdentity(): ?string { return $this->signedDeviceIdentity; }
    /** @return list<SignalIdentity> */ public function signalIdentities(): array { return $this->signalIdentities; }
    public function myLid(): ?string { return $this->myLid; }
    public function platform(): ?string { return $this->platform; }
    public function deviceId(): ?string { return $this->deviceId; }
    public function phoneNumber(): ?string { return $this->phoneNumber; }
    public function routingInfo(): ?string { return $this->routingInfo; }
    public function lastPropertyHash(): ?string { return $this->lastPropertyHash; }
    public function isRegistered(): bool { return $this->registered; }

    public function setMe(AccountIdentity $identity): void { $this->me = $identity; }
    public function setSignedDeviceIdentity(string $bytes): void
    {
        if ($bytes === '' || strlen($bytes) > 1048576) { throw new InvalidArgumentException('Signed device identity is invalid.'); }
        $this->signedDeviceIdentity = $bytes;
    }
    public function setRegistered(bool $registered): void { $this->registered = $registered; }
    public function setRoutingInfo(?string $routingInfo): void
    {
        if ($routingInfo !== null && strlen($routingInfo) > 65535) { throw new InvalidArgumentException('Routing info exceeds limit.'); }
        $this->routingInfo = $routingInfo;
    }
    public function setPlatform(?string $platform): void
    {
        if ($platform !== null && (strlen($platform) > 1024 || preg_match('//u', $platform) !== 1)) {
            throw new InvalidArgumentException('Platform value is invalid.');
        }
        $this->platform = $platform;
    }
    public function setMyLid(?string $lid): void
    {
        if ($lid !== null) {
            $parsed = \PhpWA\Protocol\Jid\Jid::parse($lid);
            if (!$parsed instanceof \PhpWA\Protocol\Jid\Jid || (!$parsed->isLid() && !$parsed->isHostedLid())) {
                throw new InvalidArgumentException('Account LID is invalid.');
            }
        }
        $this->myLid = $lid;
    }
    public function advancePreKeyCursor(int $nextPreKeyId, int $firstUnuploadedPreKeyId): void
    {
        if ($nextPreKeyId < 0 || $nextPreKeyId > 16777215 || $firstUnuploadedPreKeyId < 0 || $firstUnuploadedPreKeyId > $nextPreKeyId) {
            throw new InvalidArgumentException('Invalid pre-key cursor transition.');
        }
        $this->nextPreKeyId = $nextPreKeyId;
        $this->firstUnuploadedPreKeyId = $firstUnuploadedPreKeyId;
    }

    public function withPairing(
        AccountIdentity $identity,
        string $signedDeviceIdentity,
        SignalIdentity $signalIdentity,
        ?string $platform
    ): self {
        $identities = $this->signalIdentities;
        $found = false;
        foreach ($identities as $storedIdentity) {
            if ($storedIdentity->equals($signalIdentity)) {
                $found = true;
                break;
            }
        }
        if (!$found) {
            $identities[] = $signalIdentity;
        }
        $jid = \PhpWA\Protocol\Jid\Jid::parse($identity->id());
        return new self(
            $this->noiseKey,
            $this->pairingEphemeralKey,
            $this->signedIdentityKey,
            $this->signedPreKey,
            $this->registrationId,
            $this->advSecretKey,
            $this->processedHistoryMessages,
            $this->nextPreKeyId,
            $this->firstUnuploadedPreKeyId,
            $this->accountSyncCounter,
            $this->unarchiveChats,
            $identity,
            $signedDeviceIdentity,
            $identities,
            $identity->lid(),
            $platform,
            $jid instanceof \PhpWA\Protocol\Jid\Jid && $jid->device() !== null ? (string) $jid->device() : null,
            $jid instanceof \PhpWA\Protocol\Jid\Jid ? $jid->user() : null,
            $this->routingInfo,
            $this->lastPropertyHash,
            false
        );
    }

    public function withRegistered(bool $registered): self
    {
        $copy = clone $this;
        $copy->registered = $registered;
        return $copy;
    }

    public function withAdvSecretKey(string $advSecretKey): self
    {
        if (strlen($advSecretKey) !== 32) { throw new InvalidArgumentException('ADV secret must contain exactly 32 bytes.'); }
        $copy = clone $this;
        $copy->advSecretKey = $advSecretKey;
        return $copy;
    }

    public function withRoutingInfo(?string $routingInfo): self
    {
        if ($routingInfo !== null && strlen($routingInfo) > 65535) {
            throw new InvalidArgumentException('Routing info exceeds limit.');
        }
        $copy = clone $this;
        $copy->routingInfo = $routingInfo;
        return $copy;
    }

    public function withPreKeyCursor(int $nextPreKeyId, int $firstUnuploadedPreKeyId): self
    {
        if ($nextPreKeyId < 0 || $nextPreKeyId > 16777215
            || $firstUnuploadedPreKeyId < 0 || $firstUnuploadedPreKeyId > $nextPreKeyId) {
            throw new InvalidArgumentException('Invalid pre-key cursor transition.');
        }
        $copy = clone $this;
        $copy->nextPreKeyId = $nextPreKeyId;
        $copy->firstUnuploadedPreKeyId = $firstUnuploadedPreKeyId;
        return $copy;
    }

    /** @return array<string,mixed> @internal Storage serialization only. */
    public function exportState(): array
    {
        return [
            'schema_version' => self::SCHEMA_VERSION,
            'noise_key' => self::keyState($this->noiseKey),
            'pairing_ephemeral_key' => self::keyState($this->pairingEphemeralKey),
            'signed_identity_key' => self::keyState($this->signedIdentityKey),
            'signed_pre_key' => [
                'id' => $this->signedPreKey->id(),
                'key' => self::keyState($this->signedPreKey->keyPair()),
                'signature' => base64_encode($this->signedPreKey->signature()),
            ],
            'registration_id' => $this->registrationId,
            'adv_secret_key' => base64_encode($this->advSecretKey),
            'processed_history_messages' => $this->processedHistoryMessages,
            'next_pre_key_id' => $this->nextPreKeyId,
            'first_unuploaded_pre_key_id' => $this->firstUnuploadedPreKeyId,
            'account_sync_counter' => $this->accountSyncCounter,
            'unarchive_chats' => $this->unarchiveChats,
            'me' => $this->me === null ? null : ['id' => $this->me->id(), 'name' => $this->me->name(), 'lid' => $this->me->lid()],
            'signed_device_identity' => $this->signedDeviceIdentity === null ? null : base64_encode($this->signedDeviceIdentity),
            'signal_identities' => array_map(static function (SignalIdentity $identity): array {
                return $identity->exportState();
            }, $this->signalIdentities),
            'my_lid' => $this->myLid,
            'platform' => $this->platform,
            'device_id' => $this->deviceId,
            'phone_number' => $this->phoneNumber,
            'routing_info' => $this->routingInfo === null ? null : base64_encode($this->routingInfo),
            'last_property_hash' => $this->lastPropertyHash,
            'registered' => $this->registered,
        ];
    }

    /** @return array<string,string> */
    public function __debugInfo(): array
    {
        return [
            'registered' => $this->registered ? 'yes' : 'no',
            'account' => $this->me === null ? 'unpaired' : hash('sha256', $this->me->id()),
            'noise_public_sha256' => hash('sha256', $this->noiseKey->publicKey()),
        ];
    }

    /** @return array<string,string> */
    private static function keyState(KeyPair $keyPair): array
    {
        return ['private' => base64_encode($keyPair->privateKey()), 'public' => base64_encode($keyPair->publicKey())];
    }
}
