<?php

namespace CleantalkSP\SpbctWP\Scanner\ScannerActions;

use CleantalkSP\SpbctWP\Scanner\Cure;
use CleantalkSP\SpbctWP\Helpers\Helper;

class BackupsActions
{
    /**
     * Check AJAX nonce
     * @param string $action
     * @param string $query_arg
     * @return void
     */
    protected static function checkAjaxReferer($action = 'spbc_secret_nonce', $query_arg = 'security')
    {
        spbc_check_ajax_referer($action, $query_arg);
    }

    /**
     * @param $result
     *
     * @return void|string
     */
    protected static function sendAjaxResult($result)
    {
        wp_send_json($result);
    }

    // ==================== AJAX HANDLERS ====================

    /**
     * AJAX handler for deleting backup
     *
     * @return string|void
     * @psalm-suppress PossiblyUnusedReturnValue
     */
    public static function deleteBackupAjax() // phpcs:ignore Tools.Spbc.AjaxNonce.MissingNonceCheck
    {
        static::checkAjaxReferer();

        $backup_id = !empty($_POST['backup_id']) ? (int)$_POST['backup_id'] : null;

        $output = static::deleteBackupById($backup_id);
        $output = static::prepareAjaxResponse($output, 'Backup deleted', 'rgba(240, 110, 110, 0.7)');

        return static::sendAjaxResult($output);
    }

    /**
     * AJAX handler for rollback backup
     * @param bool $direct_call
     * @param int|string|null $backup_id
     * @return array<string, mixed>
     */
    public static function rollbackBackupAjax($direct_call = false, $backup_id = null) // phpcs:ignore Tools.Spbc.AjaxNonce.MissingNonceCheck
    {
        if ( ! $direct_call ) {
            static::checkAjaxReferer();
        }
        $backup_id = ! $direct_call && ! empty($_POST['backup_id']) ? (int)$_POST['backup_id'] : $backup_id;

        $output = static::rollbackBackupById($backup_id);

        if ( ! $direct_call ) {
            $output = static::prepareAjaxResponse($output, 'Rollback succeeded', 'rgba(110, 240, 110, 0.7)');
            static::sendAjaxResult($output);
        }

        return $output;
    }

    /**
     * Method for creating backups of files with signatures
     *
     * @return array<string, mixed>
     */
    public static function createBackupsForFilesWithSignatures()
    {
        $output = static::createBackupsForFilesWithSignaturesHandler();
        $output['end'] = 1;
        return $output;
    }

    /**
     * Helper to prepare AJAX response with HTML formatting
     * @param array $output
     * @param string $message
     * @param string $background
     * @return array
     */
    private static function prepareAjaxResponse($output, $message, $background)
    {
        if (!empty($output['success'])) {
            $colspan = isset($_POST['cols']) ? (int) $_POST['cols'] : 0;
            $colspan_attr = $colspan > 0 ? " colspan='" . esc_attr((string) $colspan) . "'" : '';
            $output['html'] = '<td' . $colspan_attr . '>' . $message . '</td>';
            $output['color'] = 'black';
            $output['background'] = $background;
        }
        return $output;
    }

    // ==================== BUSINESS LOGIC ====================
    /**
     * Check if file has backup
     * @param string $real_path
     * @return bool
     */
    public static function fileHasBackup($real_path)
    {
        global $spbc, $wpdb;
        $real_path = $spbc->is_windows ? str_replace('/', '\\', $real_path) : $real_path;
        $query = 'SELECT * FROM ' . SPBC_TBL_BACKUPED_FILES;
        $result = $wpdb->get_results($query, ARRAY_A);
        foreach ($result as $row) {
            if ($row['real_path'] === $real_path) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get backup record by real path
     * @param string $real_path
     * @return array<string, mixed>
     */
    protected static function getBackupByRealPath($real_path)
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE real_path = %s',
            $real_path
        );

        $result = $wpdb->get_row($sql, ARRAY_A);

        if (!is_array($result)) {
            return array();
        }

        return $result;
    }

