<?php

namespace AlibabaCloud\OpenApiUtil;

use AlibabaCloud\Tea\Model;
use AlibabaCloud\Tea\Request;
use AlibabaCloud\Tea\Utils\Utils;
use OneSm\Sm3;
use Psr\Http\Message\StreamInterface;

/**
 * This is for OpenApi Util.
 */
class OpenApiUtilClient
{
    /**
     * Convert all params of body other than type of readable into content.
     *
     * @param Model $body    source Model
     * @param Model $content target Model
     */
    public static function convert($body, $content)
    {
        $map = $body->toMap();
        $map = self::exceptStream($map);
        $newContent = $content::fromMap($map);
        $class = new \ReflectionClass($newContent);
        foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
            $name = $property->getName();
            if (!$property->isStatic()) {
                $content->{$name} = $property->getValue($newContent);
            }
        }
    }

    private static function exceptStream($map)
    {
        if ($map instanceof StreamInterface) {
            return null;
        } elseif (\is_array($map)) {
            $data = [];
            foreach ($map as $k => $v) {
                if (null !== $v) {
                    $item = self::exceptStream($v);
                    if (null !== $item) {
                        $data[$k] = $item;
                    }
                } else {
                    $data[$k] = $v;
                }
            }
            return $data;
        }
        return $map;
    }

    /**
     * Get the string to be signed according to request.
     *
     * @param Request $request which contains signed messages
     *
     * @return string the signed string
     */
    public static function getStringToSign($request)
    {
        $pathname = $request->pathname ?: '';
        $query    = $request->query ?: [];

        $accept      = isset($request->headers['accept']) ? $request->headers['accept'] : '';
        $contentMD5  = isset($request->headers['content-md5']) ? $request->headers['content-md5'] : '';
        $contentType = isset($request->headers['content-type']) ? $request->headers['content-type'] : '';
        $date        = isset($request->headers['date']) ? $request->headers['date'] : '';

        $result = $request->method . "\n" .
            $accept . "\n" .
            $contentMD5 . "\n" .
            $contentType . "\n" .
            $date . "\n";

        $canonicalizedHeaders  = self::getCanonicalizedHeaders($request->headers);
        $canonicalizedResource = self::getCanonicalizedResource($pathname, $query);

        return $result . $canonicalizedHeaders . $canonicalizedResource;
    }

    /**
     * Get signature according to stringToSign, secret.
     *
     * @param string $stringToSign the signed string
     * @param string $secret       accesskey secret
     *
     * @return string the signature
     */
    public static function getROASignature($stringToSign, $secret)
    {
        return base64_encode(hash_hmac('sha1', $stringToSign, $secret, true));
    }

    /**
     * Parse filter into a form string.
     *
     * @param array $filter object
     *
     * @return string the string
     */
    public static function toForm($filter)
    {
        $query = $filter;
        if (null === $query) {
            return '';
        }
        if ($query instanceof Model) {
            $query = $query->toMap();
        }
        $tmp = [];
        foreach ($query as $k => $v) {
            if (0 !== strpos($k, '_')) {
                $tmp[$k] = $v;
            }
        }
        $res = self::flatten($tmp);
        ksort($res);

        return http_build_query($res);
    }

    /**
     * Get timestamp.
     *
     * @return string the timestamp string
     */
    public static function getTimestamp()
    {
        return gmdate('Y-m-d\\TH:i:s\\Z');
    }

    /**
     * Parse filter into a object which's type is map[string]string.
     *
     * @param array $filter query param
     *
     * @return array the object
     */
    public static function query($filter)
    {
        if (null === $filter) {
            return [];
        }
        $dict = $filter;
        if ($dict instanceof Model) {
            $dict = $dict->toMap();
        }
        $tmp = [];
        foreach ($dict as $k => $v) {
            if (0 !== strpos($k, '_')) {
                $tmp[$k] = $v;
            }
        }

        return self::flatten($tmp);
    }

    /**
     * Get signature according to signedParams, method and secret.
     *
     * @param array  $signedParams params which need to be signed
     * @param string $method       http method e.g. GET
     * @param string $secret       AccessKeySecret
     *
     * @return string the signature
     */
    public static function getRPCSignature($signedParams, $method, $secret)
    {
        $secret    .= '&';
        $strToSign = self::getRpcStrToSign($method, $signedParams);

        $signMethod = 'HMAC-SHA1';

        return self::encode($signMethod, $strToSign, $secret);
    }

    /**
     * Parse object into a string with specified style.
     *
     * @style specified style e.g. repeatList
     *
     * @param mixed  $object  the object
     * @param string $prefix the prefix string
     * @param string $style
     *
     * @return string the string
     */
    public static function arrayToStringWithSpecifiedStyle($object, $prefix, $style)
    {
        if (null === $object) {
            return '';
        }
        if ('repeatList' === $style) {
            return self::toForm([$prefix => $object]);
        }
        if ('simple' == $style || 'spaceDelimited' == $style || 'pipeDelimited' == $style) {
            $strs = self::flatten($object);

            switch ($style) {
                case 'spaceDelimited':
                    return implode(' ', $strs);

                case 'pipeDelimited':
                    return implode('|', $strs);

                default:
                    return implode(',', $strs);
            }
        } elseif ('json' === $style) {
            self::parse($object, $parsed);
            return json_encode($parsed);
        }

        return '';
    }

    /**
     * Transform input as array.
     *
     * @param mixed $input
     *
     * @return array
     */
    public static function parseToArray($input)
    {
        self::parse($input, $result);

        return $result;
    }

    /**
     * Transform input as map.
     *
     * @param mixed $input
     *
     * @return array
     */
    public static function parseToMap($input)
    {
        self::parse($input, $result);

        return $result;
    }

    public static function getEndpoint($endpoint, $useAccelerate, $endpointType = 'public')
    {
        if ('internal' == $endpointType) {
            $tmp      = explode('.', $endpoint);
            $tmp[0]   .= '-internal';
            $endpoint = implode('.', $tmp);
        }
        if ($useAccelerate && 'accelerate' == $endpointType) {
            return 'oss-accelerate.aliyuncs.com';
        }

        return $endpoint;
    }

    /**
     * Encode raw with base16.
     *
     * @param int[] $raw encoding data
     *
     * @return string encoded string
     */
    public static function hexEncode($raw)
    {
        if (is_array($raw)) {
            $raw = Utils::toString($raw);
        }
        return bin2hex($raw);
    }

    /**
     * Hash the raw data with signatureAlgorithm.
     *
     * @param int[]  $raw                hashing data
     * @param string $signatureAlgorithm the autograph method
     *
     * @return array hashed bytes
     */
    public static function hash($raw, $signatureAlgorithm)
    {
        $str = Utils::toString($raw);

        switch ($signatureAlgorithm) {
            case 'ACS3-HMAC-SHA256':
            case 'ACS3-RSA-SHA256':
                $res = hash('sha256', $str, true);
                return Utils::toBytes($res);
            case 'ACS3-HMAC-SM3':
                $res = self::sm3($str);
                return Utils::toBytes(hex2bin($res));
        }

        return [];
    }

    /**
     * Get the authorization.
     *
     * @param Request $request            request params
     * @param string  $signatureAlgorithm the autograph method
     * @param string  $payload            the hashed request
     * @param string  $accesskey          the accessKey string
     * @param string  $accessKeySecret    the accessKeySecret string
     *
     * @return string authorization string
     * @throws \ErrorException
     *
     */
    public static function getAuthorization($request, $signatureAlgorithm, $payload, $accesskey, $accessKeySecret)
    {
        $canonicalURI = $request->pathname ? $request->pathname : '/';
        $query    = $request->query ?: [];
        $method               = strtoupper($request->method);
        $canonicalQueryString = self::getCanonicalQueryString($query);
        $signHeaders          = [];
        foreach ($request->headers as $k => $v) {
            $k = strtolower($k);
            if (0 === strpos($k, 'x-acs-') || 'host' === $k || 'content-type' === $k) {
                $signHeaders[$k] = $v;
            }
        }
        ksort($signHeaders);
        $headers = [];
        foreach ($request->headers as $k => $v) {
            $k = strtolower($k);
            if (0 === strpos($k, 'x-acs-') || 'host' === $k || 'content-type' === $k) {
                $headers[$k] = trim($v);
            }
        }
        $canonicalHeaderString = '';
        ksort($headers);
        foreach ($headers as $k => $v) {
            $canonicalHeaderString .= $k . ':' . trim(self::filter($v)) . "\n";
        }
        if (empty($canonicalHeaderString)) {
            $canonicalHeaderString = "\n";
        }

        $canonicalRequest = $method . "\n" . $canonicalURI . "\n" . $canonicalQueryString . "\n" .
            $canonicalHeaderString . "\n" . implode(';', array_keys($signHeaders)) . "\n" . $payload;
        $strtosign        = $signatureAlgorithm . "\n" . self::hexEncode(self::hash(Utils::toBytes($canonicalRequest), $signatureAlgorithm));
        $signature        = self::sign($accessKeySecret, $strtosign, $signatureAlgorithm);
        $signature        = self::hexEncode($signature);

        return $signatureAlgorithm .
            ' Credential=' . $accesskey .
            ',SignedHeaders=' . implode(';', array_keys($signHeaders)) .
            ',Signature=' . $signature;
    }

    public static function sign($secret, $str, $algorithm)
    {
        $result = '';
        switch ($algorithm) {
            case 'ACS3-HMAC-SHA256':
                $result = hash_hmac('sha256', $str, $secret, true);
                break;
            case 'ACS3-HMAC-SM3':
                $result = self::hmac_sm3($str, $secret, true);
                break;
            case 'ACS3-RSA-SHA256':
                $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . $secret . "\n-----END RSA PRIVATE KEY-----";
                @openssl_sign($str, $result, $privateKey, OPENSSL_ALGO_SHA256);
        }

        return Utils::toBytes($result);
    }

    /**
     * Get encoded path.
     *
     * @param string $path the raw path
     *
     * @return string encoded path
     */
    public static function getEncodePath($path)
    {
        $tmp = explode('/', $path);
        foreach ($tmp as &$t) {
            $t = rawurlencode($t);
        }

        return implode('/', $tmp);
    }

    /**
     * Get encoded param.
     *
     * @param string $param the raw param
     *
     * @return string encoded param
     */
    public static function getEncodeParam($param)
    {
        return rawurlencode($param);
    }

    private static function getRpcStrToSign($method, $query)
    {
        ksort($query);

        $params = [];
        foreach ($query as $k => $v) {
            if (null !== $v) {
                $k = rawurlencode($k);
                $v = rawurlencode($v);
                $params[] = $k . '=' . (string)$v;
            }
        }
        $str = implode('&', $params);

        return $method . '&' . rawurlencode('/') . '&' . rawurlencode($str);
    }

    private static function encode($signMethod, $strToSign, $secret)
    {
        switch ($signMethod) {
            case 'HMAC-SHA256':
                return base64_encode(hash_hmac('sha256', $strToSign, $secret, true));

            default:
                return base64_encode(hash_hmac('sha1', $strToSign, $secret, true));
        }
    }

    /**
     * @param array  $items
     * @param string $delimiter
     * @param string $prepend
     *
     * @return array
     */
    private static function flatten($items = [], $delimiter = '.', $prepend = '')
    {
        $flatten = [];

        foreach ($items as $key => $value) {
            $pos = \is_int($key) ? $key + 1 : $key;

            if ($value instanceof Model) {
                $value = $value->toMap();
            } elseif (\is_object($value)) {
                $value = get_object_vars($value);
            }

            if (\is_array($value) && !empty($value)) {
                $flatten = array_merge(
                    $flatten,
                    self::flatten($value, $delimiter, $prepend . $pos . $delimiter)
                );
            } else {
                if (\is_bool($value)) {
                    $value = true === $value ? 'true' : 'false';
                }
                $flatten[$prepend . $pos] = $value;
            }
        }

        return $flatten;
    }

    private static function getCanonicalizedHeaders($headers, $prefix = 'x-acs-')
    {
        ksort($headers);
        $str = '';
        foreach ($headers as $k => $v) {
            if (0 === strpos(strtolower($k), $prefix)) {
                $str .= $k . ':' . trim(self::filter($v)) . "\n";
            }
        }

        return $str;
    }

    private static function getCanonicalizedResource($pathname, $query)
    {
        if (0 === \count($query)) {
            return $pathname;
        }
        ksort($query);
        $tmp = [];
        foreach ($query as $k => $v) {
            if (!empty($v)) {
                $tmp[] = $k . '=' . $v;
            } else {
                $tmp[] = $k;
            }
        }

        return $pathname . '?' . implode('&', $tmp);
    }

    private static function parse($input, &$output)
    {
        if (null === $input || '' === $input) {
            $output = [];
        }
        $recursive = function ($input) use (&$recursive) {
            if ($input instanceof Model) {
                $input = $input->toMap();
            } elseif (\is_object($input)) {
                $input = get_object_vars($input);
            }
            if (!\is_array($input)) {
                return $input;
            }
            $data = [];
            foreach ($input as $k => $v) {
                $data[$k] = $recursive($v);
            }

            return $data;
        };
        $output    = $recursive($input);
        if (!\is_array($output)) {
            $output = [$output];
        }
    }

    private static function filter($str)
    {
        return str_replace(["\t", "\n", "\r", "\f"], '', $str);
    }

    private static function hmac_sm3($data, $key, $raw_output = false)
    {
        $pack      = 'H' . \strlen(self::sm3('test'));
        $blocksize = 64;
        if (\strlen($key) > $blocksize) {
            $key = pack($pack, self::sm3($key));
        }
        $key  = str_pad($key, $blocksize, \chr(0x00));
        $ipad = $key ^ str_repeat(\chr(0x36), $blocksize);
        $opad = $key ^ str_repeat(\chr(0x5C), $blocksize);
        $hmac = self::sm3($opad . pack($pack, self::sm3($ipad . $data)));

        return $raw_output ? pack($pack, $hmac) : $hmac;
    }

    private static function sm3($message)
    {
        return (new Sm3())->sign($message);
    }

    private static function getCanonicalQueryString($query)
    {
        ksort($query);

        $params = [];
        foreach ($query as $k => $v) {
            if (null === $v) {
                continue;
            }
            $str = rawurlencode($k);
            if ('' !== $v && null !== $v) {
                $str .= '=' . rawurlencode($v);
            } else {
                $str .= '=';
            }
            $params[] = $str;
        }

        return implode('&', $params);
    }
}
