<?php
// =============================================================
// Msgpack — минимальный энкодер для action_hash Hyperliquid.
// =============================================================
// Воспроизводит msgpack.packb() из Python-SDK ровно для тех типов, что
// встречаются в торговых action: map (assoc-массив), array (list), string,
// int (вкл. отрицательные и uint64), bool, null.
//
// КРИТИЧНО: порядок ключей в map берётся из порядка вставки в PHP-массив —
// он обязан совпадать с порядком в SDK, иначе action_hash (и подпись) не
// сойдётся. Поэтому собирайте action-массивы в том же порядке полей, что
// в hyperliquid/exchange.py. Числа выбирают минимальное представление,
// как msgpack по умолчанию.
//
// Float намеренно НЕ поддержан: в actions все дробные значения уже
// сконвертированы в wire-строки (float_to_wire) или int (float_to_int).

namespace HL;

final class Msgpack
{
    public static function encode($value): string
    {
        if ($value === null) {
            return "\xc0";
        }
        if (is_bool($value)) {
            return $value ? "\xc3" : "\xc2";
        }
        if (is_int($value)) {
            return self::encodeInt($value);
        }
        if (is_string($value)) {
            return self::encodeStr($value);
        }
        if (is_array($value)) {
            return array_is_list($value)
                ? self::encodeArray($value)
                : self::encodeMap($value);
        }
        throw new \InvalidArgumentException('Msgpack: неподдерживаемый тип ' . gettype($value));
    }

    private static function encodeInt(int $n): string
    {
        if ($n >= 0) {
            if ($n <= 0x7f) {
                return chr($n);                          // positive fixint
            }
            if ($n <= 0xff) {
                return "\xcc" . chr($n);                 // uint8
            }
            if ($n <= 0xffff) {
                return "\xcd" . pack('n', $n);           // uint16
            }
            if ($n <= 0xffffffff) {
                return "\xce" . pack('N', $n);           // uint32
            }
            return "\xcf" . pack('J', $n);               // uint64
        }
        // отрицательные
        if ($n >= -32) {
            return chr($n & 0xff);                       // negative fixint
        }
        if ($n >= -128) {
            return "\xd0" . chr($n & 0xff);              // int8
        }
        if ($n >= -32768) {
            return "\xd1" . pack('n', $n & 0xffff);      // int16
        }
        if ($n >= -2147483648) {
            return "\xd2" . pack('N', $n & 0xffffffff);  // int32
        }
        return "\xd3" . self::int64BE($n);              // int64
    }

    /** Знаковый int64 → 8 байт big-endian (two's complement). */
    private static function int64BE(int $n): string
    {
        $bytes = '';
        for ($i = 0; $i < 8; $i++) {
            $bytes = chr($n & 0xff) . $bytes;
            $n >>= 8;
        }
        return $bytes;
    }

    private static function encodeStr(string $s): string
    {
        $len = strlen($s);
        if ($len <= 31) {
            return chr(0xa0 | $len) . $s;                // fixstr
        }
        if ($len <= 0xff) {
            return "\xd9" . chr($len) . $s;              // str8
        }
        if ($len <= 0xffff) {
            return "\xda" . pack('n', $len) . $s;        // str16
        }
        return "\xdb" . pack('N', $len) . $s;            // str32
    }

    private static function encodeArray(array $arr): string
    {
        $len = count($arr);
        if ($len <= 15) {
            $out = chr(0x90 | $len);
        } elseif ($len <= 0xffff) {
            $out = "\xdc" . pack('n', $len);
        } else {
            $out = "\xdd" . pack('N', $len);
        }
        foreach ($arr as $item) {
            $out .= self::encode($item);
        }
        return $out;
    }

    private static function encodeMap(array $map): string
    {
        $len = count($map);
        if ($len <= 15) {
            $out = chr(0x80 | $len);
        } elseif ($len <= 0xffff) {
            $out = "\xde" . pack('n', $len);
        } else {
            $out = "\xdf" . pack('N', $len);
        }
        foreach ($map as $key => $val) {
            $out .= self::encodeStr((string) $key);
            $out .= self::encode($val);
        }
        return $out;
    }
}
