<?php

use CleantalkSP\Common\DNS;
use CleantalkSP\SpbctWP\DB;
use CleantalkSP\SpbctWP\Firewall\FW;
use CleantalkSP\SpbctWP\Cron;
use CleantalkSP\SpbctWP\Queue;
use CleantalkSP\Variables\Request;
use CleantalkSP\SpbctWP\RemoteCalls;
use CleantalkSP\SpbctWP\Helpers\Data;

// Prevent direct call
if ( ! defined('ABSPATH') ) {
    die('Not allowed!');
}

/**
 * Called by update_security_firewall remote call
 * Starts the Security Firewall update and could use a delay before start
 *
 * @param int $delay
 *
 * @return bool|string|string[]|array[]
 * @throws Exception
 * @psalm-suppress NullArgument
 */
function spbc_security_firewall_update__init($delay = null)
{
    global $spbc;

    sleep((int)$delay);

    $spbc->update_logger::clearStorage();
    $spbc->update_logger->writeLog('UPDATE INIT START');

    // Prevent start an update if update is already running and started less than 2 minutes ago
    if (
        $spbc->fw_stats['updating_id'] &&
        spbc_security_firewall_update__is_in_progress() &&
        time() - $spbc->fw_stats['updating_last_start'] < 120
    ) {
        $spbc->update_logger->writeLog('UPDATE INIT EXIT: less than 120 seconds gone');
        return true;
    }

    if ( ! $spbc->api_key ) {
        $spbc->update_logger->writeLog('UPDATE INIT EXIT ERROR: KEY_EMPTY');
        return array( 'error' => 'FW UPDATE INIT: KEY_EMPTY' );
    }

    if ( ! $spbc->key_is_ok ) {
        $spbc->update_logger->writeLog('UPDATE INIT EXIT ERROR: KEY_IS_NOT_VALID');
        return array( 'error' => 'FW UPDATE INIT: KEY_IS_NOT_VALID' );
    }

    $spbc->update_logger->writeLog('UPDATE INIT LOG: deleting temp tables..');

    // Delete temporary tables
    FW::dataTablesDeleteTemporary(DB::getInstance(), array(
        SPBC_TBL_FIREWALL_DATA_V4,
        SPBC_TBL_FIREWALL_DATA_V6,
        SPBC_TBL_FIREWALL_DATA__IPS_V4,
        SPBC_TBL_FIREWALL_DATA__IPS_V6,
        SPBC_TBL_FIREWALL_DATA__COUNTRIES
    ));

    $spbc->update_logger->writeLog('UPDATE INIT LOG: flushing fw stats..');

    $wp_upload_dir = wp_upload_dir();
    $spbc->fw_stats['updating_folder'] = $wp_upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'fw_files_for_blog_' . get_current_blog_id() . DIRECTORY_SEPARATOR;

    // Set a new update ID and an update time start
    $spbc->fw_stats['update_percent'] = 5;
    $spbc->fw_stats['calls']          = 0;
    $spbc->fw_stats['updating_id']    = md5((string) mt_rand(0, 100000));
    $spbc->fw_stats['updating_last_start'] = time();
    $spbc->save('fw_stats', true, false);
    delete_option('spbc_secfw_data_files_info');

    $spbc->update_logger->writeLog('UPDATE INIT LOG: deleting errors..');

    // Delete update errors
    $spbc->error_delete('firewall_update', true);
    $spbc->error_delete('firewall_update', 'save_data', 'cron');

    $spbc->update_logger->writeLog('UPDATE INIT LOG: making direct update decision..');

    if (spbc_security_firewall_update_is_switch_to_direct()) {
        $spbc->update_logger->writeLog('UPDATE INIT EXIT: run direct update');
        return spbc_security_firewall_update_direct();
    }

    $spbc->update_logger->writeLog('UPDATE INIT LOG: initializing queue..');

    $queue = new Queue('fw_update', 'update_security_firewall__worker');
    $queue->clearQueue();
    $queue->addStage('spbc_security_firewall_update__get_multifiles');

    $spbc->update_logger->writeLog('UPDATE INIT LOG: initializing cron fw_update_checker..');

    Cron::addTask('fw_update_checker', 'spbc_security_firewall_update__checker', 5, time() + 30);

    $spbc->update_logger->writeLog('UPDATE INIT LOG: initializing worker RC..');

    $result = RemoteCalls::performToHost(
        'update_security_firewall__worker',
        array( 'updating_id' => $spbc->fw_stats['updating_id'], ),
        array( 'async' )
    );

    if ( ! empty($result['error']) ) {
        $spbc->update_logger->writeLog('UPDATE INIT LOG: first worker remote call error', $result['error']);
    }

    return ( ! empty($result['error']) && $queue->isQueueFinished() ) ? $result : true;
}

