<?php
// =============================================================
// Signing — порт ключевых функций hyperliquid/utils/signing.py.
// =============================================================
// Содержит только то, что нужно копи-боту: подпись L1-actions (order/
// cancel/updateLeverage и пр.), user-signed actions (approveAgent),
// конвертацию ордеров в wire-формат и хеш action.
//
// Корректность гарантируется golden-векторами из tests/ (порт
// signing_test.py официального SDK).

namespace HL;

require_once __DIR__ . '/Msgpack.php';
require_once __DIR__ . '/crypto/Keccak.php';
require_once __DIR__ . '/crypto/Eip712.php';
require_once __DIR__ . '/crypto/Secp256k1.php';

use HL\Crypto\Keccak;
use HL\Crypto\Eip712;
use HL\Crypto\Secp256k1;

final class Signing
{
    // ------------------------------------------------------ числа → wire

    /** float → строка как float_to_wire() Python-SDK (до 8 знаков, без хвостовых нулей). */
    public static function floatToWire(float $x): string
    {
        $rounded = sprintf('%.8f', $x);
        if (abs((float) $rounded - $x) >= 1e-12) {
            throw new \InvalidArgumentException('floatToWire: округление теряет точность для ' . $x);
        }
        if ($rounded === '-0.00000000') {
            $rounded = '0.00000000';
        }
        // нормализация: убрать хвостовые нули и точку (Decimal.normalize + ':f')
        if (str_contains($rounded, '.')) {
            $rounded = rtrim($rounded, '0');
            $rounded = rtrim($rounded, '.');
        }
        if ($rounded === '' || $rounded === '-0') {
            $rounded = '0';
        }
        return $rounded;
    }

    public static function floatToInt(float $x, int $power): int
    {
        $withDecimals = $x * (10 ** $power);
        if (abs(round($withDecimals) - $withDecimals) >= 1e-3) {
            throw new \InvalidArgumentException('floatToInt: округление теряет точность для ' . $x);
        }
        return (int) round($withDecimals);
    }

    public static function floatToIntForHashing(float $x): int
    {
        return self::floatToInt($x, 8);
    }

    public static function floatToUsdInt(float $x): int
    {
        return self::floatToInt($x, 6);
    }

    // ------------------------------------------------------ ордера → wire

    /**
     * @param array $order ['coin','is_buy','sz','limit_px','order_type','reduce_only', 'cloid'?]
     *                     order_type: ['limit'=>['tif'=>..]] | ['trigger'=>['isMarket'=>..,'triggerPx'=>..,'tpsl'=>..]]
     */
    public static function orderRequestToWire(array $order, int $asset): array
    {
        $wire = [
            'a' => $asset,
            'b' => (bool) $order['is_buy'],
            'p' => self::floatToWire((float) $order['limit_px']),
            's' => self::floatToWire((float) $order['sz']),
            'r' => (bool) $order['reduce_only'],
            't' => self::orderTypeToWire($order['order_type']),
        ];
        if (!empty($order['cloid'])) {
            $wire['c'] = $order['cloid'];   // строка 0x... (16 байт)
        }
        return $wire;
    }

    private static function orderTypeToWire(array $orderType): array
    {
        if (isset($orderType['limit'])) {
            return ['limit' => ['tif' => $orderType['limit']['tif']]];
        }
        if (isset($orderType['trigger'])) {
            $t = $orderType['trigger'];
            return ['trigger' => [
                'isMarket'  => (bool) $t['isMarket'],
                'triggerPx' => self::floatToWire((float) $t['triggerPx']),
                'tpsl'      => $t['tpsl'],
            ]];
        }
        throw new \InvalidArgumentException('orderTypeToWire: неизвестный тип ордера');
    }

    /** @param array[] $orderWires */
    public static function orderWiresToAction(array $orderWires, ?array $builder = null, string $grouping = 'na'): array
    {
        $action = [
            'type'     => 'order',
            'orders'   => $orderWires,
            'grouping' => $grouping,
        ];
        if ($builder !== null) {
            $action['builder'] = $builder;
        }
        return $action;
    }

    // ------------------------------------------------------ action hash

    public static function actionHash(array $action, ?string $vaultAddress, int $nonce, ?int $expiresAfter): string
    {
        $data = Msgpack::encode($action);
        $data .= pack('J', $nonce);                       // nonce, 8 байт big-endian
        if ($vaultAddress === null) {
            $data .= "\x00";
        } else {
            $data .= "\x01" . self::addressToBytes($vaultAddress);
        }
        if ($expiresAfter !== null) {
            $data .= "\x00" . pack('J', $expiresAfter);
        }
        return Keccak::hash256($data);
    }

    private static function addressToBytes(string $address): string
    {
        $hex = str_starts_with($address, '0x') ? substr($address, 2) : $address;
        return hex2bin($hex);
    }

    // ------------------------------------------------------ подпись L1

    /**
     * @return array{r:string,s:string,v:int}
     */
    public static function signL1Action(
        string $privHex,
        array $action,
        ?string $vaultAddress,
        int $nonce,
        ?int $expiresAfter,
        bool $isMainnet
    ): array {
        $hash = self::actionHash($action, $vaultAddress, $nonce, $expiresAfter);
        $phantom = [
            'source'       => $isMainnet ? 'a' : 'b',
            'connectionId' => $hash,                       // 32 сырых байта
        ];
        $domain = [
            'name'              => 'Exchange',
            'version'           => '1',
            'chainId'           => 1337,
            'verifyingContract' => '0x0000000000000000000000000000000000000000',
        ];
        $types = [
            'Agent' => [
                ['name' => 'source', 'type' => 'string'],
                ['name' => 'connectionId', 'type' => 'bytes32'],
            ],
        ];
        $digest = Eip712::digest($domain, $types, 'Agent', $phantom);
        return Secp256k1::sign($digest, $privHex);
    }

    // ------------------------------------------------------ подпись user-signed

    /**
     * Для действий, подписываемых основным кошельком напрямую (approveAgent,
     * usdSend и т.п.). Мутирует $action, добавляя signatureChainId/hyperliquidChain.
     * @return array{r:string,s:string,v:int}
     */
    public static function signUserSignedAction(
        string $privHex,
        array $action,
        array $payloadTypes,
        string $primaryType,
        bool $isMainnet
    ): array {
        $action['signatureChainId'] = '0x66eee';
        $action['hyperliquidChain'] = $isMainnet ? 'Mainnet' : 'Testnet';
        $domain = [
            'name'              => 'HyperliquidSignTransaction',
            'version'           => '1',
            'chainId'           => 0x66eee,
            'verifyingContract' => '0x0000000000000000000000000000000000000000',
        ];
        $digest = Eip712::digest($domain, [$primaryType => $payloadTypes], $primaryType, $action);
        return Secp256k1::sign($digest, $privHex);
    }

    public static function getTimestampMs(): int
    {
        return (int) (microtime(true) * 1000);
    }
}
