<?php

namespace CleantalkSP\SpbctWP\Scanner\Stages;

use CleantalkSP\Common\Scanner\HeuristicAnalyser\Structures\Verdict;
use CleantalkSP\SpbctWP\DB;
use CleantalkSP\SpbctWP\Helpers\Helper as QueueHelper;
use CleantalkSP\SpbctWP\Scanner\Cure;
use CleantalkSP\SpbctWP\Scanner\CureLog\CureLog;
use CleantalkSP\SpbctWP\Scanner\CureLog\CureLogRecord;
use CleantalkSP\SpbctWP\Scanner\FileInfoExtended;
use CleantalkSP\SpbctWP\Scanner\ScanningLog\ScanningLogFacade;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\ScanningStagesStorage;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\AutoCure;
use CleantalkSP\SpbctWP\Scanner\ScannerInteractivity\ScannerInteractivityData;

class CureStage
{
    /**
     * @var DB
     */
    private $db;
    /**
     * @var array
     */
    private $stage_result;
    /**
     * @var null|int
     */
    private $total_files_to_cure_at_first_run = null;

    /**
     * Constructor for the CureStage class.
     *
     * @param DB $db An instance of the DB class. This is used for database operations within the CureStage class.
     */
    public function __construct($db)
    {
        $this->db = $db;
    }

    public function runStage($offset, $amount)
    {
        global $spbc;

        //prepare facade log
        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(AutoCure::class);

        //init common vars
        $processed = 0;
        $cured = array();

        //init cure log
        $cure_log = new CureLog();

        //count first offset counter for total file
        //todo fix this (int) cast, if false - this is error
        if ( $offset === 0 ) {
            $cure_log->removeIrrelevantRestoredFiles();
            $cure_log->removeIrrelevantFailedFiles();
            $this->total_files_to_cure_at_first_run = count($this->getFilesToCure());
            //to facade log
            $stage_data_obj->increase('count_files', $this->total_files_to_cure_at_first_run);
        }

        //get files with signatures weak spots
        $files = $this->getFilesToCure($amount);

        if ( count($files) ) {
            //main cure process
            foreach ( $files as $file ) {
                $cure_result = $this->processCure($file);
                $cure_log->logCureResult($cure_result);
                if ( $cure_result->cure_status === 1 ) {
                    //for logs sending way
                    $cured[$file['path']] = 'CURED';
                }
                //inc processed count anyway
                $processed++;
            }
            //this stuff is used to send cure logs
            $spbc->data['scanner']['cured'] = $cured;
            $spbc->save('data');
        }

        // Adding to facade log
        $stage_data_obj->increase('count_cured', count($cured));
        $scanning_stages_storage->saveToDb();
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $this->stage_result = $this->prepareAjaxOutput($processed, count($cured), $stage_data_obj);
    }

    /**
     * This method is used to retrieve files that need to be cured.
     *
     * @param int $limit The maximum number of files to retrieve.
     * @return array An array of files that need to be cured. Each file is represented as an associative array.
     */
    public function getFilesToCure($limit = null)
    {
        // get files with signatures
        $files_with_signatures = '
                    SELECT fast_hash, full_hash, mtime 
                    FROM ' . SPBC_TBL_SCAN_FILES . '
                    WHERE weak_spots LIKE "%SIGNATURES%"
                    ';
        $files_with_signatures = $this->db->fetchAll($files_with_signatures, OBJECT_K);

        // get cure log data
        $cure_log = new CureLog();
        $cure_log_data = $cure_log->getCureLogData();

        $to_cure_fast_hashes = array();
        foreach ($files_with_signatures as $key => $value) {
            if (isset($cure_log_data[$key])) { //fast hash found, do check for last cure date and hash
                $found_by_fast_hash = $cure_log_data[$key];
                // skip restored files
                if ($found_by_fast_hash->is_restored == 1) {
                    continue;
                }

                // get files were not cured on this scan
                $is_old_scan = (time() - (int)$found_by_fast_hash->last_cure_date) > 5;
                if ((int)$found_by_fast_hash->cure_status == 0 && $is_old_scan) { // file was cured more than 5 secs ago
                    $to_cure_fast_hashes[] = $key;
                    continue;
                }
                // get files were cured but changed since last scan
                if ((int)$found_by_fast_hash->last_cure_date < (int)$value->mtime) { // file was changed after last cure
                    if ($found_by_fast_hash->cured_hash !== $value->full_hash) { // file hash cured changed after last cure
                        $to_cure_fast_hashes[] = $key;
                    }
                }
            } else {
                $to_cure_fast_hashes[] = $key; // fast hash not found, add it to cure
            }
        }

        $to_cure_query = '
                    SELECT * 
                    FROM ' . SPBC_TBL_SCAN_FILES . '
                    WHERE fast_hash IN ("' . implode('","', $to_cure_fast_hashes) . '")
                    ';
        if (isset($limit)) {
            $to_cure_query = $to_cure_query . ' LIMIT ' . $limit;
        }
        $result = $this->db->fetchAll($to_cure_query);

        if (is_null($result) || is_object($result)) {
            $result = array();
        }
        return $result;
    }