/**
 * Updating Security FireWall data
 *
 * @param bool $checker_work flag indicates that the function were called by checker cron task
 *
 * @return array|bool|array[]|string[]
 */
function spbc_security_firewall_update__worker($checker_work = null)
{
    global $spbc;

    // fix for wordpress.com hostings - need a delay to avoid 429 repsonses
    if (defined('WPCOMSH__PLUGIN_FILE')) {
        sleep(1);
    }

    $spbc->update_logger->writeLog('- WORKER START');

    if ( ! $spbc->key_is_ok ) {
        $spbc->update_logger->writeLog('- WORKER EXIT ERROR: KEY_IS_NOT_VALID');
        return array( 'error' => 'KEY_IS_NOT_VALID' );
    }

    // Check if the update performs right now. Blocks remote calls with different ID
    // This was done to make sure that we won't have multiple updates at a time

    if ( ! $checker_work ) {
        if (
            Request::equal('updating_id', '')
            || ! Request::equal('updating_id', $spbc->fw_stats['updating_id'])
        ) {
            $spbc->update_logger->writeLog('- WORKER EXIT ERROR: WRONG_UPDATE_ID');
            return array( 'error' => 'FW UPDATE WORKER: WRONG_UPDATE_ID' );
        }
    }

    $spbc->fw_stats['calls']++;
    $spbc->save('fw_stats', true, false);

    if ( $spbc->fw_stats['calls'] > 600 ) {
        $spbc->error_add('firewall_update', 'WORKER_CALL_LIMIT_EXCEEDED');
        $spbc->save('errors');
        $spbc->update_logger->writeLog('- WORKER EXIT ERROR', 'WORKER_CALL_LIMIT_EXCEEDED');
        return array( 'error' => 'WORKER_CALL_LIMIT_EXCEEDED' );
    }

    // Queue is already empty. Exit.
    $queue = new Queue('fw_update', 'update_security_firewall__worker');
    if ( $queue->isQueueFinished() ) {
        $spbc->update_logger->writeLog('- WORKER EXIT SUCCESS: queue is finished before next stage executing');
        return true;
    }

    $result = $queue->executeStage();

    $spbc->update_logger->writeLog('WORKER LOG: executeStage() done, new call params', $result);

    if ( $result === null ) {
        // The stage is in progress, will try to wait up to 5 seconds to its complete
        for ( $i = 0; $i < 5; $i++ ) {
            sleep(1);
            $queue->refreshQueue();
            if ( ! $queue->isQueueInProgress() ) {
                break;
            }
            if ( $i >= 4 ) {
                $spbc->update_logger->writeLog('- WORKER EXIT ERROR: the stage still not executed');
                return true;
            }
        }
    }

    if ( isset($result['error']) ) {
        $spbc->error_add('firewall_update', $result['error']);
        $spbc->save('errors');
        $spbc->update_logger->writeLog('- WORKER EXIT ERROR', $result['error']);
        delete_option('spbc_secfw_data_files_info');

        return array('error' => $result['error']);
    }

    if ( $queue->isQueueFinished() ) {
        $queue->queue['finished'] = time();
        $queue->saveQueue();
        $spbc->update_logger->writeLog('WORKER LOG: queue is finished');

        if ( array_column($queue->queue['stages'], 'error') ) {
            $spbc->update_logger->writeLog('WORKER LOG: some stages has errors, notice added');
            $spbc->error_add('firewall_update', current(array_column($queue->queue['stages'], 'error')));
        }
        $spbc->update_logger->writeLog('- WORKER EXIT SUCCESS: queue is finished after stage executed');
        return true;
    }

    // This is the repeat stage request, do not generate any new RC
    if ( stripos(Request::getString('stage'), 'Repeat') !== false ) {
        $spbc->update_logger->writeLog('- WORKER EXIT SUCCESS: requested to repeat stage');
        return true;
    }

    $spbc->update_logger->writeLog('WORKER LOG: call to a new worker using async RC..');

    $result = RemoteCalls::performToHost(
        'update_security_firewall__worker',
        array( 'updating_id' => $spbc->fw_stats['updating_id'] ),
        array( 'async' )
    );

    if ( ! empty($result['error']) ) {
        $stage_error = current(array_column($queue->queue['stages'], 'error'));
        if ( empty($stage_error) ) {
            $stage_error = esc_html($result['error']);
            if (empty($stage_error)) {
                $stage_error = __('unknown remote call error', 'security-malware-firewall');
            }
        }
        $spbc->error_add('firewall_update', $stage_error);
    }

    $final_worker_result = ! empty($result['error']) && $queue->isQueueFinished() ? $result : true;

    return $final_worker_result;
}

