<?php

namespace Acms\Services\Http;

use AcmsLogger;
use Composer\CaBundle\CaBundle;
use CurlHandle;

class Engine
{
    /**
     * @var array
     */
    protected $responseHeaders;

    /**
     * @var string
     */
    protected $responseBody;

    /**
     * @var CurlHandle
     */
    protected $curl;

    /**
     * Constructor
     */
    public function __construct()
    {
    }

    /**
     * init request
     *
     * @param string $uri
     * @param string $method
     *
     * @return self
     */
    public function init($uri, $method = "get")
    {
        $method = strtoupper($method);
        $this->curl = curl_init();
        curl_setopt($this->curl, CURLOPT_URL, $uri);
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method);
        if (isDebugMode()) {
            curl_setopt($this->curl, CURLOPT_VERBOSE, true);
        }
        curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true); // Locationを辿る
        curl_setopt($this->curl, CURLOPT_MAXREDIRS, 10);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, ['Expect:']);
        curl_setopt($this->curl, CURLOPT_TIMEOUT, MAX_EXECUTION_TIME);

        $this->setCurlProxy($this->curl);
        $this->setCurlOption();

        return $this;
    }

    /**
     * set request headers
     *
     * @param array $headers
     */
    public function setRequestHeaders($headers = [])
    {
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, $headers);
    }

    /**
     * set post data
     *
     * @param array $data
     *
     * @return self
     */
    public function setPostData($data = [])
    {
        curl_setopt($this->curl, CURLOPT_POST, true);
        curl_setopt($this->curl, CURLOPT_POSTFIELDS, $data);

        return $this;
    }

    /**
     * send request
     *
     * @return self
     */
    public function send()
    {
        $response = curl_exec($this->curl);
        $header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE);
        $errno = curl_errno($this->curl);
        $error = curl_error($this->curl);

        if (CURLE_OK !== $errno) {
            AcmsLogger::debug($error, [
                'errno' => $errno,
            ]);
            throw new \RuntimeException($error, $errno);
        }
        $this->responseHeaders = $this->getHeadersFromCurlResponse(substr($response, 0, $header_size));
        $this->responseBody = substr($response, $header_size);

        return $this;
    }

    /**
     * get response header
     *
     * @param string|false $name
     *
     * @return string|array
     */
    public function getResponseHeader($name = false)
    {
        if ($name === false) {
            return $this->responseHeaders;
        }
        $name = strtolower($name);
        if (isset($this->responseHeaders[$name])) {
            return $this->responseHeaders[$name];
        }
        return '';
    }

    /**
     * get response body
     *
     * @return string
     */
    public function getResponseBody()
    {
        return $this->responseBody;
    }

    /**
     * get headers from curl response
     *
     * @param $header_string
     *
     * @return array
     */
    protected function getHeadersFromCurlResponse($header_string)
    {
        $headers = [];

        foreach (preg_split("/(\r|\n|\r\n)/", $header_string) as $i => $line) {
            if ($i === 0) {
                $headers['http_code'] = $line;
                if (preg_match('/HTTP\/[1|2]\.[0|1|x] ([0-9]{3})/', $line, $matches)) {
                    $headers['status_code'] = $matches[1];
                }
            } elseif (strpos($line, ':') !== false) {
                list ($key, $value) = explode(': ', $line);
                $headers[strtolower($key)] = $value;
            }
        }
        return $headers;
    }

    /**
     * @return void
     */
    protected function setCurlOption()
    {
        $this->setCurlSsl($this->curl);
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_HEADER, true);
    }

    /**
     * cURL に安全な TLS 検証オプションを設定する。
     *
     * VERIFYPEER=true / VERIFYHOST=2 を既定とし、CA bundle を明示することで、
     * 環境に CA が見つからず検証が失敗するのを防ぐ。Webhook / オンラインアップデートなど
     * 他の外部通信からも facade 経由（Http::setCurlSsl）で共有する。
     *
     * @param CurlHandle $ch
     * @return void
     */
    public function setCurlSsl(CurlHandle $ch): void
    {
        if (!$this->isTlsVerificationEnabled()) {
            // .env の HTTP_SSL_VERIFY を明示的に無効化した場合のみここに来る
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
            return;
        }
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

        $caBundle = $this->getCaBundlePath();
        if ($caBundle !== null) {
            // ディレクトリは CAPATH、ファイルは CAINFO と cURL の要求が分かれるため振り分ける
            if (is_dir($caBundle)) {
                curl_setopt($ch, CURLOPT_CAPATH, $caBundle);
            } else {
                curl_setopt($ch, CURLOPT_CAINFO, $caBundle);
            }
        }
    }

    /**
     * TLS 検証の有効可否を設定値から決める（純粋関数）。
     *
     * 既定は有効。環境変数 HTTP_SSL_VERIFY を偽値（false / 0 / off 等）にしたときだけ
     * 無効化する。本番／開発は区別せず、この設定値のみで判断する。env 依存を持たせず
     * 単体テスト可能にするため、判定材料は引数で受け取る。
     *
     * @param string $verifySetting HTTP_SSL_VERIFY の値（'true' / 'false' / '1' / '0' 等）
     * @return bool 検証する場合 true
     */
    public static function shouldVerifyTls(string $verifySetting): bool
    {
        return filter_var($verifySetting, FILTER_VALIDATE_BOOLEAN);
    }

    /**
     * 現在の実行環境（.env の HTTP_SSL_VERIFY）から TLS 検証の有効可否を解決する。
     *
     * 生 cURL は setCurlSsl() が内部で使うが、embed など自前の CurlHandle を持たない
     * トランスポートも同一の設定を共有できるよう public にしている。
     *
     * @return bool
     */
    public function isTlsVerificationEnabled(): bool
    {
        // 既定は 'true'（検証有効）。Redis の CACHE_REDIS_SSL_VERIFY_PEER と同じ .env 方式。
        return self::shouldVerifyTls(env('HTTP_SSL_VERIFY', 'true'));
    }

    /**
     * cURL に渡す CA bundle のパスを解決する。
     *
     * システムの CA bundle が見つからない環境でも composer/ca-bundle 同梱の cacert.pem に
     * フォールバックするため、検証を有効化しても環境差で失敗しにくくなる。解決に失敗した
     * 場合は null を返し、cURL のビルトイン既定 CA に委ねる（検証自体は維持する）。
     *
     * @return non-empty-string|null CA bundle のファイルまたはディレクトリのパス。解決不能なら null
     */
    public function getCaBundlePath(): ?string
    {
        try {
            $path = CaBundle::getSystemCaRootBundlePath();
        } catch (\Throwable) {
            return null;
        }

        return $path !== '' ? $path : null;
    }

    /**
     * cURLプロキシを設定する
     *
     * @param CurlHandle $ch
     * @return void
     */
    public function setCurlProxy(CurlHandle $ch): void
    {
        $proxy_port = defined('PROXY_PORT') ? PROXY_PORT : '';
        $proxy_ip = defined('PROXY_IP') ? PROXY_IP : '';
        $is_ip = false;
        if ($proxy_ip !== '') {
            $ip = (string)preg_replace('/^https?:\/\//', '', $proxy_ip);
            $ip = (string)preg_replace('/:\d+$/', '', $ip);
            $is_ip = (
                filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false ||
                filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false
            );
        }
        if (is_numeric($proxy_port) && $is_ip) {
            curl_setopt($ch, CURLOPT_PROXYPORT, (int)$proxy_port);
            curl_setopt($ch, CURLOPT_PROXY, $proxy_ip);
        }
    }
}