    public function processCure($file)
    {
        global $spbc;
        $weak_spots_decoded = '';
        if ($file['weak_spots']) {
            $weak_spots_decoded = json_decode($file['weak_spots'], true);
        }

        //init cure log item, this item is DTO, used during all the process
        $cure_log_record = new CureLogRecord(array(
            'fast_hash' => isset($file['fast_hash']) ? $file['fast_hash'] : '',
            'full_hash' => isset($file['full_hash']) ? $file['full_hash'] : '',
            'cured_hash' => '',
            'real_path' => isset($file['path']) ? $file['path'] : '',
            'cure_status' => 0,
            'is_restored' => 0,
            'weak_spots_cured' => '',
            'weak_spots_uncured' => '',
            'cci_cured' => null,
            'has_backup' => 0,
            'fail_reason' => '',
            'last_cure_date' => time(),
            'heuristic_rescan_result' => null,
            'scanner_start_local_date' => $spbc->data['scanner']['scanner_start_local_date'],
        ));

        $cure_log_record = $this->preCheckFile($file, $cure_log_record);

        if ( $cure_log_record->fail_reason ) {
            return $cure_log_record;
        }

        //process Cure
        $cure_file_result = $this->doCureFile($file);
        if ( isset($cure_file_result->result['error']) ) {
            $cure_log_record->cure_status = 0;
            $cure_log_record->fail_reason = $cure_file_result->result['error'];
            return $cure_log_record;
        }
        $weak_spots_result = CureLog::parseWeakSpotsFromCureResult($cure_file_result->weak_spots_result);
        $cure_log_record->weak_spots_cured = $weak_spots_result['cured'];
        $cure_log_record->weak_spots_uncured = $weak_spots_result['uncured'];
        //update hash after cure
        $cure_log_record->cured_hash = md5_file(spbc_get_root_path() . $file['path']);
        //update table
        $cure_log_record = $this->updateScanResultsTableOnAfterCure(
            $file,
            $cure_log_record,
            $weak_spots_decoded
        );

        //if cured, rescan file with heuristic
        if ( $cure_log_record->cure_status !== 0 ) {
            $recheck_result = $this->rescanFileHeuristic($file);
            $cure_log_record->heuristic_rescan_result = json_encode($recheck_result);
            //if rescanned, update results table
            if ( empty($recheck_result->error_msg) ) {
                $this->updateScanResultsTableOnHeuristicReCheck($file, $recheck_result);
            }
        }

        return $cure_log_record;
    }

    /**
     * This method is used to perform preliminary checks on the file before attempting to cure it.
     *
     * @param array $file An associative array representing the file to be checked.
     * @param CureLogRecord $cure_log_record An instance of the CureLogRecord class. This is used to log the results of the cure process.
     *
     * @return CureLogRecord The updated CureLogRecord instance. This may contain a failure reason if the file cannot be cured.
     */
    private function preCheckFile($file, $cure_log_record)
    {
        spbc_backup__files_with_signatures(true);

        //check if file has backup
        if ( !$this->fileHasBackup($file) ) {
            //cure is not safe without backups, log this and return cure_log_record
            $cure_log_record->cure_status = 0;
            $cure_log_record->fail_reason = __('File has no backup.', "security-malware-firewall");
            return $cure_log_record;
        } else {
            $cure_log_record->has_backup = 1;
        }

        return $cure_log_record;
    }