/**
 * Get updating multifiles hashes.
 * @param array $result Optional. firewallUpdateGetMultifiles result, if empty the function
 * will run new firewallUpdateGetMultifiles to get result.
 * @return array of hashes
 */
function spbc_security_firewall_update_get_update_files_hashes($result)
{
    global $spbc;
    $file_hashes = array();
    $spbc->update_logger->writeLog('STAGE: FILE HASHES START');

    // Create array with urls
    if ( empty($result['error']) && isset($result['file_urls']) ) {
        foreach ( $result['file_urls'] as $file_url_info ) {
            $file_hashes[] = isset($file_url_info[1]) ? $file_url_info[1] : '';
        }
    }

    $spbc->update_logger->writeLog('STAGE: FILE HASHES EXIT');


    return $file_hashes;
}

/**
 * @param array $hashes
 * @return bool
 */
function spbc_security_firewall_update_save_files_hashes_option($hashes)
{
    if ( ! is_array($hashes) ) {
        return false;
    }

    $normalized_hashes = array_values(array_filter(array_map(
        static function ($hash) {
            $hash = strtolower(trim((string)$hash));
            return preg_match('/^[a-f0-9]{32}$/', $hash) ? $hash : '';
        },
        $hashes
    )));

    return update_option('spbc_secfw_data_files_info', $normalized_hashes, false);
}

/**
 * @return array
 */
function spbc_security_firewall_update_get_files_hashes_option()
{
    $hashes = get_option('spbc_secfw_data_files_info', array());
    if ( ! is_array($hashes) ) {
        return array();
    }

    return array_values(array_filter(array_map(
        static function ($hash) {
            $hash = strtolower(trim((string)$hash));
            return preg_match('/^[a-f0-9]{32}$/', $hash) ? $hash : '';
        },
        $hashes
    )));
}

/**
 * @return array[]|string[]
 */
function spbc_security_firewall_update__get_multifiles()
{
    global $spbc;

    $spbc->update_logger->writeLog('STAGE: GET MULTIFILES START');

    if ( $spbc->key_is_ok ) {
        $result = FW::firewallUpdateGetMultifiles($spbc->api_key);

        if ( empty($result['error']) ) {
            $tries_for_download_again         = 3 + (int)(count($result['file_urls']) / 20);
            $spbc->fw_stats['files_count']    = count($result['file_urls']);
            $spbc->fw_stats['update_percent'] = 10;
            $file_urls = array();

            $spbc->save('fw_stats', true, false);

            // Create array with urls
            foreach ($result['file_urls'] as $file_url_info) {
                $file_urls[] = isset($file_url_info[0]) ? $file_url_info[0] : '';
            }

            // Save info about urls and hashes
            $hashes = spbc_security_firewall_update_get_update_files_hashes($result);
            $spbc->data['secfw_data_files_info'] = $hashes;
            $spbc->save('data');
            $spbc->update_logger->writeLog('STAGE: GET MULTIFILES EXIT SUCCESS');
            spbc_security_firewall_update_save_files_hashes_option($hashes);

            return array(
                'next_stage' => array(
                    'name'    => 'spbc_security_firewall_update__download_files',
                    'args'    => $file_urls,
                    'accepted_tries' => $tries_for_download_again
                )
            );
        }
        $spbc->update_logger->writeLog('STAGE: GET MULTIFILES EXIT ERROR', $result['error']);
        return array('error' => 'GET MULTIFILE: ' . $result['error']);
    }
    $spbc->update_logger->writeLog('STAGE: GET MULTIFILES EXIT ERROR - KEY_IS_NOT_VALID');

    return array('error' => 'FW UPDATE PREPARE: KEY_IS_NOT_VALID');
}

