<?php

namespace Api\PhpUtils\Http;
/*
 * Copyright 2011-2012 Hipu Holding Inc, Beijing. All Rights Reserved.
 * DO NOT ALERT OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * 
 * This code is distributed as free software within the company.
 * You can redistribute it and/or modify it without any restriction.
 * 
 * If you need additional information or have any questions,
 * please contact: xukf@hipu.com.
 */

/**
 * Description of HttpUtil
 *
 * @author mengweifu
 * @date 2021-7-12 20:00:00
 */


use Api\PhpUtils\Log\FileLog;
use Api\PhpUtils\Log\Tracer;
use Api\PhpUtils\Mon\MonUtil;

class HttpUtil
{

    // 成功
    private const CODE_SUCCESS = 0;
    // 失败
    private const CODE_FAIL = 999999;

    /**
     * head 请求
     * @param string $url 链接地址
     * @param int $timeout 超时时间(MS)
     * @param int $retries 重试次数
     * @param array $headers 请求头
     * @param string $proxy 代理地址
     * @return array Response
     */
    public static function head(string $url, int $timeout = 10000, int $retries = 1, array $headers = [], string $proxy = ''): array
    {
        return self::curl($url, $timeout, $retries, $headers, false, $proxy, 'HEAD');
    }

    /**
     *
     * @param string $url
     * @param array $params
     * @param int $timeout
     * @param int $retries
     * @param array $headers
     * @param string $proxy
     * @param array $curl_opts
     * @return array
     */
    public static function get(string $url,array $params = [], int $timeout = 10000,int $retries = 1,  array $headers = [], string $proxy = '',array $curl_opts = []): array
    {
        // 增加traceId
        if(is_array($headers)) {
            $headers[] = 'X-TRACE-ID: ' . Tracer::getTraceId();
        }
        $url = $url ."?". http_build_query($params);
        return self::curl($url, $timeout, $retries, $headers, [], $proxy, 'GET', $curl_opts);
    }

    /**
     * 发起 curl put 请求
     * @param string $url url 地址
     * @param mixed $post POST 内容 参见 https://www.php.net/manual/en/function.curl-setopt.php CURLOPT_POSTFIELDS
     * @param int $timeout 超时时间(MS)
     * @param int $retries 重试次数
     * @param array $headers 请求头
     * @param string $proxy 代理地址
     * @return array Response
     */
    public static function put(string $url, $post,int $timeout = 10000,int $retries = 1,array $headers = [],string $proxy = ''): array
    {
        return self::curl($url, $timeout, $retries, $headers, $post, $proxy, 'PUT');
    }

    /**
     * 发起 curl post 请求
     * @param string $url url 地址
     * @param mixed $post POST 内容 参见 https://www.php.net/manual/en/function.curl-setopt.php CURLOPT_POSTFIELDS
     * @param int $timeout 超时时间(MS)
     * @param int $retries 重试次数
     * @param array $headers 请求头
     * @param string $proxy 代理地址
     * @param array $curl_opts curl 参数
     * @return array Response
     */
    public static function post(string $url, $post,int $timeout = 10000,int $retries = 1,array $headers = [], string $proxy = '',array $curl_opts = []): array
    {
        // 增加traceId
        if(is_array($headers)) {
            $headers[] = 'X-TRACE-ID: ' . Tracer::getTraceId();
        }
        return self::curl($url, $timeout, $retries, $headers, $post, $proxy, 'POST', $curl_opts);
    }

    /**
     * 发起 curl delete 请求
     * @param string $url url 地址
     * @param int $timeout 超时时间(MS)
     * @param int $retries 重试次数
     * @param array $headers 请求头
     * @param string $proxy 代理地址
     * @return array Response
     */
    public static function delete(string $url, int $timeout = 10000, int $retries = 1, array $headers = [], string $proxy = ''): array
    {
        return self::curl($url, $timeout, $retries, $headers, false, $proxy, 'DELETE');
    }