    /**
     * This method checks if the file has a backup.
     *
     * @param array $file An associative array representing the file to be checked.
     *
     * @return bool Returns true if the file has a backup, false otherwise.
     */
    private function fileHasBackup($file)
    {
        return spbc_file_has_backup($file['path']);
    }

    /**
     * This method is used to perform the cure operation on the file.
     *
     * @param array $file An associative array representing the file to be cured.
     *
     * @return Cure Returns an instance of the Cure class.
     */
    private function doCureFile($file)
    {
        return new Cure($file);
    }

    /**
     * This method updates the scan results table after the cure process.
     *
     * @param array $file An associative array representing the file that was cured.
     * @param CureLogRecord $cure_log_record An instance of the CureLogRecord class. This is used to log the results of the cure process.
     * @param array $weak_spots_decoded An associative array representing the JSON decoded weak spots in the file.
     *
     * @return CureLogRecord The updated CureLogRecord instance. This may contain a failure reason if the file cannot be cured.
     */
    private function updateScanResultsTableOnAfterCure($file, $cure_log_record, $weak_spots_decoded)
    {
        $log_uncured = $cure_log_record->getPropAsArray('weak_spots_uncured');
        $log_cured = $cure_log_record->getPropAsArray('weak_spots_cured');

        if ( !empty($log_uncured) && !empty($log_cured) ) {
            //if Cure process errored keep the reason
            $cure_log_record->fail_reason = __('Partially success, has uncured weak spots.', 'security-malware-firewall');
            $cure_log_record->cure_status = -1;
            $cure_log_record->cci_cured = count($log_cured);
        } else if ( empty($log_uncured) && !empty($log_cured) ) {
            $cure_log_record->cure_status = 1;
            $cure_log_record->cci_cured = count($log_cured);
        } else {
            $cure_log_record->fail_reason = __('Has uncured weak spots.', 'security-malware-firewall');
            $cure_log_record->cure_status = 0;
        }

        $weak_spots_lines_to_remove = array_map(
            function ($weak_spot) {
                return $weak_spot['line'];
            },
            $log_cured
        );

        //file is partially or fully cured, remove signatures weakspots
        if ( !empty($weak_spots_lines_to_remove) ) {
            foreach ($weak_spots_decoded['SIGNATURES'] as $line => $_data) {
                if ( in_array($line, $weak_spots_lines_to_remove) ) {
                    unset($weak_spots_decoded['SIGNATURES'][$line]);
                }
            }
        }

        if (empty($weak_spots_decoded['SIGNATURES'])) {
            unset($weak_spots_decoded['SIGNATURES']);
        }

        //process any other weakspots to save them
        if ( empty($weak_spots_decoded) ) {
            $weak_spots_encoded = 'NULL';
            $severity = 'NULL';
            $status = 'OK';
        } else {
            $weak_spots_encoded = QueueHelper::prepareParamForSQLQuery(json_encode($weak_spots_decoded));
            $severity = $file['severity'];
            $status = $file['status'];
        }

        //update scan results table
        $this->db->execute(
            'UPDATE ' . SPBC_TBL_SCAN_FILES
            . ' SET '
            . 'weak_spots = ' . $weak_spots_encoded . ','
            . 'severity = "' . $severity . '",'
            . 'full_hash = "' . $cure_log_record->cured_hash . '",'
            . 'status = "' . $status . '"'
            . ' WHERE fast_hash = "' . $file['fast_hash'] . '";'
        );


        return $cure_log_record;
    }

    /**
     * This method is used to rescan a file using heuristic analysis.
     *
     * @param array $file An associative array representing the file to be rescanned.
     *
     * @return Verdict Returns the result of the heuristic scan. The type and content of this result can vary depending on the heuristic scanner.
     */
    private function rescanFileHeuristic($file)
    {
        //get root path to rescan
        $root_path = spbc_get_root_path();

        // Get file form DB - not from thread - important!
        $file_to_check_with_heuristic = $this->db->fetchAll(
            'SELECT * '
            . ' FROM ' . SPBC_TBL_SCAN_FILES
            . ' WHERE fast_hash = "' . $file['fast_hash'] . '";'
        );
        $file_to_check_with_heuristic = $file_to_check_with_heuristic[0];

        $file_to_check = new FileInfoExtended($file_to_check_with_heuristic);

        // init heuristic module to rescan
        $heuristic_scanner = new \CleantalkSP\Common\Scanner\HeuristicAnalyser\Controller();

        return $heuristic_scanner->scanFile($file_to_check, $root_path);
    }