function spbc_security_firewall_update__download_files($urls)
{
    global $spbc;

    $spbc->update_logger->writeLog('STAGE: GET DOWNLOAD FILES START');

    sleep(3);

    // Split URLs
    $urls_to_download_next_time = array_slice($urls, 20);
    $urls_to_download_right_now    = array_slice($urls, 0, 20);

    $http    = new CleantalkSP\Common\HTTP\Request();
    $results = $http
        ->setUrl($urls_to_download_right_now)
        ->setPresets(array('get', 'ssl'))
        ->addCallback(
            static function ($content, $url) use ($spbc) {
                if ( is_dir($spbc->fw_stats['updating_folder']) && is_writable($spbc->fw_stats['updating_folder'])) {
                    return file_put_contents($spbc->fw_stats['updating_folder'] . Data::getFilenameFromUrl($url), $content)
                        ? 'success'
                        : 'error';
                }

                return $content;
            }
        )
        ->request();

    if ( is_scalar($results) ) {
        $truncated = substr((string)$results, 0, 300);
        if (strlen((string)$results) > 300) {
            $truncated .= '…'; // sign to inform that response is truncated
        }
        $spbc->update_logger->writeLog('STAGE: GET DOWNLOAD FILES EXIT ERROR', $truncated);
        return [ 'error' => 'UNKNOWN ERROR: ' . substr((string)$results, 0, 300) ];
    }

    if ( ! empty($results['error']) ) {
        $spbc->update_logger->writeLog('STAGE: GET DOWNLOAD FILES EXIT ERROR', $results['error']);
        return $results;
    }

    $download_again = array();
    $results        = array_values($results);

    for ( $i = 0, $iMax = count($results); $i < $iMax; $i++ ) {
        if ( $results[$i] === 'error' ) {
            $download_again[] = $urls[$i];
        }
    }

    $download_again = array_merge($download_again, $urls_to_download_next_time);

    if ( count($download_again) !== 0 ) {
        $spbc->update_logger->writeLog('STAGE: GET DOWNLOAD FILES EXIT - DOWNLOAD AGAIN');
        return array(
            'error' => 'Files download not completed.',
            'update_args' => array(
                'args'    => $download_again
            )
        );
    }

    $spbc->fw_stats['update_percent'] = 10;
    $spbc->save('fw_stats', true, false);
    $spbc->update_logger->writeLog('STAGE: GET DOWNLOAD FILES EXIT SUCCESS');

    return array(
        'next_stage' => array(
            'name'    => 'spbc_security_firewall_update__prepare'
        )
    );
}

function spbc_security_firewall_update__prepare()
{
    global $spbc;

    $spbc->update_logger->writeLog('STAGE: UPDATE PREPARE START');

    if ( ! $spbc->key_is_ok ) {
        $spbc->update_logger->writeLog('STAGE: UPDATE PREPARE EXIT ERROR', 'KEY_IS_NOT_VALID');
        return array( 'error' => 'FW UPDATE PREPARE: KEY_IS_NOT_VALID' );
    }

    global $wpdb;
    // Make sure that the table exists. Creating it if not.
    $db_tables_creator = new \CleantalkSP\SpbctWP\DB\TablesCreator();
    $db_tables_creator->createTable(SPBC_TBL_FIREWALL_DATA_V4);
    $db_tables_creator->createTable(SPBC_TBL_FIREWALL_DATA_V6);
    $db_tables_creator->createTable(SPBC_TBL_FIREWALL_DATA__IPS_V4);
    $db_tables_creator->createTable(SPBC_TBL_FIREWALL_DATA__IPS_V6);
    $db_tables_creator->createTable(SPBC_TBL_FIREWALL_DATA__COUNTRIES);

    // Update only personal tables for daughter blogs
    $result = FW::dataTablesCreateTemporaryTablesForTables(
        DB::getInstance(),
        array(
            SPBC_TBL_FIREWALL_DATA_V4,
            SPBC_TBL_FIREWALL_DATA_V6,
            SPBC_TBL_FIREWALL_DATA__IPS_V4,
            SPBC_TBL_FIREWALL_DATA__IPS_V6,
            SPBC_TBL_FIREWALL_DATA__COUNTRIES
        )
    );

    if ( ! empty($result['error']) ) {
        $spbc->update_logger->writeLog('STAGE: UPDATE PREPARE EXIT ERROR', $result);
        return $result;
    }

    // Copying data without country code
    $result_v4 = FW::dataTablesCopyCountiesDataFromMainTable(DB::getInstance(), SPBC_TBL_FIREWALL_DATA_V4);
    $result_v6 = FW::dataTablesCopyCountiesDataFromMainTable(DB::getInstance(), SPBC_TBL_FIREWALL_DATA_V6);
    if ( ! empty($result_v4['error']) ) {
        $spbc->update_logger->writeLog('STAGE: UPDATE PREPARE EXIT ERROR - v4 tables copy failed', $result_v4);
        return $result_v4;
    }
    if ( ! empty($result_v6['error']) ) {
        $spbc->update_logger->writeLog('STAGE: UPDATE PREPARE EXIT ERROR - v6 tables copy failed', $result_v6);
        return $result_v6;
    }

    $spbc->fw_stats['update_percent'] = 15;
    $spbc->save('fw_stats', true, false);
    $spbc->update_logger->writeLog('STAGE: UPDATE PREPARE EXIT SUCCESS');

    return array(
        'next_stage' => array(
            'name'    => 'spbc_security_firewall_update__process_files',
        )
    );
}

