<?php

namespace CleantalkSP\SpbctWP\Scanner;

use CleantalkSP\Common\Scanner\SignaturesAnalyser\Structures\Verdict;
use CleantalkSP\SpbctWP\DB;
use CleantalkSP\SpbctWP\Scanner\CureLog\CureLog;
use CleantalkSP\SpbctWP\Scanner\ScannerActions\BackupsActions;
use CleantalkSP\SpbctWP\Scanner\ScannerActions\ScanActions;
use CleantalkSP\SpbctWP\Scanner\ScannerActions\ScanResultsTableActions;
use CleantalkSP\SpbctWP\Scanner\Stages\CureStage;

class Cure
{
    public $signature = null;
    public $objects = array();

    /**
     * @var array
     * @psalm-suppress PossiblyUnusedProperty
     */
    public $actions = array();

    /**
     * @var array
     * @psalm-suppress PossiblyUnusedProperty
     */
    public $modifiers = array();

    /**
     * @var array
     * @psalm-suppress PossiblyUnusedProperty
     */
    public $comments = array();

    public $result = true;

    public $weak_spots_result = array();

    public function __construct($file)
    {
        global $wpdb;

        $weak_spots = json_decode($file['weak_spots'], true);
        $counter = 0;

        if ( ! empty($weak_spots['SIGNATURES']) ) {
            foreach ( $weak_spots['SIGNATURES'] as $_string => $signatures_in_string ) {
                $this->weak_spots_result[$counter] = array(
                    'weak_spots_file_line' => $_string,
                    'signature_id' => '',
                    'weakspot_is_cured' => 0,
                    'error' => ''
                );
                foreach ( $signatures_in_string as $signature_id ) {
                    $this->weak_spots_result[$counter]['signature_id'] = $signature_id;
                    $tmp             = $wpdb->get_results(
                        $wpdb->prepare(
                            'SELECT * FROM ' . SPBC_TBL_SCAN_SIGNATURES . ' WHERE id = %s',
                            $signature_id
                        ),
                        OBJECT
                    );
                    $this->signature = $tmp[0];
                    $result = $this->signatureCure($file, $this->signature, $this->weak_spots_result[$counter]);
                    $this->weak_spots_result[$counter]['weakspot_is_cured'] = (int)$result;
                }
                $counter++;
            }
        } else {
            $this->result = array('error' => 'COULD NOT GET SIGNATURE FROM DB');
        }
    }

    /**
     * @param $file
     * @param $signature
     * @param array $weak_spots_log
     * @return bool
     */
    public function signatureCure($file, $signature, &$weak_spots_log)
    {
        global $spbc;

        $cci_data = array();

        if ( ! empty($signature->cci) ) {
            $instructions = json_decode($signature->cci, true);
            if ($instructions && !empty($instructions['cci']) && is_array($instructions['cci'])) {
                $cci_data = $instructions['cci'];
            }
        }

        if ( ! empty($cci_data) ) {
            foreach ( $cci_data as $instruction ) {
                // Object
                foreach ( $instruction['objects'] as $_key => &$object ) {
                    // Building path to file

                    // Default if unset
                    if ( ! isset($object['file']) ) {
                        $object['file'] = 'self';
                    }

                    // self
                    if ( $object['file'] === 'self' ) {
                        $object['file'] = spbc_get_root_path() . $file['path'];
                        // absolute "/var/www/wordpress.com"
                    } elseif ( $object['file'][0] == '/' || $object['file'][0] == '\\' ) {
                        $object['file'] = spbc_get_root_path() . $object['file'];
                        // relative ".some_file.php"
                    } else {
                        $object['file'] = spbc_get_root_path()
                                          . preg_replace('/(.*\\\\).*?\.php$/', '$1', $file['path'])
                                          . $object['file'];
                    }

                    // Building code if nessary
                    if ( isset($object['code']) ) {
                        if ( $object['code'] === 'self' ) {
                            $object['code'] = $signature->body;
                        }
                    }

                    // Perform actions
                    if ( ! isset($object['code']) ) {
                        $result = $this->actionPerformWithFile(
                            $object,
                            $instruction['action'],
                            $file,
                            $signature->id
                        ); // Actions with file.
                    } else {
                        $result = $this->actionPerformWithCode(
                            $object,
                            $instruction['action'],
                            $file,
                            $signature->id
                        ); // Actions with code.
                    }

                    $this->objects[] = $object;

                    if ( ! empty($result['error']) ) {
                        $weak_spots_log['error'] = $result['error'];
                        return false;
                    }

                    if ( ! $spbc->settings['there_was_signature_treatment'] ) {
                        $spbc->settings['there_was_signature_treatment'] = 1;
                        $spbc->save('settings');
                    }
                }
            }
            return true;
        }
        $weak_spots_log['error'] = __('No cure instruction found for line.', 'security-malware-firewall');
        return false;
    }

