<?php
/**
 * MainWP monitor site.
 *
 * @package MainWP/Dashboard
 */

namespace MainWP\Dashboard;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class MainWP_Uptime_Monitoring_Connect
 *
 * @package MainWP\Dashboard
 */
class MainWP_Uptime_Monitoring_Connect { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
    // phpcs:disable WordPress.WP.AlternativeFunctions
    const DOWN    = 0;
    const UP      = 1;
    const PENDING = 2;
    const FIRST   = 3;

    const RETRY      = 10;
    const NOTALLOWED = 11;


    const TIMEOUTED_ERROR   = 20;
    const CERT_ERROR        = 21;
    const RESOLVEHOST_ERROR = 22;
    const UNDEFINED_ERROR   = 23;

    /**
     * The single instance of the class
     *
     * @var mixed Default null
     */
    protected static $instance = null;

    /**
     * Get instance.
     *
     *  @return mixed
     */
    public static function instance() {
        if ( null === static::$instance ) {
            static::$instance = new self();
        }
        return static::$instance;
    }

    /**
     * Check multi uptime monitors.
     *
     * @param  array $monitors object array monitors.
     * @param  array $glo_settings global settings.
     * @return mixed
     */
    public function check_monitors( &$monitors, $glo_settings ) {

        $disabled_functions = ini_get( 'disable_functions' );

        /**
         * Apply disable check multi exec.
         *
         * @since 5.3
         */
        $disable_multi_exec = apply_filters( 'mainwp_fetch_uptime_disable_check_multi_exec', false );

        if ( ! $disable_multi_exec && ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) ) {
            $this->check_multi_monitors( $monitors, $glo_settings );
        } else {
            $this->check_uptime_monitors( $monitors, $glo_settings );
        }
    }

    /**
     * Check multi uptime monitors.
     *
     * @param  array $monitors object array monitors.
     * @param  array $glo_settings global settings.
     * @return mixed
     */
    public function check_multi_monitors( &$monitors, $glo_settings ) {
        $output                  = new \stdClass();
        $output->global_settings = $glo_settings;
        static::fetch_uptime_urls( $monitors, array( &$this, 'handle_response_fetch_uptime' ), $output );
    }

    /**
     * Check uptime monitors.
     *
     * @param  array $monitors object array monitors.
     * @param  array $glo_settings glo settings.
     * @return mixed
     */
    public function check_uptime_monitors( &$monitors, $glo_settings ) {
        MainWP_System_Utility::set_time_limit( 3600 );
        $chunkSize = apply_filters( 'mainwp_fetch_uptime_chunk_size_urls', 10 );
        $i         = 0;
        foreach ( $monitors as $monitor ) {
            ++$i;
            $this->fetch_uptime_monitor( $monitor, $glo_settings );
            if ( 0 === $i % $chunkSize ) {
                sleep( 3 );
            }
        }
    }

    /**
     * Method fetch uptime monitor.
     *
     * @param  mixed $monitor monitor.
     * @param  mixed $global_settings global settings.
     * @param  bool  $second_try second try.
     * @param  array $params params.
     *
     * @return mixed
     */
    public function fetch_uptime_monitor( &$monitor, $global_settings = array(), $second_try = false, $params = array() ) { //phpcs:ignore -- NOSONAR - complexity.

        $mo_url = static::get_apply_monitor_url( $monitor );

        $start = microtime( true );

        $agent = static::get_user_agent();

        // Initialize curl session.
        $ch = curl_init();

        $proxy = new \WP_HTTP_Proxy();

        if ( $proxy->is_enabled() && $proxy->send_through_proxy( $mo_url ) ) {
            curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
            curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
            curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );

            if ( $proxy->use_authentication() ) {
                curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
                curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
            }
        }
        curl_setopt( $ch, CURLOPT_URL, $mo_url );

        $mo_apply_type   = static::get_apply_setting( 'type', $monitor->type, $global_settings, 'useglobal', 'http' );
        $mo_apply_method = static::get_apply_setting( 'method', $monitor->method, $global_settings, 'useglobal', 'get' );
        $mo_apply_method = strtolower( $mo_apply_method );

        if ( ! empty( $params['check_http_site'] ) ) {
            $chk_http_method = get_option( 'mainwp_check_http_response_method', 'head' );
            $chk_http_method = in_array( $chk_http_method, array( 'get', 'head' ) ) ? $chk_http_method : 'head';
            $mo_apply_method = $chk_http_method;
        }

        if ( 'head' !== $mo_apply_method ) {
            curl_setopt( $ch, CURLOPT_POST, 'post' === $mo_apply_method ? true : false );
        }

        if ( 'post' === $mo_apply_method ) {
            $postdata = array( 'time' => time() );
            $body     = http_build_query( $postdata );
            curl_setopt( $ch, CURLOPT_POSTFIELDS, $body );

            $headers = array( 'Content-Length' => strlen( $body ) );
            $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, false, $monitor );

            if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
                $headers = \WpOrg\Requests\Requests::flatten( $headers );
            } else {
                $headers = \Requests::flatten( $headers );
            }
            curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
        }

        if ( 'ping' === $mo_apply_type || 'head' === $mo_apply_method ) {
            curl_setopt( $ch, CURLOPT_NOBODY, true ); // We only care about the response code, not the content.
        }

        curl_setopt( $ch, CURLOPT_HEADER, true );
        curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, strtoupper( $mo_apply_method ) );

        if ( 'get' === $mo_apply_method ) {
            // to fix Content-Length.
            curl_setopt(
                $ch,
                CURLOPT_HTTPHEADER,
                array(
                    'Content-Length:', // removes the Content-Length header.
                )
            );
        }

        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); // We want to get the output as a string.
        curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
        curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );

        $http_user = '';
        $http_pass = '';

        if ( property_exists( $monitor, 'http_user' ) ) {
            // MWP-1548: $monitor here carries http_user / http_pass joined
            // from the wp table; decrypt at the boundary so the outbound
            // CURLOPT_USERPWD gets plaintext.
            $http_user = MainWP_Credential_Storage::decrypt_credential( $monitor->http_user );
        }
        if ( property_exists( $monitor, 'http_pass' ) ) {
            $http_pass = MainWP_Credential_Storage::decrypt_credential( $monitor->http_pass );
        }

        if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
            $http_pass = stripslashes( $http_pass );
            curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
        }

        $ssl_verifyhost    = false;
        $verifyCertificate = isset( $monitor->verify_certificate ) ? (int) $monitor->verify_certificate : null;
        if ( null !== $verifyCertificate ) {
            if ( 1 === $verifyCertificate ) {
                $ssl_verifyhost = true;
            } elseif ( 2 === $verifyCertificate ) {
                if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
                    $ssl_verifyhost = true;
                }
            }
        } elseif ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
            $ssl_verifyhost = true;
        }

        if ( $ssl_verifyhost ) {
            curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
            curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
        } else {
            curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // NOSONAR.
            curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // NOSONAR.
        }

        curl_setopt( $ch, CURLOPT_SSLVERSION, $monitor->ssl_version );

        $mo_apply_timeout = static::get_apply_setting( 'timeout', (int) $monitor->timeout, $global_settings, -1, 60 );

        curl_setopt( $ch, CURLOPT_TIMEOUT, $mo_apply_timeout ); // seconds.

        MainWP_Utility::end_session();

        // Execute the curl request.
        $data = curl_exec( $ch );

        // Get the HTTP response code.
        $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );

        $http_error = '';
        if ( curl_errno( $ch ) ) {
            $http_error = curl_error( $ch );
        }

        $realurl = curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );

        $host   = wp_parse_url( ( empty( $realurl ) ? $mo_url : $realurl ), PHP_URL_HOST );
        $ip     = false;
        $target = false;

        // Ask only for the record types read below. dns_get_record() defaults to DNS_ANY, which
        // most resolvers now refuse or answer with a stub (RFC 8482), so it buys retries and
        // timeouts instead of answers. Names that exist only in the hosts file are not resolved
        // here at all -- dns_get_record() never reads the hosts file -- they fall through to the
        // gethostbynamel() call below.
        $found     = false;
        $dnsRecord = @dns_get_record( $host, DNS_A | DNS_AAAA | DNS_CNAME ); //phpcs:ignore --ok.

        if ( false !== $dnsRecord && is_array( $dnsRecord ) ) {
            if ( ! isset( $dnsRecord['ip'] ) ) {
                foreach ( $dnsRecord as $dnsRec ) {
                    if ( isset( $dnsRec['ip'] ) ) {
                        $ip = $dnsRec['ip'];
                        break;
                    }
                }
            } else {
                $ip = $dnsRecord['ip'];
            }

            if ( ! isset( $dnsRecord['host'] ) ) {
                foreach ( $dnsRecord as $dnsRec ) {
                    if ( $dnsRec['host'] === $host ) {
                        if ( 'CNAME' === $dnsRec['type'] ) {
                            $target = $dnsRec['target'];
                        }
                        $found = true;
                        break;
                    }
                }
            } else {
                $found = ( $dnsRecord['host'] === $host );
                if ( 'CNAME' === $dnsRecord['type'] ) {
                    $target = $dnsRecord['target'];
                }
            }
        }

        if ( false === $ip ) {
            $ip = gethostbynamel( $host );
        }
        if ( ( false !== $target ) && ( $target !== $host ) ) {
            $host .= ' (CNAME: ' . $target . ')';
        }

        // Close the curl session.
        curl_close( $ch );

        $down_count = 0;
        $set_retry  = false;
        $is_pending = false;

        $retry_later   = static::is_http_code_type( 'retry', $http_code );
        $is_notallowed = static::is_http_code_type( 'notallowed', $http_code );

        if ( $retry_later && empty( $monitor->issub ) ) {
            $max_retries = static::get_apply_setting( 'maxretries', (int) $monitor->maxretries, $global_settings, -1, 0 );
            if ( $max_retries > 0 && $monitor->retries < $max_retries ) {
                $is_pending = true;
                ++$down_count;
                $set_retry = true;
            }
        } elseif ( ! $is_notallowed && empty( $data ) && 'ping' !== $mo_apply_type && ! $second_try ) {
            usleep( 200000 );
            $this->fetch_uptime_monitor( $monitor, $global_settings, true, $params );
        }

        $output                  = new \stdClass();
        $output->global_settings = $global_settings;

        $resp_info = array(
            'http_code'        => $http_code,
            'http_error'       => $http_error,
            'down_count'       => $down_count,
            'retry'            => $set_retry,
            'is_pending'       => $is_pending ? 1 : 0,
            'start'            => $start,
            'end'              => microtime( true ),
            'use_monitor_type' => $mo_apply_type,
            'use_method'       => $mo_apply_method,
            'use_timeout'      => $mo_apply_timeout,
        );

        $output->requests_info                         = array();
        $output->requests_info[ $monitor->monitor_id ] = $resp_info;

        $handle_data = $this->handle_response_fetch_uptime( $data, $monitor, $output, $params );

        // data compatible with data from try_visit().
        $compatible_data = array(
            'host'               => $host,
            'httpCode'           => $http_code,
            'httpCodeString'     => MainWP_Utility::get_http_codes( $http_code ),
            'check_offline_time' => is_array( $handle_data ) && isset( $handle_data['check_offline_time'] ) ? $handle_data['check_offline_time'] : time(),
        );

        if ( false !== $ip ) {
            $compatible_data['ip'] = $ip;
            $found                 = true;
        }
        $compatible_data['error'] = '' === $http_error && false === $found ? 'Invalid host.' : $http_error;

        if ( is_array( $handle_data ) && isset( $handle_data['new_uptime_status'] ) ) {
            $compatible_data['new_uptime_status'] = $handle_data['new_uptime_status'];
        }

        return $compatible_data;
    }

    /**
     * Fetch uptime urls.
     *
     * @param  array  $websites websites.
     * @param  array  $handler callable.
     * @param  object $output output.
     * @param  array  $params params.
     * @return mixed
     */
    public static function fetch_uptime_urls( &$websites, $handler, &$output, $params = array() ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity -- NOSONAR - complex function. Current complexity is the only way to achieve desired results, pull request solutions appreciated.

        if ( ! is_array( $websites ) || empty( $websites ) ) {
            return false;
        }

        if ( ! is_array( $params ) ) {
            $params = array();
        }

        if ( ! is_object( $output ) ) {
            $output = new \stdClass();
        }

        $global_settings = $output->global_settings;

        $chunkSize = apply_filters( 'mainwp_fetch_uptime_chunk_size_urls', 10 );

        if ( count( $websites ) > $chunkSize ) {
            $total = count( $websites );
            $loops = ceil( $total / $chunkSize );
            for ( $i = 0; $i < $loops; $i++ ) {
                $newSites = array_slice( $websites, $i * $chunkSize, $chunkSize, true );
                static::fetch_uptime_urls( $newSites, $handler, $output, $params );
                sleep( 3 );
            }
            return false;
        }

        $agent = static::get_user_agent();
        $mh    = curl_multi_init();

        $disabled_functions    = ini_get( 'disable_functions' );
        $handleToWebsite       = array();
        $requestUrls           = array();
        $requestHandles        = array();
        $output->requests_info = array();

        foreach ( $websites as $website ) {

            $http_user = '';
            $http_pass = '';

            MainWP_Logger::instance()->log_uptime_check( 'Schedule uptime checks: [siteid=' . $website->id . '] :: [url=' . $website->url . ' :: [monitorid=' . $website->monitor_id . '] :: [monitor-active=' . $website->active . ']' );

            $mo_url = static::get_apply_monitor_url( $website );

            if ( property_exists( $website, 'http_user' ) ) {
                // MWP-1548: decrypt at boundary for outbound Basic Auth.
                $http_user = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
            }
            if ( property_exists( $website, 'http_pass' ) ) {
                $http_pass = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
            }

            $ch = curl_init();

            $proxy = new \WP_HTTP_Proxy();
            if ( $proxy->is_enabled() && $proxy->send_through_proxy( $mo_url ) ) {
                curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
                curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
                curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );

                if ( $proxy->use_authentication() ) {
                    curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
                    curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
                }
            }

            curl_setopt( $ch, CURLOPT_URL, $mo_url );
            curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
            curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );

            $mo_apply_type   = static::get_apply_setting( 'type', $website->type, $global_settings, 'useglobal', 'http' );
            $mo_apply_method = static::get_apply_setting( 'method', $website->method, $global_settings, 'useglobal', 'get' );
            $mo_apply_method = strtolower( $mo_apply_method );

            if ( 'head' !== $mo_apply_method ) {
                curl_setopt( $ch, CURLOPT_POST, 'post' === $mo_apply_method ? true : false );
            }

            if ( 'post' === $mo_apply_method ) {
                $postdata = array( 'time' => time() );
                $body     = http_build_query( $postdata );
                curl_setopt( $ch, CURLOPT_POSTFIELDS, $body );

                $headers = array( 'Content-Length' => strlen( $body ) );
                $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, false, $monitor );

                if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
                    $headers = \WpOrg\Requests\Requests::flatten( $headers );
                } else {
                    $headers = \Requests::flatten( $headers );
                }
                curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
            }

            if ( 'ping' === $mo_apply_type || 'head' === $mo_apply_method ) {
                curl_setopt( $ch, CURLOPT_NOBODY, true ); // We only care about the response code, not the content.
            }

            curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, strtoupper( $mo_apply_method ) );

            if ( 'get' === $mo_apply_method ) {
                // to fix Content-Length.
                curl_setopt(
                    $ch,
                    CURLOPT_HTTPHEADER,
                    array(
                        'Content-Length:', // removes the Content-Length header.
                    )
                );
            }

            curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
            curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
            curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
            curl_setopt( $ch, CURLOPT_ENCODING, 'none' );

            if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
                $http_pass = stripslashes( $http_pass );
                curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
            }

            $ssl_verifyhost    = false;
            $verifyCertificate = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : null;
            if ( null !== $verifyCertificate ) {
                if ( 1 === $verifyCertificate ) {
                    $ssl_verifyhost = true;
                } elseif ( 2 === $verifyCertificate ) {
                    if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
                        $ssl_verifyhost = true;
                    }
                }
            } elseif ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
                $ssl_verifyhost = true;
            }

            if ( $ssl_verifyhost ) {
                curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
                curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
            } else {
                curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // NOSONAR.
                curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // NOSONAR.
            }

            curl_setopt( $ch, CURLOPT_SSLVERSION, $website->ssl_version );

            if ( is_object( $website ) && property_exists( $website, 'id' ) ) {
                $http_version = apply_filters( 'mainwp_curl_http_version', false, $website->id );
                if ( false !== $http_version ) {
                    curl_setopt( $ch, CURLOPT_HTTP_VERSION, $http_version );
                }

                $curlopt_resolve = apply_filters( 'mainwp_curl_curlopt_resolve', false, $website->id, $mo_url );
                if ( is_array( $curlopt_resolve ) && ! empty( $curlopt_resolve ) ) {
                    curl_setopt( $ch, CURLOPT_RESOLVE, $curlopt_resolve );
                    curl_setopt( $ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false );
                }
            }

            $mo_apply_timeout = static::get_apply_setting( 'timeout', (int) $website->timeout, $global_settings, -1, 60 );

            curl_setopt( $ch, CURLOPT_TIMEOUT, $mo_apply_timeout ); // seconds.

            MainWP_System_Utility::set_time_limit( 3600 );

            if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
                curl_multi_add_handle( $mh, $ch );
            }

            $resource_id = MainWP_Connect::get_resource_id( $ch );

            $start                             = microtime( true );
            $handleToWebsite[ $resource_id ]   = $website;
            $requestUrls[ $resource_id ]       = $mo_url;
            $requestHandles[ $resource_id ]    = $ch;
            $requestMonitorIds[ $resource_id ] = $website->monitor_id;
            $output->requests_info[ $website->monitor_id ] = array(
                'start'            => $start,
                'use_monitor_type' => $mo_apply_type,
                'use_method'       => $mo_apply_method,
                'use_timeout'      => $mo_apply_timeout,
            );
        }

        if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {

            $lastRun = 0;
            $running = null;

            do {
                if ( 20 < time() - $lastRun ) {
                    MainWP_System_Utility::set_time_limit( 3600 );
                    $lastRun = time();
                }

                curl_multi_exec( $mh, $running );
                curl_multi_select( $mh );
                while ( $info = curl_multi_info_read( $mh ) ) {
                    $data        = curl_multi_getcontent( $info['handle'] );
                    $resource_id = MainWP_Connect::get_resource_id( $info['handle'] );
                    curl_multi_remove_handle( $mh, $info['handle'] );
                    $http_code = curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );

                    $down_count = 0;
                    $set_retry  = false;
                    $is_pending = false;

                    $retry_later   = static::is_http_code_type( 'retry', $http_code );
                    $is_notallowed = static::is_http_code_type( 'notallowed', $http_code );

                    if ( ! empty( $requestUrls[ $resource_id ] ) ) {

                        $mo_apply_type = static::get_apply_setting( 'type', $website->type, $global_settings, 'useglobal', 'http' );

                        $_try_second = false;
                        if ( $retry_later && empty( $website->issub ) ) {
                            $max_retries = static::get_apply_setting( 'maxretries', (int) $website->maxretries, $global_settings, -1, 0 );
                            if ( $max_retries > 0 && $website->retries < $max_retries ) {
                                $is_pending = true;
                                ++$down_count;
                                $set_retry = true;
                            }
                        } elseif ( ! $is_notallowed && empty( $data ) && 'ping' !== $mo_apply_type ) {
                            curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ $resource_id ] );
                            $_try_second = true;
                        }

                        if ( $_try_second ) {
                            curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ $resource_id ] );
                            curl_setopt( $info['handle'], CURLOPT_FRESH_CONNECT, true );
                            curl_setopt( $info['handle'], CURLOPT_FORBID_REUSE, true );
                            curl_multi_add_handle( $mh, $info['handle'] );
                            unset( $requestUrls[ $resource_id ] );
                            continue;
                        }
                    }

                    if ( isset( $requestMonitorIds[ $resource_id ] ) ) {
                        $mo_id      = $requestMonitorIds[ $resource_id ];
                        $http_error = '';
                        if ( curl_errno( $info['handle'] ) ) {
                            $http_error = curl_error( $info['handle'] );
                        }
                        $output->requests_info[ $mo_id ]['http_error'] = $http_error;
                        $output->requests_info[ $mo_id ]['http_code']  = $http_code;

                        $output->requests_info[ $mo_id ]['is_pending'] = $is_pending ? 1 : 0;
                        $output->requests_info[ $mo_id ]['retry']      = $set_retry ? 1 : 0;
                        $output->requests_info[ $mo_id ]['down_count'] = $down_count;
                        $output->requests_info[ $mo_id ]['end']        = microtime( true );
                    }

                    if ( null !== $handler ) {
                        $site = &$handleToWebsite[ $resource_id ];
                        call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
                    }

                    unset( $handleToWebsite[ $resource_id ] );
                    if ( MainWP_Connect::is_valid_curl_handle( $info['handle'] ) ) {
                        curl_close( $info['handle'] );
                    }
                }
                usleep( 10000 );
            } while ( $running > 0 );

            if ( MainWP_Connect::is_valid_curl_handle( $mh ) ) {
                curl_multi_close( $mh );
            }
        } else {
            foreach ( $requestHandles as $ch ) {
                $resource_id = MainWP_Connect::get_resource_id( $ch );
                if ( isset( $handleToWebsite[ $resource_id ] ) ) {
                    $site = &$handleToWebsite[ $resource_id ];
                    static::fetch_single_uptime_url( $ch, $handler, $site, $output, $params );
                    unset( $handleToWebsite[ $resource_id ] );
                }
            }
        }

        return true;
    }

    /**
     * Fetch single curl exec.
     *
     * @param  mixed  $ch ch.
     * @param  mixed  $handler handler.
     * @param  object $website website.
     * @param  object $output output.
     * @param  array  $params params.
     * @param  bool   $try_second try second.
     * @return mixed
     */
    public static function fetch_single_uptime_url( $ch, $handler, $website, $output, $params, $try_second = false ) {  //phpcs:ignore -- NOSONAR - complexity.

        $global_settings = $output->global_settings;

        $data = curl_exec( $ch );

        $mo_id = $website->monitor_id;

        $down_count = 0;
        $set_retry  = false;
        $is_pending = false;

        $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );

        $http_error = '';

        if ( curl_errno( $ch ) ) {
            $http_error = curl_error( $ch );
        }

        $retry_later      = static::is_http_code_type( 'retry', $http_code );
        $is_notallowed    = static::is_http_code_type( 'notallowed', $http_code );
        $mo_apply_type    = static::get_apply_setting( 'type', $website->type, $global_settings, 'useglobal', 'http' );
        $mo_apply_method  = static::get_apply_setting( 'method', $website->method, $global_settings, 'useglobal', 'get' );
        $mo_apply_timeout = static::get_apply_setting( 'timeout', (int) $website->timeout, $global_settings, -1, 60 );

        if ( $retry_later && empty( $website->issub ) ) {
            $max_retries = static::get_apply_setting( 'maxretries', (int) $website->maxretries, $global_settings, -1, 0 );
            if ( $max_retries > 0 && $website->retries < $max_retries ) {
                $is_pending = true;
                ++$down_count;
                $set_retry = true;
            }
        }

        // if is not second try, check to retry now.
        if ( ! $try_second && ! $retry_later && ! $is_notallowed ) {

            $_try_second = false;

            $mo_url = static::get_apply_monitor_url( $website );

            if ( empty( $data ) && 'ping' !== $mo_apply_type ) {
                curl_setopt( $ch, CURLOPT_URL, $mo_url );
                $_try_second = true;
            }

            if ( $_try_second ) {
                static::fetch_single_uptime_url( $ch, $handler, $website, $output, $params, true );
            }
        }

        $output->requests_info[ $mo_id ]['http_code']  = $http_code;
        $output->requests_info[ $mo_id ]['http_error'] = $http_error;

        $output->requests_info[ $mo_id ]['is_pending'] = $is_pending ? 1 : 0;
        $output->requests_info[ $mo_id ]['retry']      = $set_retry ? 1 : 0;
        $output->requests_info[ $mo_id ]['down_count'] = $down_count;

        $output->requests_info[ $mo_id ]['end'] = microtime( true );

        $output->requests_info[ $mo_id ]['use_monitor_type'] = $mo_apply_type;
        $output->requests_info[ $mo_id ]['use_method']       = $mo_apply_method;
        $output->requests_info[ $mo_id ]['use_timeout']      = $mo_apply_timeout;

        if ( ! empty( $handler ) ) {
            call_user_func_array( $handler, array( $data, $website, &$output, $params ) );
        }

        if ( MainWP_Connect::is_valid_curl_handle( $ch ) ) {
            curl_close( $ch );
        }
    }

    /**
     * Method handle response fetch uptime.
     *
     * @param  mixed $data data.
     * @param  mixed $monitor monitor.
     * @param  mixed $output output.
     * @param  array $params params.
     * @return mixed
     */
    public function handle_response_fetch_uptime( $data, $monitor, &$output, $params = array() ) { //phpcs:ignore -- NOSONAR - complexity.

        $request_info = ! empty( $output->requests_info ) && is_array( $output->requests_info ) ? $output->requests_info : array();
        $resp_info    = ! empty( $request_info[ $monitor->monitor_id ] ) && is_array( $request_info[ $monitor->monitor_id ] ) ? $request_info[ $monitor->monitor_id ] : array();

        $resp_info = apply_filters( 'mainwp_uptime_monitor_check_result', $resp_info, $data, $monitor, $output, $params );

        $global_settings = $output->global_settings;

        $http_code  = isset( $resp_info['http_code'] ) ? $resp_info['http_code'] : 0;
        $http_error = isset( $resp_info['http_error'] ) ? $resp_info['http_error'] : '';
        $start      = isset( $resp_info['start'] ) ? $resp_info['start'] : 0;
        $end        = isset( $resp_info['end'] ) ? $resp_info['end'] : 0;

        $down_count     = isset( $resp_info['down_count'] ) ? $resp_info['down_count'] : 0;
        $set_retry      = isset( $resp_info['retry'] ) && $resp_info['retry'] ? true : false;
        $use_mo_method  = isset( $resp_info['use_method'] ) ? $resp_info['use_method'] : '';
        $use_mo_type    = isset( $resp_info['use_monitor_type'] ) ? $resp_info['use_monitor_type'] : '';
        $use_mo_timeout = isset( $resp_info['use_timeout'] ) ? $resp_info['use_timeout'] : '';
        $is_pending     = ! empty( $resp_info['is_pending'] ) ? true : false;

        $ping = $end - $start; // ms - millisecond.

        $parsed = $this->parse_response_status( $http_code, $http_error );

        $status  = $parsed['status'];
        $error   = $parsed['error'];
        $success = $parsed['success'];

        $up_codes = $this->get_up_codes( $monitor, $global_settings );
        $up_codes = ! empty( $up_codes ) ? explode( ',', $up_codes ) : array();

        // check up status codes.
        if ( $set_retry ) {
            if ( $is_pending ) { // to check maxtries.
                $status = static::PENDING;
            } else {
                $status = static::DOWN;
            }
        } elseif ( static::PENDING === $status ) {
            $status = static::DOWN;
        }

        if ( is_string( $data ) && false !== stripos( $data, 'Briefly unavailable for scheduled maintenance' ) ) {
            $status = static::PENDING;
        } elseif ( ! empty( $http_code ) && is_array( $up_codes ) ) {
            if ( in_array( $http_code, $up_codes ) ) {
                $status = static::UP;
            } elseif ( static::UP === $status ) {
                $status = static::DOWN;
            }
        }

        if ( empty( $http_code ) ) {
            $status = static::PENDING;
        }

        $mo_url = static::get_apply_monitor_url( $monitor );

        $_status_str = '';
        if ( static::UP === $status ) {
            $_status_str = 'up!';
        } elseif ( static::PENDING === $status ) {
            $_status_str = 'pending.';
        } else {
            $_status_str = 'down.';
            $status      = static::DOWN; // to fix.
        }

        $heart_msg = "{$http_code} - " . $_status_str;

        $code_msg = MainWP_Utility::get_http_codes( $http_code );

        if ( ! empty( $code_msg ) ) {
            $heart_msg .= " {$code_msg}";
        }

        $mo_apply_type   = static::get_apply_setting( 'type', $monitor->type, $global_settings, 'useglobal', 'http' );
        $use_mo_active   = static::get_apply_setting( 'active', (int) $monitor->active, $global_settings, -1, 0 );
        $use_mo_interval = static::get_apply_setting( 'interval', (int) $monitor->interval, $global_settings, -1, 60 );

        $keywordFound = true; // set it as found.

        if ( 'keyword' === $mo_apply_type ) {
            if ( ! is_string( $data ) ) {
                $data = wp_json_encode( $data );
            }

            $keyword = '';
            if ( 'useglobal' === $monitor->type ) {
                $keyword = ! empty( $global_settings['keyword'] ) ? $global_settings['keyword'] : '';
            } else {
                $keyword = $monitor->keyword;
            }

            if ( ! empty( $keyword ) && ! empty( $data ) ) {
                $keywordFound = false !== stripos( $data, $keyword );
                $heart_msg   .= '. Keyword ' . ( $keywordFound ? 'is' : 'not' ) . ' found.';
            }
        }

        // forced status down.
        if ( ! $keywordFound ) {
            $status = static::DOWN;
        }

        $max_retries = static::get_apply_setting( 'maxretries', (int) $monitor->maxretries, $global_settings, -1, 0 );

        if ( static::DOWN === $status && ! $set_retry && $max_retries > 0 && $monitor->retries < $max_retries ) {
            $status = static::PENDING; // to retry.
            ++$down_count;
            $set_retry = true;
            MainWP_Logger::instance()->log_uptime_check( 'Uptime Down status - set retry :: [site-id=' . (string) $monitor->wpid . ']' );
        }

        if ( ! empty( $error ) ) {
            $heart_msg .= " {$error}";
        }

        $previous_heartbeat = MainWP_DB_Uptime_Monitoring::instance()->get_previous_monitor_heartbeat( $monitor->monitor_id );
        $sec_since_last     = $previous_heartbeat && ! empty( $previous_heartbeat->time ) ? (int) $end - strtotime( $previous_heartbeat->time ) : 1;

        $previous_main_status = $monitor->last_main_status;

        if ( 99 === (int) $previous_main_status ) { // initial status, to set as up, and not send notification for the first time.
            $previous_main_status = static::UP;
        }

        $importance = $this->is_importance_status( (int) $previous_main_status, (int) $status ) ? 1 : 0;

        // get this time first.
        $uptime_init_time = mainwp_get_current_utc_datetime_db( false );

        $db_datetime = mainwp_get_current_utc_datetime_db();

        $heartbeat = array(
            'monitor_id' => $monitor->monitor_id,
            'status'     => $status,
            'importance' => $importance,
            'time'       => $db_datetime,
            'ping_ms'    => (int) ( $ping * 1000 ), // convert seconds to milliseconds.
            'duration'   => $sec_since_last, // milliseconds - timestamp.
            'msg'        => $heart_msg,
            'down_count' => $down_count,
            'http_code'  => $http_code,
        );

        $importance = apply_filters( 'mainwp_uptime_monitoring_check_importance', $importance, $heartbeat );

        $heartbeat['importance'] = $importance;

        $heartbeat = apply_filters( 'mainwp_uptime_monitoring_uptime_data', $heartbeat, $monitor, $previous_heartbeat );

        MainWP_DB_Uptime_Monitoring::instance()->update_heartbeat( $heartbeat );

        $db_timestamp = strtotime( $db_datetime );

        $up_mo = array(
            'monitor_id'     => $monitor->monitor_id,
            'last_status'    => $status,
            'last_http_code' => $http_code,
            'lasttime_check' => $db_timestamp,
        );

        if ( static::UP === $status || static::DOWN === $status ) {
            $up_mo['last_main_status'] = $status; // to save the last important status for notification.
        }

        MainWP_DB_Uptime_Monitoring::instance()->update_wp_monitor( $up_mo );

        if ( $importance ) {
            MainWP_Uptime_Monitoring_Handle::instance()->update_process_monitor_notification( $monitor->monitor_id, $uptime_init_time );
        }

        MainWP_Uptime_Monitoring_Handle::instance()->calc_and_save_site_uptime_stat_hourly_data( $monitor->monitor_id, $heartbeat );

        if ( ! empty( $monitor->wpid ) && empty( $monitor->issub ) && empty( $params['ignore_compatible_save'] ) ) {
            // legacy compatible.
            MainWP_Uptime_Monitoring_Handle::instance()->handle_update_website_legacy_uptime_status(
                $monitor,
                array(
                    'httpCode'           => $http_code,
                    'new_uptime_status'  => $status,
                    'importance'         => $importance,
                    'check_offline_time' => $db_timestamp,
                )
            );
        }

        MainWP_Uptime_Monitoring_Schedule::instance()->update_monitoring_time( $monitor, $set_retry, $status ); // update monitor check info, and retry or not.

        do_action( 'mainwp_uptime_monitoring_after_check_uptime', $heartbeat, $monitor, $previous_heartbeat );

        $debug  = 'Check Uptime - ' . ( $success ? 'succeeded' : 'not succeed' );
        $debug .= ' :: [monitor_url=' . $mo_url . ']';
        if ( static::PENDING === $status ) {
            $debug .= ' :: [status=PENDING]';
        } else {
            $debug .= ' :: [status=' . ( static::UP === $status ? 'UP' : 'DOWN' ) . ']';
        }
        $debug .= ' :: [msg=' . $heart_msg . ']';
        MainWP_Logger::instance()->log_uptime_check( $debug );

        $debug  = ' [siteid=' . $monitor->wpid . '] :: [monitor_id=' . $monitor->monitor_id . ']';
        $debug .= ' :: [monitor_active=' . $monitor->active . '] :: [apply_monitor_active=' . $use_mo_active . ']';
        $debug .= ' :: [monitor_type=' . $monitor->type . '] :: [apply_monitor_type=' . $use_mo_type . ']';
        $debug .= ' :: [monitor_interval=' . $monitor->interval . '] :: [apply_monitor_interval=' . $use_mo_interval . ']';
        $debug .= ' :: [monitor_method=' . $monitor->method . '] :: [apply_monitor_method=' . $use_mo_method . ']';
        $debug .= ' :: [monitor_timeout=' . $monitor->timeout . '] :: [apply_monitor_timeout=' . $use_mo_timeout . ']';
        $debug .= ' :: [ping_ms=' . $heartbeat['ping_ms'] . ']';
        $debug .= ' :: [duration_sec=' . $sec_since_last . ']';
        $debug .= ' :: [retry=' . ( $set_retry ? 1 : 0 ) . ']';
        $debug .= ' :: [retries=' . $monitor->retries . ']';
        $debug .= ' :: [apply maxretries=' . $max_retries . ']';
        $debug .= ' :: [time=' . $db_datetime . ']';

        MainWP_Logger::instance()->log_uptime_check( $debug );

        if ( 301 === (int) $http_code && $importance ) {
            // advanced debug.
            MainWP_Logger::instance()->log_uptime_notice( 'Uptime monitor 301 status :: [http_code=' . $http_code . '] :: [monitor_url=' . $mo_url . '] :: [monitor_id=' . $monitor->monitor_id . '] :: [siteid=' . $monitor->wpid . '] :: [monitor_method=' . $monitor->method . ']' );
        }

        if ( empty( $data ) ) {
            MainWP_Logger::instance()->log_uptime_check( '[data=EMPTY]' );
        }

        // for compatible http status data.
        return array(
            'httpCode'           => $http_code,
            'new_uptime_status'  => $status,
            'importance'         => $importance,
            'check_offline_time' => $db_timestamp,
        );
    }


    /**
     * Method is_http_code_type
     *
     * @param  mixed $type type.
     * @param  mixed $code code.
     * @return bool
     */
    public static function is_http_code_type( $type, $code ) {
        $retry_codes       = array( 429, 502, 503, 504 );
        $not_allowed_codes = array( 405 );

        if ( ( 'retry' === $type && in_array( $code, $retry_codes ) ) || ( 'notallowed' === $type && in_array( $code, $not_allowed_codes ) ) ) {
            return true;
        }

        return false;
    }

    /**
     * Parse response status.
     *
     * @param  mixed $httpCode http code.
     * @param  mixed $error error.
     * @return mixed
     */
    public function parse_response_status( $httpCode, $error = false ) {

        $httpCode     = (int) $httpCode;
        $status       = '';
        $status_error = '';

        if ( ! empty( $error ) ) {
            switch ( $error ) {
                case CURLE_OPERATION_TIMEOUTED:
                    $status_error = 'Error: Request timed out.';
                    $status       = static::TIMEOUTED_ERROR;
                    break;
                case CURLE_SSL_CACERT:
                case CURLE_SSL_CERTPROBLEM:
                    $status_error = 'Error: SSL certificate expired or invalid.';
                    $status       = static::CERT_ERROR;
                    break;
                case CURLE_COULDNT_RESOLVE_HOST:
                    $status_error = 'Error: DNS resolution failed, could not resolve host.';
                    $status       = static::RESOLVEHOST_ERROR;
                    break;
                default:
                    $status_error = 'Error: ' . $error;
                    $status       = static::UNDEFINED_ERROR;
                    break;
            }
        } elseif ( $httpCode >= 400 ) {
            $status = static::DOWN;
        } elseif ( static::is_http_code_type( 'notallowed', $httpCode ) ) {
            $status = static::NOTALLOWED;
        } elseif ( static::is_http_code_type( 'retry', $httpCode ) ) {
            $status = static::RETRY;
        } else {
            $status = static::UP;
        }

        $success = in_array( $status, array( static::DOWN, static::UP, static::RETRY ) );

        return array(
            'status'  => $status,
            'error'   => $status_error,
            'success' => $success,
        );
    }

    /**
     * Method get_mapping_status_code_names
     *
     * @return array
     */
    public static function get_mapping_status_code_names() {
        return array(
            static::TIMEOUTED_ERROR   => 'TIMEOUTED_ERROR',
            static::CERT_ERROR        => 'CERT_ERROR',
            static::RESOLVEHOST_ERROR => 'RESOLVEHOST_ERROR',
            static::UNDEFINED_ERROR   => 'UNDEFINED_ERROR',
            static::DOWN              => 'DOWN',
            static::NOTALLOWED        => 'NOTALLOWED',
            static::RETRY             => 'RETRY',
            static::UP                => 'UP',
        );
    }


    /**
     * Get apply monitor url.
     *
     * @param  mixed $monitor monitor.
     *
     * @return string
     */
    public static function get_apply_monitor_url( $monitor ) {

        if ( ! empty( $monitor ) ) {

            if ( is_object( $monitor ) ) {
                $url    = $monitor->url;
                $suburl = $monitor->suburl;
                $issub  = $monitor->issub;
            } elseif ( is_array( $monitor ) ) {
                $url    = $monitor['url'];
                $suburl = $monitor['suburl'];
                $issub  = $monitor['issub'];
            }

            if ( '/' !== substr( $url, -1 ) ) {
                $url .= '/';
            }

            if ( $issub && ! empty( $suburl ) ) {
                $url .= $suburl;
            }
        }

        return apply_filters( 'mainwp_uptime_monitoring_check_url', $url, $monitor );
    }

    /**
     * Method is_importance_status
     *
     * @param  int $previous previous.
     * @param  int $current current.
     *
     * @return mixed
     */
    public function is_importance_status( $previous, $current ) {
        // * ? -> ANY STATUS | FIRST = not important
        // UP -> PENDING = not important
        // * UP -> DOWN = important
        // UP -> UP = not important
        // PENDING -> PENDING = not important
        // * PENDING -> DOWN = important
        // PENDING -> UP = not important
        // DOWN -> PENDING = this case not exists
        // DOWN -> DOWN = not important
        // * DOWN -> UP = important

        return ( static::UP === $previous && static::DOWN === $current ) ||
        ( static::PENDING === $previous && static::DOWN === $current ) ||
        ( static::DOWN === $previous && static::UP === $current );
    }

    /**
     * Get apply setting.
     *
     * @param  mixed $name name.
     * @param  mixed $indiv_settings indiv settings.
     * @param  mixed $glo_settings glo settings.
     * @param  mixed $apply_global_value apply global value.
     * @param  mixed $default_value default value.
     * @return mixed value
     */
    public static function get_apply_setting( $name, $indiv_settings, $glo_settings, $apply_global_value, $default_value ) {

        if ( false === $glo_settings ) {
            $glo_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
        }

        if ( is_object( $indiv_settings ) && property_exists( $indiv_settings, $name ) ) {
            $indi_value = $indiv_settings->{$name};
        } elseif ( is_array( $indiv_settings ) && isset( $indiv_settings[ $name ] ) ) {
            $indi_value = $indiv_settings[ $name ];
        } elseif ( is_scalar( $indiv_settings ) ) {
            $indi_value = $indiv_settings;
        } else {
            $indi_value = $default_value;
        }
        $glo_value = is_array( $glo_settings ) && isset( $glo_settings[ $name ] ) ? $glo_settings[ $name ] : $default_value;

        // to support allow use global in individual up_status_codes settings.
        if ( 'up_status_codes' === $name && false !== strpos( $indi_value, 'useglobal' ) ) {
            if ( $indi_value === $apply_global_value ) {
                return $glo_value;
            } else {
                return str_replace( 'useglobal', $glo_value, $indi_value );
            }
        }
        // NOTE: need to match type of values.
        return ( $indi_value === $apply_global_value ) ? $glo_value : $indi_value; //phpcs:ignore -- NOSONAR - compatible.
    }

    /**
     * Method get_up_codes
     *
     * @param  mixed $monitor monitor.
     * @param  mixed $global_settings global settings.
     * @return mixed
     */
    public function get_up_codes( $monitor, $global_settings ) {
        return static::get_apply_setting( 'up_status_codes', $monitor, $global_settings, 'useglobal', '' );
    }


    /**
     * Method get_user_agent
     *
     * @return string
     */
    public static function get_user_agent() {
        return 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
    }
}