function spbc_security_firewall_update__process_files()
{
    global $spbc;

    $spbc->update_logger->writeLog('STAGE: PROCESS FILES START');

    $files = glob($spbc->fw_stats['updating_folder'] . '/*csv.gz');

    if ( count($files) ) {
        $result = spbc_security_firewall_update__process_file(reset($files));

        if ( ! empty($result['error']) ) {
            $spbc->update_logger->writeLog('STAGE: PROCESS FILES ERROR', $result);
            return $result;
        }

        if ( file_exists(reset($files))) {
            unlink(reset($files));
        }

        $spbc->fw_stats['update_percent'] = 15 + round(65 * (($spbc->fw_stats['files_count'] - count($files)) / $spbc->fw_stats['files_count']), 2);
        $spbc->save('fw_stats', true, false);
        $spbc->update_logger->writeLog('STAGE: PROCESS FILES EXIT - CALL NEXT FILES ITERATION');

        return array(
            'next_stage' => array(
                'name'    => 'spbc_security_firewall_update__process_files',
            )
        );
    }
    $spbc->update_logger->writeLog('STAGE: PROCESS FILES EXIT SUCCESS');

    return array(
        'next_stage' => array(
            'name'    => 'spbc_security_firewall_update__process_exclusions',
        )
    );
}


/**
 * @param $path
 *
 * @return array|bool|int|mixed|string
 * @throws Exception
 */
function spbc_security_firewall_update__process_file($path, $is_direct_mode = false)
{
    global $spbc;

    $spbc->update_logger->writeLog('STAGE: PROCESS SINGLE FILE START');

    if ( !$is_direct_mode ) { // in direct mode we don't need to check if file exists and is readable
        if ( !file_exists($path) ) {
            return array('error' => 'PROCESS FILE: file does not exist: ' . $path);
        }

        if ( !is_readable($path) ) {
            return array('error' => 'PROCESS FILE: file is not readable: ' . $path);
        }
    }

    $current_file_content = @file_get_contents($path);

    if ( $current_file_content === false ) {
        // Handle the case when file_get_contents fails (file not readable, empty, etc.)
        return array('error' => 'PROCESS FILE: ' . 'failed to read file contents: ' . $path);
    }

    if ( ! $current_file_content ) {
        // Handle the case when file_get_contents returns 0 or ''
        return array('error' => 'PROCESS FILE: ' . 'the file has been read but no contents: ' . $path);
    }

    // Check current file content hash against the list of the hashes saved on spbc_security_firewall_update__get_multifiles
    $current_file_hash = md5($current_file_content);
    $updated_hashes_list = spbc_security_firewall_update_get_files_hashes_option();
    if ( count($updated_hashes_list) === 0 ) {
        $updated_hashes_list = isset($spbc->data['secfw_data_files_info']) && is_array($spbc->data['secfw_data_files_info'])
            ? array_values(array_filter(array_map(
                static function ($hash) {
                    $hash = strtolower(trim((string)$hash));
                    return preg_match('/^[a-f0-9]{32}$/', $hash) ? $hash : '';
                },
                $spbc->data['secfw_data_files_info']
            )))
            : array();
    }
    if ( ! in_array($current_file_hash, $updated_hashes_list) ) {
        return array('error' => 'PROCESS FILE: ' . 'unexpected file contents: ' . $path . ' hash:' . $current_file_hash);
    }

    $result = FW::updateWriteToDb(
        DB::getInstance(),
        SPBC_TBL_FIREWALL_DATA . '_temp', // Write to the main table for daughter blogs
        SPBC_TBL_FIREWALL_DATA__IPS . '_temp',
        SPBC_TBL_FIREWALL_DATA__COUNTRIES . '_temp',
        $path
    );

    $single_file_result = empty($result['error'])
        ? $result
        : array( 'error' => 'PROCESS FILE: ' . $result['error']);

    $spbc->update_logger->writeLog('STAGE: PROCESS SINGLE FILE EXIT', $single_file_result);

    return $single_file_result;
}

/**
 * @return array
 * @throws Exception
 */