    /**
     * @param $object
     * @param $actions
     * @param $file
     * @param string $signature_id
     *
     * @return array|bool|int|null
     */
    public function actionPerformWithFile($object, $actions, $file, $signature_id)
    {
        global $spbc;

        $result = true;

        if ( ! file_exists($object['file']) ) {
            return array('error' => 'File does not exists.');
        }

        foreach ( $actions as $action => $action_details ) {
            switch ( $action ) {
                case 'delete':
                    $result = unlink($object['file']);
                    file_put_contents(
                        $object['file'],
                        "<?php\n// " . $spbc->data["wl_brandname"] . ": Malware was deleted: #" . $signature_id
                    );
                    break;
                case 'quarantine':
                    $result = ScannerAjaxEndpoints::doQuarantineFile(true, $file['fast_hash']);
                    break;
                case 'move_to':
                    /** @todo moveTo */
                    break;
                case 'replace_with':
                    if ( $action_details === 'original' ) {
                        $result = ScannerAjaxEndpoints::replaceFileWithOriginal(true, $file['fast_hash']);
                    }
                    if ( $action_details === 'blank' ) {
                        $result = file_put_contents(
                            $object['file'],
                            '<?php\n/* File was cleaned by ' . $spbc->data["wl_brandname"] . ' */'
                        );
                    }
                    break;
            }
        }

        return $result;
    }

    /**
     * @param $object
     * @param $actions
     * @param $_file
     * @param $signature_id
     *
     * @return array|bool|int
     */
    public function actionPerformWithCode($object, $actions, $_file, $signature_id)
    {
        global $spbc;

        $result = true;

        try {
            foreach ( $actions as $action => $action_details ) {
                if ( file_exists($object['file']) ) {
                    $file_content = file_get_contents($object['file']);
                } else {
                    return array('error' => 'File does not exists.');
                }

                $is_regexp = \CleantalkSP\SpbctWP\Helpers\Helper::isRegexp($object['code']);

                switch ( $action ) {
                    case 'delete':
                        $file_content = $is_regexp
                            ? preg_replace(
                                $object['code'],
                                '/* ' . $spbc->data["wl_brandname"] . ': Malware was deleted: #' . $signature_id . '*/',
                                $file_content,
                                1
                            )
                            : str_replace(
                                $object['code'],
                                '/* ' . $spbc->data["wl_brandname"] . ': Malware was deleted: #' . $signature_id . '*/',
                                $file_content
                            );

                        $result = @file_put_contents($object['file'], $file_content);

                        break;

                    case 'replace_with':
                        $file_content = $is_regexp
                            ? preg_replace($object['code'], $action_details, $file_content, 1)
                            : str_replace($object['code'], $action_details, $file_content);

                        $result = @file_put_contents($object['file'], $file_content);

                        break;
                }
            }

            if ( false === $result ) {
                $result = array('error' => 'Permissions denied.');
            }
        } catch (\Exception $e) {
            $result = array('error' => 'Something went wrong during cure: ' . $e->getMessage());
        }
        return $result;
    }