    /**
     * Delete backup log and file by real path
     * @param string $real_path
     * @return void
     */
    public static function deleteLogAndFile($real_path)
    {
        global $wpdb;

        $backup_record = self::getBackupByRealPath($real_path);

        if (!empty($backup_record)) {
            self::removeBackupFromDB($real_path);
        }

        // check if backup set is empty, then delete backup record, else remove only file
        if (!empty($backup_record['backup_id'])) {
            $sql = $wpdb->prepare('SELECT COUNT(*) FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE backup_id = %d', $backup_record['backup_id']);
            $backup_set = $wpdb->get_var($sql);
            if ($backup_set == 0) {
                self::deleteBackupById($backup_record['backup_id']);
            } elseif (!empty($backup_record['back_path'])) {
                $backup_file_path = $backup_record['back_path'];

                if (!file_exists($backup_file_path)) {
                    $backup_file_path = rtrim(ABSPATH, '/\\') . DIRECTORY_SEPARATOR . ltrim($backup_record['back_path'], '/\\');
                }

                self::deleteBackupFile($backup_file_path);
            }
        }
    }

    /**
     * Delete backup by ID (files and DB records)
     * @param int $backup_id
     * @return array<string, mixed>
     */
    protected static function deleteBackupById($backup_id)
    {
        global $wpdb;

        $output = array();

        if (is_dir(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id)) {
            // Deleting backup files
            foreach (glob(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id . '/*') as $filename) {
                if (!unlink($filename)) {
                    $output = array('error' => 'FILE_DELETE_ERROR: ' . substr($filename, 0, 1024));
                    break;
                }
            }

            if (empty($output['error'])) {
                if (rmdir(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id)) {
                    if (false !== $wpdb->delete(SPBC_TBL_BACKUPED_FILES, array('backup_id' => $backup_id), array('%d'))) {
                        if (false !== $wpdb->delete(SPBC_TBL_BACKUPS, array('backup_id' => $backup_id), array('%d'))) {
                            $output = array('success' => true);
                        } else {
                            $output = array('error' => 'DELETING_BACKUP_DB_ERROR: ' . substr($wpdb->last_error, 0, 1024));
                        }
                    } else {
                        $output = array('error' => 'DELETING_BACKUP_FILES_DB_ERROR: ' . substr($wpdb->last_error, 0, 1024));
                    }
                } else {
                    $output = array('error' => 'DIRECTORY_DELETE_ERROR: ' . substr(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id, 0, 1024));
                }
            }
        } else {
            $output = array('comment' => 'DIRECTORY_NOT_EXISTS: ' . substr(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id, 0, 1024));
        }

        return $output;
    }