function spbc_security_firewall_update__process_exclusions()
{

    global $spbc;

    $spbc->update_logger->writeLog('STAGE: PROCESS EXCLUSIONS START');

    $result = FW::updateWriteToDbExclusions(
        DB::getInstance(),
        SPBC_TBL_FIREWALL_DATA__IPS . '_temp',
        SPBC_TBL_FIREWALL_DATA . '_temp'
    );

    if ( ! empty($result['error']) ) {
        $spbc->update_logger->writeLog('STAGE: PROCESS EXCLUSIONS ERROR', $result['error']);
        return array( 'error' => 'EXCLUSIONS: ' . $result['error'] );
    }

    $spbc->fw_stats['update_percent'] = 90;
    $spbc->save('fw_stats', true, false);

    $spbc->update_logger->writeLog('STAGE: PROCESS EXCLUSIONS EXIT');

    return array(
        'next_stage' => array(
            'name' => 'spbc_security_firewall_update__end_of_update',
            'accepted_tries' => 1,
        )
    );
}

function spbc_security_firewall_update__end_of_update()
{

    global $spbc, $wpdb;

    $spbc->update_logger->writeLog('STAGE: END OF UPDATE START');

    // Put in maintenance mode
    $spbc->fw_stats['is_on_maintenance'] = true;
    $spbc->save('fw_stats', true, false);
    usleep(100000);


    //Increment firewall entries
    $tables_to_work_with = array(
        SPBC_TBL_FIREWALL_DATA_V4,
        SPBC_TBL_FIREWALL_DATA_V6,
        SPBC_TBL_FIREWALL_DATA__IPS_V4,
        SPBC_TBL_FIREWALL_DATA__IPS_V6,
        SPBC_TBL_FIREWALL_DATA__COUNTRIES
    );

    $result = FW::dataTablesDelete(DB::getInstance(), $tables_to_work_with);
    if ( empty($result['error']) ) {
        $result = FW::dataTablesMakeTemporaryPermanent(DB::getInstance(), $tables_to_work_with);
        if ( empty($result['error']) ) {
            $result = FW::dataTablesClearUnusedCountriesDataFromMainTable(DB::getInstance()); // Clear useless entries about countries in the ain table
        }
    }
    if ( ! empty($result['error']) ) {
        $spbc->fw_stats['is_on_maintenance'] = false;
        $spbc->save('fw_stats', true, false);
        $spbc->update_logger->writeLog('STAGE: END OF UPDATE ERROR', $result['error']);

        return $result;
    }

    //Files array is empty update sfw stats
    $spbc->fw_stats['update_percent'] = 0;
    $spbc->fw_stats['updating_id'] = null;
    $spbc->fw_stats['updating_last_start'] = 0;
    $spbc->fw_stats['last_updated'] = current_time('timestamp');
    $spbc->fw_stats['is_on_maintenance'] = false; // Remove maintenance mode

    $sql_count_networks = "SELECT SUM(cnt) FROM ( 
                    SELECT COUNT(*) as cnt FROM " . SPBC_TBL_FIREWALL_DATA_V4 . "
                    UNION SELECT COUNT(*) FROM " . SPBC_TBL_FIREWALL_DATA_V6 . "
                    UNION SELECT COUNT(*) FROM " . SPBC_TBL_FIREWALL_DATA__IPS_V4 . "
                    UNION SELECT COUNT(*) FROM " . SPBC_TBL_FIREWALL_DATA__IPS_V6 . ") cnt";
    $spbc->fw_stats['entries'] = $wpdb->get_var($sql_count_networks);
    $spbc->fw_stats['ips_count'] = spbc_security_firewall_update__get_ips_count();

    $spbc->save('fw_stats', true, false);

    $spbc->error_delete('firewall_update', true);
    $spbc->error_delete('firewall_update', 'save_data', 'cron');

    // Get update period for server
    $update_period = DNS::getRecord('securityfirewall-ttl-txt.cleantalk.org', true, DNS_TXT);
    $update_period = isset($update_period['txt']) ? $update_period['txt'] : 0;
    $update_period = (int) $update_period > 43200 ?  (int) $update_period : 43200;
    Cron::updateTask('firewall_update', 'spbc_security_firewall_update__init', $update_period);
    Cron::removeTask('fw_update_checker');
    delete_option('spbc_secfw_data_files_info');

    Data::removeDirectoryRecursively($spbc->fw_stats['updating_folder']);
    $spbc->update_logger->writeLog('STAGE: END OF UPDATE EXIT');

    return true;
}