    /**
     * Handles the AJAX request to cure selected files.
     *
     * This method is triggered via an AJAX request and attempts to cure the files
     * specified by their IDs in the `$_POST['selectedIds']` array. It performs the following steps:
     * 1. Verifies the AJAX nonce for security.
     * 2. Initializes response data.
     * 3. Checks if any IDs were provided.
     * 4. Retrieves the fast hashes of already cured files.
     * 5. Determines which files need to be cured.
     * 6. Attempts to cure each file and updates the response data accordingly.
     * 7. Sends a JSON response indicating success or failure.
     * 8. Response json is in the following format:
     * <pre>
     *  {
     *  'message' => 'Success!',
     *  'cured_on_request' => count of cured files,
     *  'skipped' => count of already cured files,
     *  'failed_to_cure' => count of failed files,
     * }
     * </pre>
     *
     * @return void
     */
    public static function cureSelectedAction()
    {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');

        $ids = isset($_POST['selectedIds']) ? $_POST['selectedIds'] : null;

        if (empty($ids)) {
            $response_data['message'] = esc_html__('No items to restore', 'security-malware-firewall');
            wp_send_json_error($response_data);
        }
        $cured_on_request = 0;
        $failed_to_cure = array();
        $skipped = 0;
        $response_data = array(
            'message' => esc_html__('Success!', 'security-malware-firewall'),
            'cured_on_request' => $cured_on_request,
            'skipped' => $skipped,
            'failed_to_cure' => $failed_to_cure,
        );

        $cure_stage = new CureLog();
        $cured_files = $cure_stage->getCuredFilesFastHashes();
        $cured_fast_hashes = array_column($cured_files, 'fast_hash');
        $to_cure = array_diff($ids, $cured_fast_hashes);

        if (empty($to_cure)) {
            $response_data['message'] = esc_html__('All selected files are already cured.', 'security-malware-firewall');
            wp_send_json_success($response_data);
        }

        $skipped = count($ids) - count($to_cure);

        foreach ($to_cure as $id) {
            $id = sanitize_user($id, true);
            $result = self::cureFile($id);

            if (is_wp_error($result)) {
                $path = ScanResultsTableActions::getFileByID($id);
                $path = isset($path['path']) ? $path['path'] : 'unknown';
                $path = '..' . esc_html(substr($path, -50));
                $failed_to_cure[] = $path . ' - ' . $result->get_error_message();
            } else {
                $cured_on_request++;
            }
        }

        $response_data['cured_on_request'] = $cured_on_request;
        $response_data['skipped'] = $skipped;
        $response_data['failed_to_cure'] = $failed_to_cure;

        if (count($failed_to_cure) === 0) {
            wp_send_json_success($response_data);
        } else {
            $response_data['message'] = esc_html__('Some files were not cured.', 'security-malware-firewall');
            wp_send_json_error($response_data);
        }
    }

    public static function restoreSelectedAction()
    {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');

        $ids = isset($_POST['selectedIds']) ? $_POST['selectedIds'] : null;

        if (empty($ids)) {
            $response_data['message'] = esc_html__('No items to restore', 'security-malware-firewall');
            wp_send_json_error($response_data);
        }

        $restored_on_request = 0;
        $failed_to_restore = array();
        $skipped = 0;
        $response_data = array(
            'message' => esc_html__('Success!', 'security-malware-firewall'),
            'restored_on_request' => $restored_on_request,
            'skipped' => $skipped,
            'failed_to_restore' => $failed_to_restore,
        );

        $cure_log = new CureLog();
        $restored_fast_hashes = $cure_log->getRestoredFiles(true);
        $restored_fast_hashes = array_column($restored_fast_hashes, 'fast_hash');
        $to_restore = array_diff($ids, $restored_fast_hashes);
        if (empty($to_restore)) {
            $response_data['message'] = esc_html__('All selected files are already restored.', 'security-malware-firewall');
            wp_send_json_success($response_data);
        }

        $totally_failed_files_fast_hashes = $cure_log->getTotallyFailedFilesFastHashes();
        $totally_failed_files_fast_hashes = array_column($totally_failed_files_fast_hashes, 'fast_hash');
        $to_restore = array_diff($to_restore, $totally_failed_files_fast_hashes);

        $skipped = count($ids) - count($to_restore);

        foreach ($to_restore as $id) {
            $id = sanitize_user($id, true);
            $result = BackupsActions::restoreFileFromBackup($id);
            if (!isset($result['success']) || $result['success'] !== true) {
                $path = ScanResultsTableActions::getFileByID($id);
                $path = isset($path['path']) ? $path['path'] : 'unknown';
                $path = '..' . esc_html(substr($path, -50));
                $failed_to_restore[] = $path . ' - ' . !empty($result['error']) ? $result['error'] : __('Unknown error', 'security-malware-firewall');
            } else {
                $restored_on_request++;
            }
        }

        $response_data['restored_on_request'] = $restored_on_request;
        $response_data['skipped'] = $skipped;
        $response_data['failed_to_restore'] = $failed_to_restore;

        if (count($failed_to_restore) === 0) {
            wp_send_json_success($response_data);
        } else {
            $response_data['message'] = esc_html__('Some files were not restored.', 'security-malware-firewall');
            wp_send_json_error($response_data);
        }
    }

