<?php

declare(strict_types=1);

namespace PhpWA\AppState;

use InvalidArgumentException;
use PhpWA\Crypto\CryptoPrimitives;

/** Byte-exact WhatsApp app-state MAC and LTHash primitives. */
final class AppStateCrypto
{
    public function contentMac(int $operation, string $encryptedContent, string $keyId, string $key): string
    {
        if (($operation !== 0 && $operation !== 1) || $keyId === '' || strlen($keyId) > 64 || strlen($key) !== 32) {
            throw new InvalidArgumentException('App-state content-MAC input is invalid.');
        }
        $input = chr($operation + 1) . $keyId . $encryptedContent . $this->uint64be(strlen($keyId) + 1);
        return substr(hash_hmac('sha512', $input, $key, true), 0, 32);
    }

    public function indexMac(string $indexBytes, string $key): string
    {
        if (strlen($key) !== 32) { throw new InvalidArgumentException('App-state index MAC key must contain 32 bytes.'); }
        return hash_hmac('sha256', $indexBytes, $key, true);
    }

    public function snapshotMac(string $hash, int $version, string $name, string $key): string
    {
        $this->assertState($hash, $version, $name, $key);
        return hash_hmac('sha256', $hash . $this->uint64be($version) . $name, $key, true);
    }

    /** @param list<string> $valueMacs */
    public function patchMac(string $snapshotMac, array $valueMacs, int $version, string $name, string $key): string
    {
        if (strlen($snapshotMac) !== 32 || count($valueMacs) > 10000) {
            throw new InvalidArgumentException('App-state patch MAC input is invalid.');
        }
        $data = $snapshotMac;
        foreach ($valueMacs as $mac) {
            if (!is_string($mac) || strlen($mac) !== 32) { throw new InvalidArgumentException('App-state value MAC must contain 32 bytes.'); }
            $data .= $mac;
        }
        $this->assertState(str_repeat("\0", 128), $version, $name, $key);
        return hash_hmac('sha256', $data . $this->uint64be($version) . $name, $key, true);
    }

    /** @param list<string> $subtract @param list<string> $add */
    public function updateHash(string $hash, array $subtract, array $add): string
    {
        if (strlen($hash) !== 128 || count($subtract) > 10000 || count($add) > 10000) {
            throw new InvalidArgumentException('App-state LTHash input is invalid.');
        }
        $output = $hash;
        foreach ($subtract as $valueMac) { $this->mix($output, $valueMac, true); }
        foreach ($add as $valueMac) { $this->mix($output, $valueMac, false); }
        return $output;
    }

    private function mix(string &$hash, string $valueMac, bool $subtract): void
    {
        if (strlen($valueMac) !== 32) { throw new InvalidArgumentException('LTHash value MAC must contain 32 bytes.'); }
        $point = CryptoPrimitives::hkdf($valueMac, 128, '', 'WhatsApp Patch Integrity');
        for ($offset = 0; $offset < 128; $offset += 2) {
            $left = ord($hash[$offset]) | (ord($hash[$offset + 1]) << 8);
            $right = ord($point[$offset]) | (ord($point[$offset + 1]) << 8);
            $result = ($subtract ? $left - $right : $left + $right) & 0xffff;
            $hash[$offset] = chr($result & 0xff);
            $hash[$offset + 1] = chr(($result >> 8) & 0xff);
        }
    }

    private function assertState(string $hash, int $version, string $name, string $key): void
    {
        if (strlen($hash) !== 128 || $version < 0 || !AppStateName::isValid($name) || strlen($key) !== 32) {
            throw new InvalidArgumentException('App-state MAC state is invalid.');
        }
    }

    private function uint64be(int $value): string
    {
        if ($value < 0) { throw new InvalidArgumentException('Unsigned 64-bit value is negative.'); }
        $high = intdiv($value, 4294967296); $low = $value % 4294967296;
        return pack('N2', $high, $low);
    }
}
