<?php

namespace CleantalkSP\SpbctWP\Scanner\ScannerActions;

use CleantalkSP\SpbctWP\Scanner\Helper;

class FileSystemActions
{
    /**
     * tested
     * Delete file from the database handler.
     * @param int|string $file_id
     * @return array
     */
    public static function deleteFile($file_id)
    {
        global $wpdb;

        if (!$file_id) {
            return array('error' => 'WRONG_FILE_ID');
        }

        $root_path = spbc_get_root_path();
        $response_content_ok = false;
        $response_content_admin_ok = false;

        // Getting file info.
        $sql = 'SELECT * FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE fast_hash = %s LIMIT 1';
        $sql_result = $wpdb->get_results($wpdb->prepare($sql, $file_id), ARRAY_A);
        if (!isset($sql_result[0])) {
            return array('error' => 'FILE_NOT_FOUND');
        }

        $file_info  = $sql_result[0];

        if (empty($file_info)) {
            return array('error' => 'FILE_NOT_FOUND');
        }

        if (Helper::isWpConfigPath($file_info['path'])) {
            return array('error' => Helper::getWpConfigManualCleanupMessage());
        }

        if ($file_info['status'] == 'QUARANTINED') {
            return array('error' => 'USE_QUARANTINE_DELETE');
        }

        $file_path = $root_path . $file_info['path'];

        if (!file_exists($file_path)) {
            return array('error' => 'FILE_NOT_EXISTS');
        }

        if (!is_writable($file_path)) {
            return array('error' => 'FILE_NOT_WRITABLE');
        }

        $is_file_required_result = self::isFileRequiredInPhpIni($file_path);
        if ($is_file_required_result !== false) {
            return $is_file_required_result === null
                ? array('error' => 'PHP_INI_REQUIREMENTS_CHECK_FAIL')
                : array('error' => 'FILE_IS_REQUIRED_IN_PHP_INI');
        }

        // Getting file and prechecking website
        $remembered_file_content = file_get_contents($file_path);
        $precheck_public = wp_remote_get(get_option('home'));
        $precheck_admin = wp_remote_get(get_option('home') . '/wp-admin');

        $result = unlink($file_path);

        if (!$result) {
            return array('error' => 'FILE_COULDNT_DELETE');
        }

        // if not work before and after unlink - it's ok, if both work - it's ok too and much better
        $postcheck_public = wp_remote_get(get_option('home'));
        if ((is_wp_error($postcheck_public) && is_wp_error($precheck_public)) ||
            (!is_wp_error($postcheck_public) && !is_wp_error($precheck_public) &&
             wp_remote_retrieve_response_code($postcheck_public) === wp_remote_retrieve_response_code($precheck_public))
        ) {
            $response_content_ok = true;
        }

        // if not work before and after unlink - it's ok, if both work - it's ok too and much better
        $postcheck_admin = wp_remote_get(get_option('home') . '/wp-admin');
        if ((is_wp_error($postcheck_admin) && is_wp_error($precheck_admin)) ||
            (!is_wp_error($postcheck_admin) && !is_wp_error($precheck_admin) &&
             wp_remote_retrieve_response_code($postcheck_admin) === wp_remote_retrieve_response_code($precheck_admin))
        ) {
            $response_content_admin_ok = true;
        }

        // general check to revert if something went wrong
        if (!$response_content_admin_ok || !$response_content_ok) {
            $result = file_put_contents($file_path, $remembered_file_content);
            $output = array('error' => 'WEBSITE_RESPONSE_BAD');
            $output['error'] .= $result === false ? ' REVERT_FAILED' : ' REVERT_OK';
            return $output;
        }

        // Deleting row from DB
        $sql = 'DELETE FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE fast_hash = %s';
        $result = $wpdb->query($wpdb->prepare($sql, $file_id));
        if ($result !== false) {
            $output = array('success' => true);
        } else {
            $output = array('error' => 'DB_COULDNT_DELETE_ROW');
        }

        unset($remembered_file_content);

        return $output;
    }