function spbc_security_firewall_update__get_ips_count()
{
    global $wpdb;

    $query = "SELECT SUM(ip_count) from (
        SELECT (SUM(ip_count) + COUNT(*)) as ip_count FROM (
            SELECT %q as ip_count FROM %t1
        ) ip_count
        UNION
        SELECT (SUM(ip_count) + COUNT(*)) as ip_count FROM (
            SELECT %q as ip_count FROM %t2
        ) ip_count ) t;";

    $mask = bindec(str_repeat('1', 32));
    $data = [
        '%q' => str_replace("%m", (string)$mask, "%m - mask"),
        '%t1' => SPBC_TBL_FIREWALL_DATA_V4,
        '%t2' => SPBC_TBL_FIREWALL_DATA__IPS_V4
    ];
    $query_v4 = str_replace(array_keys($data), array_values($data), $query);

    return (int)$wpdb->get_var($query_v4) + spbc_security_firewall_update__get_ipv6_ips_count();
}

/**
 * Counts IPv6 addresses covered by a single firewall table.
 *
 * Mirrors IP::convertLongIntMaskToDec() in SQL to avoid loading all rows into PHP.
 * Large prefixes (/0–/95) are excluded from host expansion.
 *
 * @param string $table
 *
 * @return int
 */
function spbc_security_firewall_update__get_ipv6_ips_count_for_table($table)
{
    global $wpdb;

    $allowed_tables = array(
        SPBC_TBL_FIREWALL_DATA_V6,
        SPBC_TBL_FIREWALL_DATA__IPS_V6,
    );
    if ( ! in_array($table, $allowed_tables, true) ) {
        return 0;
    }

    $sql = 'SELECT COALESCE(SUM(
        CASE
            WHEN prefix = 128 THEN 1
            WHEN prefix > 0 AND prefix < 128 AND (128 - prefix) <= 32 THEN CAST(POW(2, 128 - prefix) AS UNSIGNED)
            ELSE 0
        END
    ), 0)
    FROM (
        SELECT
            BIT_COUNT(mask1) + BIT_COUNT(mask2) + BIT_COUNT(mask3) + BIT_COUNT(mask4) AS prefix
        FROM `' . $table . '`
    ) networks
    WHERE prefix >= 96 AND prefix <= 128';

    return (int)$wpdb->get_var($sql);
}

/**
 * Counts IPv6 addresses covered by the firewall database.
 */
function spbc_security_firewall_update__get_ipv6_ips_count()
{
    return spbc_security_firewall_update__get_ipv6_ips_count_for_table(SPBC_TBL_FIREWALL_DATA_V6)
        + spbc_security_firewall_update__get_ipv6_ips_count_for_table(SPBC_TBL_FIREWALL_DATA__IPS_V6);
}

function spbc_security_firewall_update__is_in_progress()
{
    $queue = new Queue('fw_update', 'update_security_firewall__worker');
    return $queue->isQueueInProgress();
}

function spbc_security_firewall_update__prepare_upd_dir()
{

    global $spbc;

    $dir_name = $spbc->fw_stats['updating_folder'];

    if ( $dir_name === '' ) {
        return array('error' => 'FW dir can not be blank.');
    }

    if ( ! is_dir($dir_name) && ! mkdir($dir_name) ) {
        return ! is_writable(SPBC_PLUGIN_DIR)
            ? array( 'error' => 'Can not to make FW dir. Low permissions: ' . fileperms(SPBC_PLUGIN_DIR) )
            : array( 'error' => 'Can not to make FW dir. Unknown reason.' );
    }

    $files = glob($dir_name . '/*');
    if ( $files === false ) {
        return array( 'error' => 'Can not find FW files.' );
    }
    if ( count($files) === 0 ) {
        return (bool) @file_put_contents($dir_name . 'index.php', '<?php' . PHP_EOL);
    }

    foreach ( $files as $file ) {
        if ( is_file($file) && @unlink($file) === false ) {
            // do not worry about index.php
            if (strpos($file, 'index.php') === false ) {
                return array( 'error' => 'Can not delete the FW file: ' . $file );
            }
        }
    }

    return (bool) @file_put_contents($dir_name . 'index.php', '<?php');
}

function spbc_security_firewall_update__checker()
{

    global $spbc;

    $queue = new Queue('fw_update', 'update_security_firewall__worker');

    if (
        $spbc->fw_stats['updating_id'] &&
        $queue->hasUnstartedStages()
    ) {
        $result = spbc_security_firewall_update__worker(true);

        if ( ! empty($result['error']) && $queue->isQueueFinished() ) {
            $spbc->fw_stats['update_percent'] = 0;
            $spbc->fw_stats['updating_id'] = null;
            $spbc->save('fw_stats', true, false);

            Cron::removeTask('fw_update_checker');

            return $result;
        }
    }

    return true;
}