    /**
     * Create backups for files with signatures
     * @return array<string, mixed>
     */
    protected static function createBackupsForFilesWithSignaturesHandler()
    {
        global $wpdb, $spbc;

        $output = array('success' => true);

        $files_to_backup = $wpdb->get_results(
            'SELECT path, weak_spots, checked_heuristic, checked_signatures, status, severity
            FROM ' . SPBC_TBL_SCAN_FILES . '
            WHERE weak_spots LIKE "%\"SIGNATURES\":%"
            AND status <> "QUARANTINED";',
            ARRAY_A
        );

        if (!is_array($files_to_backup) || !count($files_to_backup)) {
            return $output;
        }

        $sql_data = array();
        $backup_id = null;

        foreach ($files_to_backup as $file) {
            if (self::fileHasBackup($file['path'])) {
                continue;
            }
            $weak_spots = json_decode($file['weak_spots'], true);

            $signatures_in_file = array();
            if (!empty($weak_spots['SIGNATURES'])) {
                foreach ($weak_spots['SIGNATURES'] as $signatures_in_string) {
                    $signatures_in_file = array_merge($signatures_in_file, array_diff($signatures_in_string, $signatures_in_file));
                }
            }

            if (empty($signatures_in_file)) {
                continue;
            }

            // Adding new backup batch
            if (!isset($backup_id)) {
                $wpdb->insert(SPBC_TBL_BACKUPS, array('type' => 'SIGNATURES', 'datetime' => date('Y-m-d H:i:s')));
                $backup_id = $wpdb->insert_id;
                $spbc->data['scanner']['last_backup'] = $backup_id;
                $spbc->save('data');
                $dir_name = SPBC_PLUGIN_DIR . 'backups/';
                if (!is_dir($dir_name)) {
                    mkdir($dir_name);
                    file_put_contents($dir_name . 'index.php', '<?php');
                }

                $dir_name .= 'backup_' . $backup_id;
                if (!is_dir($dir_name)) {
                    mkdir($dir_name);
                    file_put_contents($dir_name . '/index.php', '<?php');
                }
            }

            $result = self::backupFile($file['path'], $backup_id);

            $backup_prev_results_state = json_encode($file);
            $backup_prev_results_state = $backup_prev_results_state === false ? 'ERROR' : $backup_prev_results_state;

            if (empty($result['error'])) {
                $sql_data[] = '('
                    . $backup_id . ','
                    . Helper::prepareParamForSQLQuery($file['path']) . ','
                    . Helper::prepareParamForSQLQuery($result) . ','
                    . Helper::prepareParamForSQLQuery($backup_prev_results_state)
                    . ')';
            } else {
                // Mark the backup STOPPED while errors occurred
                $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'STOPPED'), array('backup_id' => $backup_id));
                $output = $result;
                break;
            }
        }

        if (isset($output['error'])) {
            return $output;
        }

        if (empty($sql_data)) {
            return array('success' => true);
        }

        $backup_id = isset($backup_id) ? $backup_id : $spbc->data['scanner']['last_backup'];

        // Writing backuped files to DB
        $sql_query = 'INSERT INTO ' . SPBC_TBL_BACKUPED_FILES . ' (backup_id, real_path, back_path, backup_prev_results_state) VALUES';
        // @psalm-suppress WpdbUnsafeMethodsIssue
        $result = $wpdb->query($sql_query . implode(',', $sql_data) . ';');
        if ($result === false) {
            $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'STOPPED'), array('backup_id' => $backup_id));
            return array('error' => 'DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024));
        }

        // Updating current backup status
        $result = $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'BACKUPED'), array('backup_id' => $backup_id));
        if ($result === false) {
            return array('error' => 'DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024));
        }

        $result = self::rotateBackups('signatures');
        if (!empty($result['error'])) {
            return array('error' => 'BACKUP_ROTATE: ' . substr($result['error'], 0, 1024));
        }

        return array('success' => true);
    }

    /**
     * Backup a single file
     * @param string $filename
     * @param int $backup_id
     * @return string|array Backup path on success, error array on failure
     */
    private static function backupFile($filename, $backup_id)
    {
        global $spbc;

        $file_path = spbc_get_root_path() . $filename;

        if (file_exists($file_path)) {
            if (is_readable($file_path)) {
                $backup_path = '/wp-content/plugins/security-malware-firewall/backups/backup_'
                               . $backup_id
                               . '/' . str_replace('/', '__', str_replace('\\', '__', $filename))
                               . '.' . hash('sha256', $filename . $spbc->data['salt']);

                if (copy($file_path, spbc_get_root_path() . $backup_path)) {
                    return $backup_path;
                } else {
                    return array('error' => 'COPY_FAILED');
                }
            } else {
                return array('error' => 'FILE_NOT_READABLE');
            }
        } else {
            return array('error' => 'FILE_NOT_EXISTS: ' . esc_html($file_path));
        }
    }

    /**
     * Rotate backups (keep last 10)
     * @param string $type
     * @param array $out
     * @return array
     */
    private static function rotateBackups($type = 'signatures', $out = array('success' => true))
    {
        global $wpdb;
        $result = $wpdb->get_row('SELECT COUNT(*) as cnt FROM ' . SPBC_TBL_BACKUPS . ' WHERE type = ' . Helper::prepareParamForSQLQuery(strtoupper($type)), OBJECT);
        if ($result->cnt > 10) {
            // @psalm-suppress WpdbUnsafeMethodsIssue
            $result = $wpdb->get_results(
                'SELECT backup_id'
                . ' FROM ' . SPBC_TBL_BACKUPS
                . ' WHERE datetime < ('
                . 'SELECT datetime'
                . ' FROM ' . SPBC_TBL_BACKUPS
                . ' WHERE type = ' . Helper::prepareParamForSQLQuery(strtoupper($type))
                . ' ORDER BY datetime DESC'
                . ' LIMIT 9,1)'
            );
            if ($result && count($result)) {
                foreach ($result as $backup) {
                    $result = self::deleteBackupById($backup->backup_id);
                    if (!empty($result['error'])) {
                        $out = array('error' => 'BACKUP_DELETE: ' . substr($result['error'], 0, 1024));
                    }
                }
            }
        }

        return $out;
    }

    /**
     * tested
     * Restore file from backup handler
     * @param string $id
     * @return array
     */
    public static function restoreFileFromBackup($id)
    {
        global $wpdb;

        if (is_null($id)) {
            return array('error' => esc_html__('Error: File not found.', 'security-malware-firewall'));
        }

        // Getting file path
        $file_path_sql_prepared = $wpdb->prepare(
            'SELECT path '
            . ' FROM ' . SPBC_TBL_SCAN_FILES
            . ' WHERE fast_hash=%s;',
            $id
        );
        $file_path = $wpdb->get_row($file_path_sql_prepared, ARRAY_A);

        if (is_null($file_path)) {
            return array('error' => esc_html__('Error: File path not found.', 'security-malware-firewall'));
        }

        $file_path = $file_path['path'];
        $full_file_path = ABSPATH . ltrim($file_path, '\\');

        // Getting backup path
        $sql_prepared = $wpdb->prepare(
            'SELECT back_path, backup_prev_results_state '
            . ' FROM ' . SPBC_TBL_BACKUPED_FILES
            . ' WHERE real_path="%s"'
            . ' ORDER BY backup_id DESC LIMIT 1;',
            $file_path
        );
        // prepare backup data
        $backup_data = $wpdb->get_results($sql_prepared, ARRAY_A);
        $backup_path = isset($backup_data[0]['back_path']) ? $backup_data[0]['back_path'] : null;
        // prepare previous state of the file
        $backup_prev_results_state = isset($backup_data[0]['backup_prev_results_state']) ? $backup_data[0]['backup_prev_results_state'] : null;
        $backup_prev_results_state = empty($backup_prev_results_state) || json_decode($backup_prev_results_state, true) === false
            ? null
            : json_decode($backup_prev_results_state, true);
        if (
            !isset(
                $backup_prev_results_state['weak_spots'],
                $backup_prev_results_state['checked_heuristic'],
                $backup_prev_results_state['checked_signatures'],
                $backup_prev_results_state['status'],
                $backup_prev_results_state['severity'],
                $backup_prev_results_state['path']
            )
        ) {
            $backup_prev_results_state = null;
        }

        if (is_null($backup_path)) {
            return array('error' => esc_html__('Error: Backup not found.', 'security-malware-firewall'));
        }

        $full_backup_path = ABSPATH . ltrim($backup_path, '/');

        // Trying to replace backup and original file
        $backup_content = @file_get_contents($full_backup_path);

        if ($backup_content === false) {
            $error = error_get_last();
            return array('error' => esc_html__('Error: File not exists or permissions denied: ', 'security-malware-firewall') . $error['message']);
        }

        if (!file_exists($full_file_path)) {
            return array('error' => esc_html__("The file doesn't exist and will be deleted from the log through next scan.", 'security-malware-firewall'));
        }

        $result = file_put_contents($full_file_path, $backup_content);

        if ($result === false) {
            return array('error' => esc_html__('Error: Permissions denied.', 'security-malware-firewall'));
        }

        // Success: remove all data about backup
        $error_msg = array(
            self::deleteBackupFile($full_backup_path),
            self::removeBackupFromDB($file_path),
            Cure::updateCureLog($file_path),
            ScanResultsTableActions::updateScanResults($file_path, $backup_prev_results_state)
        );

        foreach ($error_msg as $value) {
            if (array_key_exists('error', $value)) {
                return $value;
            }
        }

        return array('success' => true);
    }

    /**
     * tested
     * todo Unused??
     * Deleting a backup file
     * @param string $full_backup_path
     * @return array
     * @psalm-suppress PossiblyUnusedMethod
     */
    private static function deleteBackupFile($full_backup_path)
    {
        if ($full_backup_path) {
            if (!unlink($full_backup_path)) {
                return array('error' => esc_html__('Deleting backup error: Permissions denied.', 'security-malware-firewall'));
            }
        } else {
            return array('error' => esc_html__('Deleting backup error: Incorrect backup file path.', 'security-malware-firewall'));
        }
        return array();
    }

    /**
     * tested
     * Deleting a backup from a database
     * @param string $file_path
     * @return array
     */
    private static function removeBackupFromDB($file_path)
    {
        global $wpdb;

        if ($file_path) {
            $sql_prepared = $wpdb->prepare(
                'DELETE FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE real_path = %s;',
                $file_path
            );
            if ($wpdb->query($sql_prepared) === false) {
                return array('error' => esc_html__('Deleting backup error: Something is wrong during deleting backup.', 'security-malware-firewall'));
            }
        } else {
            return array('error' => esc_html__('Deleting backup error: Incorrect backup file path for the database query.', 'security-malware-firewall'));
        }

        return array();
    }

    /**
     * Rollback entire backup by ID
     * @param int|string $backup_id
     * @return array<string, mixed>
     */
    protected static function rollbackBackupById($backup_id)
    {
        global $wpdb;

        $files_to_rollback = $wpdb->get_results('SELECT real_path, back_path FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE backup_id = ' . (int)$backup_id . ';', ARRAY_A);

        if (!is_array($files_to_rollback) || !count($files_to_rollback)) {
            return array('error' => 'BACKUP_NOT_FOUND');
        }

        $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'ROLLBACK'), array('backup_id' => $backup_id));

        foreach ($files_to_rollback as $file) {
            $result = self::rollbackFile($file['back_path'], $file['real_path']);

            if (!empty($result['error'])) {
                return array('error' => 'FILE_BACKUP_ERROR: ' . $result['error'] . 'FILE: ' . $file['back_path']);
            }
        }

        if ($wpdb->delete(SPBC_TBL_BACKUPED_FILES, array('backup_id' => $backup_id), array('%d'))) {
            if ($wpdb->delete(SPBC_TBL_BACKUPS, array('backup_id' => $backup_id), array('%d'))) {
                rmdir(spbc_get_root_path() . '/wp-content/plugins/security-malware-firewall/backups/backup_' . $backup_id);
                return array('success' => true);
            } else {
                return array('error' => 'DELETING_BACKUP_DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024));
            }
        } else {
            return array('error' => 'DELETING_BACKUP_FILES_DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024));
        }
    }

    /**
     * Rollback a single file from backup
     * @param string $back_path Relative backup path
     * @param string $real_path Relative real file path
     * @return array<string, mixed>
     */
    private static function rollbackFile($back_path, $real_path)
    {
        $back_path = spbc_get_root_path() . $back_path;
        $real_path = spbc_get_root_path() . $real_path;

        if (!file_exists($back_path)) {
            return array('error' => 'BACKUPED_FILE_NOT_EXISTS');
        }

        if (!is_writable($back_path)) {
            return array('error' => 'BACKUPED_FILE_NOT_WRITABLE');
        }

        if (!is_dir(dirname($real_path))) {
            return array('error' => 'REAL_FILE_DIR_NOT_EXISTS');
        }

        if (!copy($back_path, $real_path)) {
            return array('error' => 'COPY_FAILED');
        }

        unlink($back_path);

        return array('success' => true);
    }

    /**
     * Count total backups
     * @return int
     * @psalm-suppress PossiblyUnusedMethod
     */
    public static function countBackups()
    {
        global $wpdb;

        $count = $wpdb->get_results(
            'SELECT COUNT(*) FROM ' . SPBC_TBL_BACKUPS,
            OBJECT_K
        );

        return $count ? (int)key($count) : 0;
    }
}