    /**
     * Download file function
     */
    public static function downloadFile($file_id = null)
    {
        global $wpdb;

        if (!$file_id) {
            return array('error' => 'WRONG_FILE_ID');
        }

        $root_path = spbc_get_root_path();

        $sql = $wpdb->prepare('SELECT *
        FROM ' . SPBC_TBL_SCAN_FILES . '
        WHERE fast_hash = %s
        LIMIT 1', $file_id);
        $sql_result = $wpdb->get_results($sql, ARRAY_A);

        if (empty($sql_result) || !isset($sql_result[0])) {
            return array('error' => 'FILE_NOT_FOUND');
        }

        $file_info = $sql_result[0];

        if (empty($file_info)) {
            return array('error' => 'FILE_NOT_FOUND');
        }

        $file_path = $file_info['status'] == 'QUARANTINED' ? $file_info['q_path'] : $root_path . $file_info['path'];

        if (!file_exists($file_path)) {
            return array('error' => 'FILE_NOT_EXISTS');
        }

        if (!is_readable($file_path)) {
            return array('error' => 'FILE_NOT_READABLE');
        }

        $file_content = file_get_contents($file_path);

        if ($file_content === false) {
            return array('error' => 'FILE_EMPTY');
        }

        return array(
            'file_name'    => preg_replace('/.*(\/|\\\\)(.*)/', '$2', $file_info['path']),
            'file_content' => $file_content,
        );
    }

    /**
     * tested
     * Check if the filepath is required in php.ini settings. It could be set in "auto_prepend_file" or in "auto_append_file" keys strings.
     * @param $file_path
     * @return bool|null True if files is required, false otherwise. Null on any code fail.
     */
    public static function isFileRequiredInPhpIni($file_path)
    {
        try {
            $ini_auto_prepend_file_req[] = @ini_get('auto_prepend_file');
            $ini_auto_prepend_file_req[] = @ini_get('auto_append_file');
            if (!empty($file_path) && is_string($file_path)) {
                foreach ($ini_auto_prepend_file_req as $required_string) {
                    if (is_string($required_string)) {
                        if (strpos($required_string, $file_path) !== false ||
                            (!empty(basename($file_path)) && strpos($required_string, basename($file_path)) !== false)) {
                            return true;
                        }
                    }
                }
            }
        } catch (\Exception $_e) {
            return null;
        }
        return false;
    }

    /**
     * tested
     * Filter paths by directory exists
     * @param $paths array existed directories
     *
     * @return array
     */
    public static function filterExistsDirectories($paths)
    {
        $exists_dirs = array();

        foreach ($paths as $path) {
            if (is_dir(ABSPATH . $path)) {
                $exists_dirs[] = $path;
            }
        }

        return $exists_dirs;
    }

    /**
     * Filter paths by file exists
     * @param array $paths
     * @return array
     */
    public static function filterExistsFiles($paths)
    {
        $exists_files = array();

        foreach ($paths as $path) {
            $full_path = ABSPATH . ltrim($path, '/\\');
            if (is_file($full_path)) {
                $exists_files[] = $path;
            }
        }

        return $exists_files;
    }

    /**
     * Replace file with original
     * @param string $file_id
     * @return array
     */
    public static function replaceFileWithOriginal($file_id)
    {
        global $wpdb;

        if (!$file_id) {
            return array('error' => 'WRONG_FILE_ID');
        }

        $time_start = microtime(true);
        $root_path = spbc_get_root_path();

        // Getting file info.
        $sql = $wpdb->prepare('SELECT path, source_type, source, version, status, severity, source_type
        FROM ' . SPBC_TBL_SCAN_FILES . '
        WHERE fast_hash = %s
        LIMIT 1', $file_id);
        $sql_result = $wpdb->get_results($sql, ARRAY_A);
        $file_info  = isset($sql_result[0]) ? $sql_result[0] : null;

        if (empty($file_info)) {
            return array('error' => 'FILE_NOT_FOUND');
        }

        if (Helper::isWpConfigPath($file_info['path'])) {
            return array('error' => Helper::getWpConfigManualCleanupMessage());
        }

        if (!file_exists($root_path . $file_info['path'])) {
            return array('error' => 'FILE_NOT_EXISTS');
        }

        if (!is_writable($root_path . $file_info['path'])) {
            return array('error' => 'FILE_NOT_WRITABLE');
        }

        // Getting file && API call
        $original_file = Helper::getOriginalFile($file_info);

        if (is_array($original_file) && isset($original_file['error'])) {
            return array('error' => 'GET_FILE_FAILED');
        }

        $file_desc = fopen($root_path . $file_info['path'], 'w');
        if (!$file_desc || !is_string($original_file)) {
            return array('error' => 'FILE_COULDNT_OPEN');
        }

        $res_fwrite = fwrite($file_desc, $original_file);
        if (!$res_fwrite) {
            return array('error' => 'FILE_COULDNT_WRITE');
        }
        fclose($file_desc);

        $db_result = $wpdb->query(
            $wpdb->prepare(
                'DELETE FROM ' . SPBC_TBL_SCAN_FILES
                . ' WHERE fast_hash = %s;',
                $file_id
            )
        );

        if (!$db_result) {
            return array('error' => 'FILE_DB_DELETE_FAIL');
        }

        $output = array('success' => true);
        $output['exec_time'] = round(microtime(true) - $time_start);

        return $output;
    }

    /**
     * View file
     * @param string $file_id
     * @return array
     */
    public static function viewFile($file_id)
    {
        global $wpdb;

        $time_start = microtime(true);
        $root_path = spbc_get_root_path();

        // Check if file exists, delete it from the list if not
        $check_file_exist_result = \CleantalkSP\SpbctWP\ListTable::spbcCheckFileExist($file_id);
        if (isset($check_file_exist_result['error'])) {
            return array( 'error' => $check_file_exist_result['error']);
        }

        if (!$file_id) {
            return array('error' => 'WRONG_FILE_ID');
        }

        // Getting file info
        $sql = $wpdb->prepare(
            'SELECT *
        FROM ' . SPBC_TBL_SCAN_FILES . '
        WHERE fast_hash = %s
        LIMIT 1',
            $file_id
        );
        $sql_result = $wpdb->get_results($sql, ARRAY_A);
        $file_info  = isset($sql_result[0]) ? $sql_result[0] : null;

        if (empty($file_info)) {
            return array('error' => 'FILE_NOT_FOUND');
        }

        if (Helper::isWpConfigPath($file_info['path']) && ! Helper::canViewWpConfigContents()) {
            return array('error' => Helper::getWpConfigViewRestrictedMessage());
        }

        $file_path = $file_info['status'] == 'QUARANTINED' ? $file_info['q_path'] : $root_path . $file_info['path'];

        if (!file_exists($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'));
        }

        if (!is_readable($file_path)) {
            return array('error' => 'FILE_NOT_READABLE');
        }

        // Binary files: render as hex dump (first chunk only)
        if (isset($file_info['source']) && $file_info['source'] === 'BINARY') {
            $file_text = self::renderBinaryHexDump($file_path);
            if (empty($file_text)) {
                return array('error' => 'FILE_EMPTY');
            }

            return array(
                'success'    => true,
                'file'       => $file_text,
                'file_path'  => $file_path,
                'difference' => $file_info['difference'],
                'weak_spots' => $file_info['weak_spots'],
                'exec_time'  => round(microtime(true) - $time_start),
            );
        }

        $file = file($file_path);
        if (!$file) {
            return array('error' => 'FILE_EMPTY');
        }

        $file_text = array();
        for ($i = 0; isset($file[ $i ]); $i++) {
            $file_text[ $i + 1 ] = htmlspecialchars($file[ $i ]);
            $file_text[ $i + 1 ] = preg_replace("/[^\S]{4}/", "&nbsp;", $file_text[ $i + 1 ]);
        }

        if (empty($file_text)) {
            return array('error' => 'FILE_TEXT_EMPTY');
        }

        return array(
            'success'    => true,
            'file'       => $file_text,
            'file_path'  => $file_path,
            'difference' => $file_info['difference'],
            'weak_spots' => $file_info['weak_spots'],
            'exec_time'  => round(microtime(true) - $time_start),
        );
    }

    /**
     * Render the first bytes of a binary file as a classic hex dump.
     * Each output line: offset | 16 hex bytes | ASCII gutter.
     *
     * @param string $file_path
     * @return array<int,string>
     */
    private static function renderBinaryHexDump($file_path)
    {
        $max_bytes = 4096;
        $bytes_per_line = 16;

        $handle = @fopen($file_path, 'rb');
        if (!$handle) {
            return array();
        }

        $data = @fread($handle, $max_bytes);
        $file_size = @filesize($file_path);
        @fclose($handle);

        if ($data === false || $data === '') {
            return array();
        }

        $len = strlen($data);
        $rows = array();
        $line_no = 1;

        for ($offset = 0; $offset < $len; $offset += $bytes_per_line) {
            $chunk = substr($data, $offset, $bytes_per_line);
            $hex_parts = array();
            $ascii = '';
            $chunk_len = strlen($chunk);
            for ($i = 0; $i < $bytes_per_line; $i++) {
                if ($i < $chunk_len) {
                    $byte = ord($chunk[$i]);
                    $hex_parts[] = sprintf('%02X', $byte);
                    $ascii .= ($byte >= 0x20 && $byte <= 0x7E) ? chr($byte) : '.';
                } else {
                    $hex_parts[] = '  ';
                    $ascii .= ' ';
                }
            }
            $hex_block = implode(' ', array_slice($hex_parts, 0, 8))
                . '  '
                . implode(' ', array_slice($hex_parts, 8, 8));

            $rows[$line_no] = sprintf(
                '%08X  %s  |%s|',
                $offset,
                $hex_block,
                htmlspecialchars($ascii)
            );
            $line_no++;
        }

        if (is_int($file_size) && $file_size > $len) {
            $rows[$line_no] = sprintf(
                '... %s',
                sprintf(
                    esc_html__('truncated, showing first %1$d of %2$d bytes', 'security-malware-firewall'),
                    $len,
                    $file_size
                )
            );
        }

        return $rows;
    }
}