    /**
     * 发起 curl 请求
     * @param string $url url 地址
     * @param int $timeout 超时时间(MS)
     * @param int $retries 重试次数
     * @param array $headers 请求头
     * @param mixed $post POST 内容 参见 https://www.php.net/manual/en/function.curl-setopt.php CURLOPT_POSTFIELDS
     * @param string $proxy 代理地址
     * @param string $method 请求方法
     * @param array $curl_opts curl 参数
     * @return array Response
     */
    public static function curl(string $url, int $timeout, int $retries, array $headers, $post, string $proxy = '', string $method = 'GET', array $curl_opts = []): array
    {

        $ch = self::ch($url, $timeout, $headers, $post, $proxy, $method, $curl_opts);

        $response = self::defaultResponse();

        if (is_resource($ch) === true)
        {
            //返回结果
            $result = false;

            while (($result === false) && ($retries-- > 0))
            {
                $result = curl_exec($ch);
            }

            // { todo
            if (curl_errno($ch) !== 0)
            {
                $GLOBALS['ERROR_MESSAGE'] = curl_error($ch);
            }
            $GLOBALS['HTTP_CODE'] = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            if( isset($GLOBALS['ENABLE_HEADER_REQUEST_TIME']) )
            {
                $GLOBALS['BELENDER_CURL_INFO'] = curl_getinfo($ch);
                //在返回header中添加blender响应时间
                $GLOBALS['DEPENDENT-REQUEST-TIME'] = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME),4).'|'.round(curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME),4).'|'.round(curl_getinfo($ch, CURLINFO_CONNECT_TIME),4);
                $GLOBALS['DEPENDENT-REQUEST'] = array('url'=>$url, 'method'=>$method, 'body'=>$post);
                $GLOBALS['DEPENDENT-URI'] = parse_url($url, PHP_URL_PATH);
                if ($GLOBALS['HTTP_CODE'] === 0)
                {
                    $GLOBALS['DEPENDENT-STATUS'] = 'timeout';
                }
                else if ($GLOBALS['HTTP_CODE'] !== 200)
                {
                    $GLOBALS['DEPENDENT-STATUS'] = '5xx';
                }else
                {
                    $GLOBALS['DEPENDENT-STATUS'] = 'normal';
                }
            }
            unset($GLOBALS['ENABLE_HEADER_REQUEST_TIME']);
            // }
            // 监控
            self::addMon(
                $url,
                $GLOBALS['HTTP_CODE'] ?? -999,
                round(curl_getinfo($ch, CURLINFO_TOTAL_TIME),4) * 1000
            );

            // 日志
            self::addLog($method, $url, $post, $result);
            // 返回结果
            $response = self::createResponse($result, $ch);

            curl_close($ch);
        }

        return $response;
    }

    /**
     * 生产 curl句柄
     * @param string $url
     * @param int $timeout
     * @param array $headers
     * @param mixed $post
     * @param string $proxy
     * @param string $method
     * @param array $curl_opts
     * @return false|mixed|resource
     */
    private static function ch(string $url,int $timeout,array $headers = [], $post, string $proxy = '', string $method = 'GET', array $curl_opts = [])
    {
        $ch = curl_init($url);

        // CURLOPT_POSTFIELDS 不支持多维数组
        if (is_array($post) && count($post) != count($post, COUNT_RECURSIVE)) {
            $post =  http_build_query($post);
        }

        if (is_resource($ch) === true)
        {
            curl_setopt($ch, CURLOPT_FAILONERROR,           true);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION,        true);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER,        true);
            curl_setopt($ch, CURLOPT_TCP_NODELAY,           true);
            curl_setopt($ch, CURLOPT_NOSIGNAL,              true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,        false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,        false);
            curl_setopt($ch, CURLOPT_BUFFERSIZE,            10240);
            curl_setopt($ch, CURLOPT_TIMEOUT_MS,            $timeout);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS,     $timeout);
            curl_setopt($ch, CURLOPT_HTTP_VERSION,          CURL_HTTP_VERSION_1_0);

            if (! empty($headers))
            {
                curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            }
            if ($proxy)
            {
                curl_setopt($ch, CURLOPT_PROXY, $proxy);
            }
            if (!empty($curl_opts))
            {
                foreach ($curl_opts as $opt => $val)
                {
                    curl_setopt($ch, $opt, $val);
                }
            }
            switch ($method) {
                case 'HEAD':
                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'HEAD');
                    break;
                case 'GET':
                    break;
                case 'POST':
                    curl_setopt($ch, CURLOPT_POST, true);
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
                    break;
                case 'DELETE':
                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
                    break;
                case 'PUT':
                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
                    break;
                default:
                    die('httpUtil bad method');
                    break;
            }
        }
        return $ch;
    }

    /**
     * 并发 curl
     * @param array $urls 链接数组
     * @param array $timeouts 超时时间数组(MS)
     * @param array $posts  POST数组 内容 参见 https://www.php.net/manual/en/function.curl-setopt.php CURLOPT_POSTFIELDS
     * @param array $headers header 数组
     * @param array $proxies 代理地址数组
     * @param array $curl_opts curl opts 数组
     * @return array
     */
    public static function concurrencyPost(array $urls = [], array $posts = [], array $timeouts = [], array $headers = [],array $proxies = [], array $curl_opts = []): array
    {
        $methods  = [];
        foreach ($urls as $k => $url) {
            $methods[$k] = 'POST';
        }
        return self::concurrencyCurl($urls, $methods, $posts, $timeouts , $headers, $proxies, $curl_opts);
    }

    /**
     * 并发 curl
     * @param array $urls 链接数组
     * @param array $timeouts 超时时间数组(MS)
     * @param array $headers header 数组
     * @param array $proxies 代理地址数组
     * @param array $curl_opts curl opts 数组
     * @return array
     */
    public static function concurrencyGet(array $urls = [],$params = [],array $timeouts = [], array $headers = [], array $proxies = [], array $curl_opts = []): array
    {
        $methods = $posts = [];
        foreach ($urls as $k => $url) {
            $urls[$k] = $url ."?". http_build_query($params[$k]);
            $methods[$k] = 'GET';
            $posts[$k] = false;
        }

        return self::concurrencyCurl($urls, $methods, $posts, $timeouts , $headers, $proxies, $curl_opts);
    }

    /**
     * 并发 curl
     * @param array $urls 链接数组
     * @param array $methods 请求方法数组 (GET POST ...)
     * @param array $posts  POST数组 内容 参见 https://www.php.net/manual/en/function.curl-setopt.php CURLOPT_POSTFIELDS
     * @param array $timeouts 超时时间数组(MS)
     * @param array $headers header 数组
     * @param array $proxies 代理地址数组
     * @param array $curl_opts curl opts 数组
     * @return array
     */
    public static function concurrencyCurl(array $urls = [], array $methods = [], array $posts = [], array $timeouts = [], array $headers = [], array $proxies = [], array $curl_opts = []): array
    {
        //step one 初始化一个批处理handle
        $mh = curl_multi_init();

        //step two 往批处理handle 添加curl_init来的子handle
        foreach ($urls as $i => $url)
        {
            $ch = self::CH((string)$url, (int)$timeouts[$i], (array)$headers[$i], $posts[$i], (string)$proxies[$i], (string)$methods[$i], (array)$curl_opts[$i]);
            $conn[$i] = $ch;

            curl_multi_add_handle($mh, $conn[$i]);
        }

        //step three 并发执行，直到请求全部结束
        do {
            curl_multi_exec($mh, $active);
        } while ($active);

        $response = [];
        //step four 获取结果，移除并close handle
        foreach ($urls as $i => $url)
        {
            $response_content = curl_multi_getcontent($conn[$i]);
            $response[$i] =  self::createResponse($response_content, $conn[$i]);

            // 监控
            self::addMon(
                $url,
                curl_errno($conn[$i]) === 0 ? curl_getinfo($conn[$i], CURLINFO_HTTP_CODE) : -999,
                round(curl_getinfo($conn[$i], CURLINFO_TOTAL_TIME),4) * 1000
            );

            // 日志
            self::addLog($methods[$i], $url, $posts[$i], $response_content);

            curl_multi_remove_handle($mh,$conn[$i]);
            curl_close($conn[$i]);
        }

        curl_multi_close($mh);

        //step five 结果返回
        return $response;
    }

    /*
     * 监控打点，模块名默认为第三方服务url，指标为请求返回的HTTP_code与总响应时间
     */
    private static function addMon(string $url,int $http_code,float $request_time) {
        MonUtil::proxyMon($url, $http_code, '', $request_time);
    }

    /**
     * 添加log
     * @param $method
     * @param $urls
     * @param $params
     * @param $response
     */
    private static function addLog($method, $urls, $params, $response)
    {
        //过滤无意义的日志
        $log = true;
        $excludes = config('request', 'log.exclude');
        if (!empty($excludes)) {
            foreach ($excludes as $exclude) {
                if (is_array($urls)) {
                    foreach ($urls as $url) {
                        if (strpos($url, $exclude) !== false) {
                            $log = false;
                            break;
                        }
                    }
                } else {
                    if (strpos($urls, $exclude) !== false) {
                        $log = false;
                        break;
                    }
                }
            }
        }
        if (!empty($log)) {
            //
            $params = self::change($params);
            FileLog::info($method, json_encode([
                'url' => $urls,
                'params' => $params,
                'response' => $response
                ]));
        }
    }

    /**
     * 过滤resource类型字段
     * @param mixed  $params array or string resource
     * @return array
     */
    private static function change($params)
    {
        if (is_array($params)) {
            foreach ($params as $k => $v) {
                if (is_array($v)) {
                    $params[$k] = self::change($v);
                } else {
                    if (strpos(gettype($v), 'resource') !== false) {
                        $params[$k] = gettype($v);
                    }
                }
            }
        }
        else {
            if (strpos(gettype($params), 'resource') !== false) {
                $params = gettype($params);
            }
        }

        return $params;
    }

    /**
     * 生成返回结果
     * @param string $response_content 请求返回结果
     * @param resource $ch_handle cURL resource
     * @return array
     */
    private static function createResponse(string $response_content, $ch_handle): array
    {
        return [
            'code'      => curl_errno($ch_handle) === 0 ? self::CODE_SUCCESS : self::CODE_FAIL,
            'response'  => empty(json_decode($response_content, true)) ? $response_content : json_decode($response_content, true),
            'http_code' => curl_getinfo($ch_handle, CURLINFO_HTTP_CODE),
            'msg'       => curl_error($ch_handle),
        ];
    }

    /**
     * 默认返回
     * @return array
     */
    private static function defaultResponse(): array
    {
        return [
            'code'      => self::CODE_FAIL,
            'response'  => '',
            'http_code' => 0,
            'msg'       => '',
        ];
    }
}