    /**
     * This method updates the scan results table after a heuristic recheck.
     *
     * @param array $file An associative array representing the file that was rescanned.
     * @param Verdict $result The result of the heuristic recheck. This is an instance of the Verdict class.
     *
     * The method updates the following fields in the scan results table:
     * - checked_heuristic: Set to 1 to indicate that a heuristic recheck has been performed.
     * - status: The status of the file after the heuristic recheck.
     * - severity: The severity of the file after the heuristic recheck. If the severity is null, it is set to 'NULL'.
     * - weak_spots: The weak spots in the file after the heuristic recheck. If there are no weak spots, it is set to 'NULL'.
     */
    private function updateScanResultsTableOnHeuristicReCheck($file, $result)
    {
        //update table
        $this->db->execute(
            'UPDATE ' . SPBC_TBL_SCAN_FILES
            . ' SET'
            . " checked_heuristic = 1,"
            . ' status = \'' . $result->status . '\','
            . ' severity = ' . ($result->severity ? '\'' . $result->severity . '\'' : 'NULL') . ','
            . ' weak_spots = ' . ($result->weak_spots ? QueueHelper::prepareParamForSQLQuery(
                json_encode($result->weak_spots)
            ) : 'NULL')
            . ' WHERE fast_hash = \'' . $file['fast_hash'] . '\';'
        );
    }

    /**
     * This method prepares the AJAX output for the cure stage.
     *
     * @param int $processed The number of files that have been processed in the current run of the cure stage.
     * @param int $cured_count The number of files that have been cured in the current run of the cure stage.
     * @param ScanningStagesStorage $stage_data_obj An instance of the ScanningStagesStorage class. This is used to get the title and description of the cure stage.
     *
     * @return array The AJAX output for the cure stage. This includes the following fields:
     * - processed: The number of files that have been processed in the current run of the cure stage.
     * - cured: The number of files that have been cured in the current run of the cure stage.
     * - end: A boolean indicating whether all files have been attempted to be cured. This is true if there are no more files to cure, and false otherwise.
     * - message: A message recommending the user to change their secret authentication keys and salts when the curing is done.
     * - stage_data_for_logging: An associative array containing the title and description of the cure stage.
     * - error: An error message if there is a database read error while counting the files that have not been attempted to be cured. This field is only present if there is a database read error.
     * - comment: A comment containing the last database error. This field is only present if there is a database read error.
     * - total: The total number of files that were to be cured at the start of the first run of the cure stage. This field is only present in the output of the first run of the cure stage.
     * @psalm-suppress UndefinedMethod - this suppress is used because the ScanningStagesStorage class does not have a static method getTitle().
     */
    private function prepareAjaxOutput($processed, $cured_count, $stage_data_obj)
    {
        $uncured = count($this->getFilesToCure());

        //prepare AJAX output
        $out = array(
            'processed' => $processed,
            'cured' => $cured_count,
            'end' => $uncured === 0,
            'message' => __(
                'We recommend changing your secret authentication keys and salts when curing is done.',
                'security-malware-firewall'
            )
        );

        $out['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $stage_data_obj->getDescription()
        );

        //provide this value just once, if provide more - there will cause unexpected percentage counter
        if ( isset($this->total_files_to_cure_at_first_run) ) {
            $out = array_merge($out, array('total' => $this->total_files_to_cure_at_first_run));
        }

        $refresh_data = array(
            'do_refresh' => !empty($cured_count),
            'control_tab' => 'cure_log',
        );
        $out['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);


        return $out;
    }

    /**
     * This method is used to get the result of the stage.
     *
     * @return array The result of the stage. The type and content of this result can vary depending on the stage.
     */
    public function getStageResult()
    {
        return $this->stage_result;
    }
}