/**
 * Update security firewall in single thread
 *
 * @return bool|string[]|array[]
 * @throws Exception
 */
function spbc_security_firewall_update_direct()
{
    global $spbc;

    $spbc->update_logger->writeLog('DIRECT UPDATE START');

    // get_multifiles
    $result_get_multifiles = spbc_security_firewall_update__get_multifiles();
    if ( ! empty($result_get_multifiles['error']) ) {
        $spbc->update_logger->writeLog('DIRECT UPDATE ERROR: get multifiles: ' . @json_encode($result_get_multifiles['error']));
        return $result_get_multifiles;
    }
    $urls = $result_get_multifiles['next_stage']['args'];

    // prepare
    $result_prepare = spbc_security_firewall_update__prepare();
    if ( ! empty($result_prepare['error']) ) {
        $spbc->update_logger->writeLog('DIRECT UPDATE ERROR: update prepare: ' . @json_encode($result_prepare['error']));
        return $result_prepare;
    }

    // process_file
    foreach ( $urls as $url ) {
        $result_process_file = spbc_security_firewall_update__process_file($url, true);
        if ( ! empty($result_process_file['error']) ) {
            $spbc->update_logger->writeLog('DIRECT UPDATE ERROR: processing file: ' . @json_encode($result_process_file['error']));
            return $result_process_file;
        }
        $spbc->fw_stats['update_percent'] = 15 + round(65 * (($spbc->fw_stats['files_count'] - count($urls)) / $spbc->fw_stats['files_count']), 2);
        $spbc->save('fw_stats', true, false);
    }

    // process_exclusions
    $result_process_exclusions = spbc_security_firewall_update__process_exclusions();
    if ( ! empty($result_process_exclusions['error']) ) {
        $spbc->update_logger->writeLog('DIRECT UPDATE ERROR: processing exclusions: ' . @json_encode($result_process_exclusions['error']));
        return $result_process_exclusions;
    }

    // end_of_update
    $result_end_of_update = spbc_security_firewall_update__end_of_update();
    if ( ! empty($result_end_of_update['error']) ) {
        $spbc->update_logger->writeLog('DIRECT UPDATE ERROR: end of update actions: ' . @json_encode($result_end_of_update['error']));
        return $result_end_of_update;
    }

    $spbc->update_logger->writeLog('DIRECT UPDATE EXIT');

    return $result_end_of_update;
}

/**
 * Decide need to force direct update
 *
 * @return bool
 * @psalm-suppress NullArgument
 */
function spbc_security_firewall_update_is_switch_to_direct()
{
    global $spbc;

    $spbc->update_logger->writeLog('DIRECT UPDATE DECISION START');

    if (defined('SPBCT_FORCE_DIRECT_SECFW_UPDATE') && SPBCT_FORCE_DIRECT_SECFW_UPDATE) {
        $spbc->update_logger->writeLog('DIRECT UPDATE DECISION: TRUE - const SPBCT_FORCE_DIRECT_SECFW_UPDATE exists');
        return true;
    }

    $prepare_dir__result = spbc_security_firewall_update__prepare_upd_dir();
    if (!empty($prepare_dir__result['error'])) {
        $spbc->update_logger->writeLog('DIRECT UPDATE DECISION: TRUE - variable prepare_dir__result has error');
        return true;
    }

    $test_rc_result = RemoteCalls::performTest(
        is_multisite() ? get_blog_option(null, 'home') : get_option('home'),
        array(
            'spbc_remote_call_token'  => md5($spbc->api_key),
            'spbc_remote_call_action' => 'update_security_firewall__worker',
            'plugin_name'             => 'security',
        )
    );
    if (!empty($test_rc_result['error'])) {
        $spbc->update_logger->writeLog('DIRECT UPDATE DECISION: TRUE - test remote call has error');
        return true;
    }

    if (isset($spbc->fw_stats['last_updated'], $spbc->cron['firewall_update']['period']) &&
        ((int)$spbc->fw_stats['last_updated'] + (int)$spbc->cron['firewall_update']['period'] + 3600) < time()
    ) {
        $spbc->update_logger->writeLog('DIRECT UPDATE DECISION: TRUE - general cron update is freezing');
        return true;
    }
    $spbc->update_logger->writeLog('DIRECT UPDATE DECISION: FALSE - no reason for direct update, do normal mode');
    return false;
}
