<?php

declare(strict_types=1);

namespace PhpWA\Auth;

use InvalidArgumentException;
use PhpWA\Protocol\Jid\Jid;
use PhpWA\Protocol\Proto\ClientPayload;
use PhpWA\Protocol\Proto\ClientPayload\ConnectReason;
use PhpWA\Protocol\Proto\ClientPayload\ConnectType;
use PhpWA\Protocol\Proto\ClientPayload\DevicePairingRegistrationData;
use PhpWA\Protocol\Proto\ClientPayload\UserAgent;
use PhpWA\Protocol\Proto\ClientPayload\UserAgent\AppVersion as UserAgentVersion;
use PhpWA\Protocol\Proto\ClientPayload\UserAgent\ReleaseChannel;
use PhpWA\Protocol\Proto\ClientPayload\WebInfo;
use PhpWA\Protocol\Proto\DeviceProps;
use PhpWA\Protocol\Proto\DeviceProps\AppVersion as DeviceVersion;
use PhpWA\Protocol\Proto\DeviceProps\HistorySyncConfig;

final class ClientPayloadFactory
{
    /** @var CompanionDeviceConfig */ private $config;

    public function __construct(?CompanionDeviceConfig $config = null)
    {
        $this->config = $config ?? new CompanionDeviceConfig();
    }

    public function registration(AuthenticationCredentials $credentials): ClientPayload
    {
        $companion = new DeviceProps([
            'os' => $this->config->operatingSystem(),
            'version' => new DeviceVersion(['primary' => 10, 'secondary' => 15, 'tertiary' => 7]),
            'platformType' => CompanionPlatform::devicePropsType($this->config),
            'requireFullSync' => $this->config->syncFullHistory(),
            'historySyncConfig' => new HistorySyncConfig([
                'storageQuotaMb' => 10240,
                'inlineInitialPayloadInE2EeMsg' => true,
                'supportCallLogHistory' => false,
                'supportBotUserAgentChatHistory' => true,
                'supportCagReactionsAndPolls' => true,
                'supportBizHostedMsg' => true,
                'supportRecentSyncChunkMessageCountTuning' => true,
                'supportHostedGroupMsg' => true,
                'supportFbidBotChatHistory' => true,
                'supportMessageAssociation' => true,
                'supportGroupHistory' => false,
            ]),
        ]);
        $signedPreKey = $credentials->signedPreKey();
        $pairing = new DevicePairingRegistrationData([
            'buildHash' => md5($this->config->version()->dotted(), true),
            'deviceProps' => $companion->serializeToString(),
            'eRegid' => self::bigEndian($credentials->registrationId(), 4),
            'eKeytype' => "\x05",
            'eIdent' => $credentials->signedIdentityKey()->publicKey(),
            'eSkeyId' => self::bigEndian($signedPreKey->id(), 3),
            'eSkeyVal' => $signedPreKey->keyPair()->publicKey(),
            'eSkeySig' => $signedPreKey->signature(),
        ]);
        $payload = $this->basePayload();
        $payload->setPassive(false)->setPull(false)->setDevicePairingData($pairing);
        return $payload;
    }

    public function login(AccountIdentity $identity): ClientPayload
    {
        $jid = Jid::parse($identity->id());
        if (!$jid instanceof Jid || (!$jid->isPhoneNumber() && !$jid->isHostedPhoneNumber())
            || preg_match('/^[1-9][0-9]{0,19}$/D', $jid->user()) !== 1) {
            throw new InvalidArgumentException('Login account must be a numeric WhatsApp phone JID.');
        }
        $payload = $this->basePayload();
        $payload
            ->setPassive(true)
            ->setPull(true)
            ->setUsername($jid->user())
            ->setDevice($jid->device() ?? 0)
            ->setLidDbMigrated(false);
        return $payload;
    }

    public function forCredentials(AuthenticationCredentials $credentials): ClientPayload
    {
        return $credentials->me() instanceof AccountIdentity
            ? $this->login($credentials->me())
            : $this->registration($credentials);
    }

    private function basePayload(): ClientPayload
    {
        $version = $this->config->version();
        $userAgent = new UserAgent([
            'appVersion' => new UserAgentVersion([
                'primary' => $version->primary(),
                'secondary' => $version->secondary(),
                'tertiary' => $version->tertiary(),
            ]),
            'platform' => CompanionPlatform::userAgentPlatform($this->config),
            'releaseChannel' => ReleaseChannel::RELEASE,
            'osVersion' => '0.1',
            'device' => 'Desktop',
            'osBuildNumber' => '0.1',
            'localeLanguageIso6391' => $this->config->languageCode(),
            'mnc' => '000',
            'mcc' => '000',
            'localeCountryIso31661Alpha2' => $this->config->countryCode(),
        ]);
        $payload = new ClientPayload([
            'connectType' => ConnectType::WIFI_UNKNOWN,
            'connectReason' => ConnectReason::USER_ACTIVATED,
            'userAgent' => $userAgent,
        ]);
        if (CompanionPlatform::userAgentPlatform($this->config) !== \PhpWA\Protocol\Proto\ClientPayload\UserAgent\Platform::ANDROID) {
            $payload->setWebInfo(new WebInfo(['webSubPlatform' => CompanionPlatform::webSubPlatform($this->config)]));
        }
        if ($this->config->pushName() !== null) {
            $payload->setPushName($this->config->pushName());
        }
        return $payload;
    }

    private static function bigEndian(int $value, int $bytes): string
    {
        if ($value < 0 || $bytes < 1 || $bytes > 4 || $value >= 2 ** ($bytes * 8)) {
            throw new InvalidArgumentException('Integer does not fit requested big-endian width.');
        }
        $result = '';
        for ($offset = $bytes - 1; $offset >= 0; $offset--) {
            $result .= chr(($value >> ($offset * 8)) & 255);
        }
        return $result;
    }
}