    /**
     * tested
     * AJAX handler for cure action.
     * @param string $file_fast_hash
     * @return string|\WP_Error
     */
    public static function cureFile($file_fast_hash)
    {
        global $wpdb;

        if (is_null($file_fast_hash)) {
            return new \WP_Error(
                '422',
                esc_html__('Error: File not found.', 'security-malware-firewall')
            );
        }

        $file_data = $wpdb->get_row(
            $wpdb->prepare(
                'SELECT * FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE fast_hash = %s',
                $file_fast_hash
            ),
            ARRAY_A
        );

        if (empty($file_data)) {
            return new \WP_Error(
                '422',
                esc_html__('Error: File not found in table.', 'security-malware-firewall')
            );
        }

        // rescan file on a single run!
        $rescan_results = ScanActions::rescanSingleFile($file_data['path'], md5_file(spbc_get_root_path() . $file_data['path']), spbc_get_root_path());
        $merged_result = $rescan_results['merged_result'];
        $verdict = isset($rescan_results['signature_result']) && $rescan_results['signature_result'] instanceof Verdict
            ? $rescan_results['signature_result']
            : new Verdict();

        $cure_log = new CureLog();
        if ( $verdict->status === 'OK') {
            $cure_log->deleteCureLogRecord($file_data['fast_hash']);
            // update file in the table
            $wpdb->update(
                SPBC_TBL_SCAN_FILES,
                array(
                    'checked_signatures' => 1,
                    'checked_heuristic'  => 1,
                    'status'             => $file_data['status'] === 'MODIFIED' ? 'MODIFIED' : $merged_result['status'],
                    'severity'           => $merged_result['severity'],
                    'weak_spots'         => json_encode($merged_result['weak_spots']),
                    'full_hash'          => md5_file(spbc_get_root_path() . $file_data['path']),
                ),
                array('fast_hash' => $file_data['fast_hash']),
                array('%s', '%s', '%s', '%s', '%s', '%s'),
                array('%s')
            );
            return esc_html__('No threats detected for current file statement.', 'security-malware-firewall');
        }

        $cure_stage = new CureStage(DB::getInstance());
        $cure_log_record = $cure_stage->processCure($file_data);

        $cure_log->logCureResult($cure_log_record);

        if ( !empty($cure_log_record->fail_reason) ) {
            return new \WP_Error(
                '422',
                esc_html__('Error: ' . $cure_log_record->fail_reason, 'security-malware-firewall')
            );
        }

        return esc_html__('Success!', 'security-malware-firewall');
    }

    /**
     * tested
     * Changing the file information in the cure_log table regarding file recovery
     * @param string $file_path
     * @return array
     */
    public static function updateCureLog($file_path)
    {
        global $wpdb;

        if ($file_path) {
            $sql_prepared = $wpdb->prepare(
                'UPDATE ' . SPBC_TBL_CURE_LOG . ' SET is_restored = 1, cure_status = 0 WHERE real_path = %s;',
                $file_path
            );
            if ($wpdb->query($sql_prepared) === false) {
                return array('error' => esc_html__('Error update cure log: Something is wrong during updating cure log.', 'security-malware-firewall'));
            }
        } else {
            return array('error' => esc_html__('Error update cure log: Incorrect backup file path for the database query.', 'security-malware-firewall'));
        }

        return array();
    }
}
