<?php
// =============================================================
// Eip712 — хеширование typed-data (EIP-712) для Hyperliquid.
// =============================================================
// Поддержаны ровно те примитивные типы полей, что используются в подписях
// SDK: string, bytes32, address, uint256/uint64 (любой uint*), bool.
// Вложенных struct-типов в actions Hyperliquid нет, поэтому resolve
// зависимостей типов не требуется.
//
// digest = keccak256( 0x1901 || domainSeparator || hashStruct(primary) )

namespace HL\Crypto;

require_once __DIR__ . '/Keccak.php';
require_once __DIR__ . '/BigInt.php';

final class Eip712
{
    /**
     * @param array  $domain   ['name'=>..,'version'=>..,'chainId'=>int,'verifyingContract'=>'0x..']
     * @param array  $types    [ typeName => [ ['name'=>..,'type'=>..], ... ] ] (без EIP712Domain)
     * @param string $primary  имя основного типа
     * @param array  $message  значения полей основного типа
     * @return string 32-байтовый дайджест (бинарно)
     */
    public static function digest(array $domain, array $types, string $primary, array $message): string
    {
        $domainTypes = [
            ['name' => 'name', 'type' => 'string'],
            ['name' => 'version', 'type' => 'string'],
            ['name' => 'chainId', 'type' => 'uint256'],
            ['name' => 'verifyingContract', 'type' => 'address'],
        ];
        $domainSep = self::hashStruct('EIP712Domain', [
            'name' => $domain['name'],
            'version' => $domain['version'],
            'chainId' => $domain['chainId'],
            'verifyingContract' => $domain['verifyingContract'],
        ], ['EIP712Domain' => $domainTypes]);

        $structHash = self::hashStruct($primary, $message, $types);

        return Keccak::hash256("\x19\x01" . $domainSep . $structHash);
    }

    private static function hashStruct(string $type, array $data, array $types): string
    {
        $typeHash = Keccak::hash256(self::encodeType($type, $types));
        $enc = $typeHash;
        foreach ($types[$type] as $field) {
            $enc .= self::encodeField($field['type'], $data[$field['name']]);
        }
        return Keccak::hash256($enc);
    }

    private static function encodeType(string $type, array $types): string
    {
        $parts = [];
        foreach ($types[$type] as $field) {
            $parts[] = $field['type'] . ' ' . $field['name'];
        }
        return $type . '(' . implode(',', $parts) . ')';
    }

    private static function encodeField(string $type, $value): string
    {
        if ($type === 'string') {
            return Keccak::hash256((string) $value);
        }
        if ($type === 'bytes32') {
            // ожидаем 32 сырых байта (например connectionId)
            if (strlen($value) !== 32) {
                throw new \InvalidArgumentException('bytes32 должен быть 32 байта');
            }
            return $value;
        }
        if ($type === 'address') {
            $hex = strtolower((string) $value);
            if (str_starts_with($hex, '0x')) {
                $hex = substr($hex, 2);
            }
            $hex = str_pad($hex, 40, '0', STR_PAD_LEFT);
            return str_repeat("\x00", 12) . hex2bin($hex);
        }
        if (str_starts_with($type, 'uint') || str_starts_with($type, 'int')) {
            return self::encodeUint($value);
        }
        if ($type === 'bool') {
            return str_repeat("\x00", 31) . ($value ? "\x01" : "\x00");
        }
        throw new \InvalidArgumentException('Eip712: неподдерживаемый тип ' . $type);
    }

    private static function encodeUint($value): string
    {
        if ($value instanceof BigInt) {
            return $value->toBytesBE(32);
        }
        if (is_int($value)) {
            return BigInt::fromInt($value)->toBytesBE(32);
        }
        if (is_string($value)) {
            $big = str_starts_with(strtolower($value), '0x')
                ? BigInt::fromHex($value)
                : self::decToBig($value);
            return $big->toBytesBE(32);
        }
        throw new \InvalidArgumentException('Eip712: uint неподдерживаемого типа');
    }

    private static function decToBig(string $dec): BigInt
    {
        // десятичная строка → BigInt (для больших uint, которых сейчас нет,
        // но оставляем для расширяемости)
        $res = BigInt::zero();
        $ten = BigInt::fromInt(10);
        for ($i = 0, $len = strlen($dec); $i < $len; $i++) {
            $res = $res->mul($ten)->add(BigInt::fromInt((int) $dec[$i]));
        }
        return $res;
    }
}
