<?php

declare(strict_types=1);

namespace SiteFort\Modules\Scanner\Service;

use SiteFort\Includes\Capabilities;
use SiteFort\Includes\DataDirectoryProtection;
use SiteFort\Includes\SitePathResolver;
use SiteFort\Modules\Scanner\Repository\QuarantineRepository;

if ( ! defined( 'ABSPATH' ) ) { exit; }

class QuarantineService {

    private const MAX_FILE_BYTES = 16777216;
    private const STORAGE_DIR_NAME = 'quarantine';
    private const MODE_DELETE_QUARANTINE = 'delete_quarantine';
    private const MODE_REPAIR_SNAPSHOT = 'repair_snapshot';
    private const PURGE_ALL_BATCH_SIZE = 100;
    private const PURGE_ALL_MAX_ERRORS = 10;

    /**
     * @param array<string,mixed> $issue_row
     * @param array<string,mixed> $resolved
     * @return array<string,mixed>|\WP_Error
     */
    public function quarantine_for_delete( array $issue_row, array $resolved, int $user_id ) {
        return $this->create_quarantine_item( $issue_row, $resolved, self::MODE_DELETE_QUARANTINE, 'delete', $user_id );
    }

    /**
     * @param array<string,mixed> $issue_row
     * @param array<string,mixed> $resolved
     * @return array<string,mixed>|\WP_Error
     */
    public function snapshot_for_repair( array $issue_row, array $resolved, int $user_id ) {
        return $this->create_quarantine_item( $issue_row, $resolved, self::MODE_REPAIR_SNAPSHOT, 'repair', $user_id );
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function restore_item( int $item_id, int $user_id ) {
        $item = QuarantineRepository::get_by_id( $item_id );
        if ( ! is_array( $item ) ) {
            return new \WP_Error( 'quarantine_item_missing', 'The selected quarantine record could not be found.' );
        }

        $mode = sanitize_key( (string) ( $item['quarantine_mode'] ?? '' ) );
        $status = sanitize_key( (string) ( $item['status'] ?? '' ) );
        $file = ltrim( (string) ( $item['original_relative_path'] ?? '' ), '/' );

        if ( $mode === self::MODE_DELETE_QUARANTINE ) {
            if ( $status !== 'active' ) {
                return new \WP_Error( 'quarantine_item_inactive', 'Only active deleted-file quarantine records can be restored.' );
            }
            if ( ! $this->has_restore_permission( $file ) ) {
                return new \WP_Error( 'quarantine_restore_forbidden', $this->get_restore_permission_message( $file ) );
            }

            return $this->restore_deleted_item( $item, $user_id );
        }

        if ( $mode === self::MODE_REPAIR_SNAPSHOT ) {
            if ( $status !== 'active' ) {
                return new \WP_Error( 'quarantine_item_inactive', 'Only active pre-repair backups can be restored.' );
            }
            if ( ! $this->has_restore_permission( $file ) ) {
                return new \WP_Error( 'quarantine_restore_forbidden', $this->get_restore_permission_message( $file ) );
            }

            return $this->restore_live_file_snapshot( $item, $user_id );
        }

        return new \WP_Error( 'quarantine_restore_not_supported', 'This quarantine record cannot be restored automatically.' );
    }

    /**
     * @param array<string,mixed> $item
     * @return array<string,mixed>|\WP_Error
     */
    private function restore_deleted_item( array $item, int $user_id ) {
        $item_id = (int) ( $item['id'] ?? 0 );
        $target = $this->resolve_restore_target_path( (string) ( $item['original_relative_path'] ?? '' ) );
        if ( is_wp_error( $target ) ) {
            return $target;
        }

        if ( file_exists( $target ) ) {
            return new \WP_Error( 'quarantine_restore_collision', 'SiteFort will not overwrite an existing file during restore. Remove or rename the current file first.' );
        }

        $applied = $this->restore_item_to_target( $item, $target, false, null );
        if ( is_wp_error( $applied ) ) {
            return $applied;
        }

        QuarantineRepository::mark_restored( $item_id, time() );
        $effect = $this->get_issue_state_effect( $item );

	                return [
            'item'               => QuarantineRepository::get_by_id( $item_id ),
            'message'            => 'File restored from quarantine.',
            'details'            => [
                'item_id' => $item_id,
                'file'    => ltrim( (string) ( $item['original_relative_path'] ?? '' ), '/' ),
                'mode'    => self::MODE_DELETE_QUARANTINE,
                'actor'   => $user_id,
            ],
            'issue_state_effect' => $effect['effect'],
            'issue_hash'         => $effect['issue_hash'],
            'issue_snapshot'     => $effect['issue_snapshot'],
        ];
    }

    /**
     * @param array<string,mixed> $item
     * @return array<string,mixed>|\WP_Error
     */
    private function restore_live_file_snapshot( array $item, int $user_id ) {
        $item_id = (int) ( $item['id'] ?? 0 );
        $target = $this->resolve_restore_target_path( (string) ( $item['original_relative_path'] ?? '' ) );
        if ( is_wp_error( $target ) ) {
            return $target;
        }

        if ( file_exists( $target ) && is_link( $target ) ) {
            return new \WP_Error( 'quarantine_restore_invalid_path', 'SiteFort will not replace a symlinked file during restore.' );
        }

        $current_permissions = null;

        if ( file_exists( $target ) ) {
            if ( ! is_file( $target ) || ! is_readable( $target ) ) {
                return new \WP_Error( 'quarantine_restore_target_unreadable', 'SiteFort could not read the current live file before restore.' );
            }

            $current_permissions = $this->get_file_permissions( $target );
            $current_hash = $this->hash_file( $target );
            if ( is_wp_error( $current_hash ) ) {
                return $current_hash;
            }

            $expected_snapshot_hash = sanitize_text_field( (string) ( $item['original_sha256'] ?? '' ) );
            if ( $expected_snapshot_hash !== '' && hash_equals( $expected_snapshot_hash, $current_hash ) ) {
                QuarantineRepository::mark_restored( $item_id, time() );
                return [
                    'item'               => QuarantineRepository::get_by_id( $item_id ),
                    'message'            => 'The current file already matches this snapshot.',
                    'details'            => [
                        'item_id' => $item_id,
                        'file'    => ltrim( (string) ( $item['original_relative_path'] ?? '' ), '/' ),
                        'mode'    => sanitize_key( (string) ( $item['quarantine_mode'] ?? '' ) ),
                        'actor'   => $user_id,
                        'result'  => 'noop',
                    ],
                    'issue_state_effect' => 'none',
                    'issue_hash'         => '',
                    'issue_snapshot'     => null,
                ];
            }
        }

        $applied = $this->restore_item_to_target( $item, $target, true, $current_permissions );
        if ( is_wp_error( $applied ) ) {
            return $applied;
        }

        QuarantineRepository::mark_restored( $item_id, time() );
        $effect = $this->get_issue_state_effect( $item );

        return [
            'item'               => QuarantineRepository::get_by_id( $item_id ),
            'message'            => 'Backup restored to the live file.',
            'details'            => [
                'item_id' => $item_id,
                'file'    => ltrim( (string) ( $item['original_relative_path'] ?? '' ), '/' ),
                'mode'    => sanitize_key( (string) ( $item['quarantine_mode'] ?? '' ) ),
                'actor'   => $user_id,
                'result'  => 'restored',
            ],
            'issue_state_effect' => $effect['effect'],
            'issue_hash'         => $effect['issue_hash'],
            'issue_snapshot'     => $effect['issue_snapshot'],
        ];
    }

    /**
     * @param array<string,mixed> $item
     * @return array<string,mixed>|\WP_Error
     */
    private function restore_item_to_target( array $item, string $target, bool $allow_overwrite, ?int $permission_bits ) {
        $blob_path = $this->resolve_blob_path( (string) ( $item['blob_rel_path'] ?? '' ) );
        if ( is_wp_error( $blob_path ) ) {
            return $blob_path;
        }

        $target_dir = dirname( $target );
        if ( ! is_dir( $target_dir ) && ! wp_mkdir_p( $target_dir ) ) {
            return new \WP_Error( 'quarantine_restore_directory_failed', 'SiteFort could not create the original directory for restore.' );
        }

        $temp_target = $target_dir . '/.sitefort-restore-' . wp_generate_uuid4() . '.tmp';
        $decrypted = QuarantineCipher::decrypt_file( $blob_path, $temp_target );
        if ( is_wp_error( $decrypted ) ) {
            return $decrypted;
        }

        $expected_hash = sanitize_text_field( (string) ( $item['original_sha256'] ?? '' ) );
        $actual_hash = sanitize_text_field( (string) ( $decrypted['sha256'] ?? '' ) );
        if ( $expected_hash !== '' && $actual_hash !== '' && ! hash_equals( $expected_hash, $actual_hash ) ) {
            @unlink( $temp_target ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            return new \WP_Error( 'quarantine_restore_hash_mismatch', 'SiteFort refused to restore the file because the quarantine blob failed integrity verification.' );
        }

        if ( is_int( $permission_bits ) && $permission_bits > 0 ) {
            @chmod( $temp_target, $permission_bits ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
        }

        if ( ! $allow_overwrite && file_exists( $target ) ) {
            @unlink( $temp_target ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            return new \WP_Error( 'quarantine_restore_collision', 'SiteFort will not overwrite an existing file during restore. Remove or rename the current file first.' );
        }

        if ( ! @rename( $temp_target, $target ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
            @unlink( $temp_target ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            return new \WP_Error( 'quarantine_restore_move_failed', 'SiteFort could not place the restored file back into its original location.' );
        }

        if ( is_int( $permission_bits ) && $permission_bits > 0 ) {
            @chmod( $target, $permission_bits ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
        }

        return [
            'sha256' => $actual_hash,
        ];
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function purge_item( int $item_id, int $user_id = 0, bool $expired = false ) {
        $item = QuarantineRepository::get_by_id( $item_id );
        if ( ! is_array( $item ) ) {
            return new \WP_Error( 'quarantine_item_missing', 'The selected quarantine record could not be found.' );
        }

        if ( in_array( (string) ( $item['status'] ?? '' ), [ 'purged' ], true ) ) {
            return new \WP_Error( 'quarantine_item_already_purged', 'This quarantine record has already been purged.' );
        }

        $blob_path = $this->resolve_blob_path( (string) ( $item['blob_rel_path'] ?? '' ) );
        if ( is_wp_error( $blob_path ) ) {
            if ( $blob_path->get_error_code() !== 'quarantine_blob_missing' ) {
                return $blob_path;
            }
        } elseif ( file_exists( $blob_path ) && ! @unlink( $blob_path ) && file_exists( $blob_path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            return new \WP_Error( 'quarantine_blob_delete_failed', 'SiteFort could not delete the encrypted quarantine blob.' );
        }

        if ( ! QuarantineRepository::mark_purged( $item_id, time() ) ) {
            return new \WP_Error( 'quarantine_manifest_update_failed', 'SiteFort deleted the quarantine blob but could not mark the record as purged.' );
        }

        return [
            'item'    => QuarantineRepository::get_by_id( $item_id ),
            'message' => $expired ? 'Expired quarantine item purged.' : 'Quarantine item purged.',
            'details' => [
                'item_id' => $item_id,
                'file'    => ltrim( (string) ( $item['original_relative_path'] ?? '' ), '/' ),
                'mode'    => sanitize_key( (string) ( $item['quarantine_mode'] ?? '' ) ),
                'expired' => $expired,
                'actor'   => $user_id,
            ],
        ];
    }

    /**
     * Purge all active/restored vault items in bounded batches.
     *
     * @return array<string,mixed>
     */
    public function purge_all_items( int $user_id ): array {
        $statuses = [ 'active', 'restored' ];
        $modes = $this->get_listable_modes();
        $started_total = QuarantineRepository::count_items( $statuses, $modes );
        $max_id = QuarantineRepository::get_max_item_id( $statuses, $modes );
        $last_id = 0;
        $purged_count = 0;
        $failed_count = 0;
        $errors = [];

        while ( $last_id < $max_id ) {
            $items = QuarantineRepository::list_items_after_id(
                self::PURGE_ALL_BATCH_SIZE,
                $last_id,
                $max_id,
                $statuses,
                $modes
            );

            if ( empty( $items ) ) {
                break;
            }

            foreach ( $items as $item ) {
                $item_id = (int) ( $item['id'] ?? 0 );
                $last_id = max( $last_id, $item_id );
                if ( $item_id <= 0 ) {
                    continue;
                }

                $result = $this->purge_item( $item_id, $user_id, false );
                if ( is_wp_error( $result ) ) {
                    $failed_count++;
                    if ( count( $errors ) < self::PURGE_ALL_MAX_ERRORS ) {
                        $errors[] = [
                            'item_id' => $item_id,
                            'file'    => ltrim( (string) ( $item['original_relative_path'] ?? '' ), '/' ),
                            'message' => $result->get_error_message(),
                        ];
                    }
                    continue;
                }

                $purged_count++;
            }
        }

        $remaining = QuarantineRepository::count_items( $statuses, $modes );
        $message = $this->build_purge_all_message( $purged_count, $failed_count, $remaining );

        return [
            'message'      => $message,
            'purged_count' => $purged_count,
            'failed_count' => $failed_count,
            'remaining'    => $remaining,
            'errors'       => $errors,
            'details'      => [
                'bulk'          => true,
                'actor'         => $user_id,
                'started_total' => $started_total,
                'purged_count'  => $purged_count,
                'failed_count'  => $failed_count,
                'remaining'     => $remaining,
            ],
        ];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function list_items( int $limit = 50, int $offset = 0 ): array {
        $rows = QuarantineRepository::list_items( $limit, $offset, [ 'active', 'restored' ], $this->get_listable_modes() );

        return array_map(
            function ( array $row ): array {
                $meta = isset( $row['meta'] ) && is_array( $row['meta'] ) ? $row['meta'] : [];
                $file = ltrim( (string) ( $row['original_relative_path'] ?? '' ), '/' );
                $mode = sanitize_key( (string) ( $row['quarantine_mode'] ?? '' ) );
                $status = sanitize_key( (string) ( $row['status'] ?? '' ) );

                return [
                    'id'                  => (int) ( $row['id'] ?? 0 ),
                    'issue_hash'          => (string) ( $row['issue_hash'] ?? '' ),
                    'issue_type'          => (string) ( $row['issue_type'] ?? '' ),
                    'file'                => $file,
                    'file_name'           => basename( $file ),
                    'status'              => $status,
                    'mode'                => $mode,
                    'mode_label'          => $this->get_mode_label( $mode ),
                    'mode_description'    => $this->get_mode_description( $mode ),
                    'size'                => (int) ( $row['original_size'] ?? 0 ),
                    'mime_type'           => (string) ( $row['mime_type'] ?? '' ),
                    'created_at'          => (int) ( $row['created_at'] ?? 0 ),
                    'expires_at'          => (int) ( $row['expires_at'] ?? 0 ),
                    'restored_at'         => (int) ( $row['restored_at'] ?? 0 ),
                    'can_restore'         => $this->can_restore_item( $mode, $status, $file ),
                    'restore_label'       => $this->get_restore_label( $mode ),
                    'can_download'        => in_array( $status, [ 'active', 'restored' ], true ),
                    'can_purge'           => $status !== 'purged',
                    'source_action'       => sanitize_key( (string) ( $meta['source_action'] ?? '' ) ),
                    'provider'            => sanitize_key( (string) ( $meta['provider'] ?? '' ) ),
                ];
            },
            $rows
        );
    }

    public function count_items(): int {
        return QuarantineRepository::count_items( [ 'active', 'restored' ], $this->get_listable_modes() );
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function prepare_download( int $item_id ) {
        $item = QuarantineRepository::get_by_id( $item_id );
        if ( ! is_array( $item ) ) {
            return new \WP_Error( 'quarantine_item_missing', 'The selected quarantine record could not be found.' );
        }

        if ( ! in_array( (string) ( $item['status'] ?? '' ), [ 'active', 'restored' ], true ) ) {
            return new \WP_Error( 'quarantine_item_not_downloadable', 'This quarantine record is not available for download.' );
        }

        $blob_path = $this->resolve_blob_path( (string) ( $item['blob_rel_path'] ?? '' ) );
        if ( is_wp_error( $blob_path ) ) {
            return $blob_path;
        }

        $temp_file = tempnam( sys_get_temp_dir(), 'sitefortq_' );
        if ( ! is_string( $temp_file ) || $temp_file === '' ) {
            return new \WP_Error( 'quarantine_download_directory_failed', 'SiteFort could not prepare a temporary download file.' );
        }
        $decrypted = QuarantineCipher::decrypt_file( $blob_path, $temp_file );
        if ( is_wp_error( $decrypted ) ) {
            return $decrypted;
        }

        $expected_hash = (string) ( $item['original_sha256'] ?? '' );
        $actual_hash = (string) ( $decrypted['sha256'] ?? '' );
        if ( $expected_hash !== '' && $actual_hash !== '' && ! hash_equals( $expected_hash, $actual_hash ) ) {
            @unlink( $temp_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            return new \WP_Error( 'quarantine_download_hash_mismatch', 'SiteFort refused to download the file because the quarantine blob failed integrity verification.' );
        }

        return [
            'item'      => $item,
            'temp_file' => $temp_file,
        ];
    }

    public function cleanup_download_temp( string $temp_file ): void {
        $temp_file = wp_normalize_path( $temp_file );
        if ( $temp_file === '' || ! file_exists( $temp_file ) ) {
            return;
        }

        @unlink( $temp_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
    }

    public function discard_item( int $item_id ): bool {
        $item = QuarantineRepository::get_by_id( $item_id );
        if ( ! is_array( $item ) ) {
            return false;
        }

        $blob_path = $this->resolve_blob_path( (string) ( $item['blob_rel_path'] ?? '' ) );
        if ( is_wp_error( $blob_path ) ) {
            if ( $blob_path->get_error_code() !== 'quarantine_blob_missing' ) {
                return false;
            }
        } elseif ( file_exists( $blob_path ) && ! @unlink( $blob_path ) && file_exists( $blob_path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            return false;
        }

        return QuarantineRepository::delete_item( $item_id );
    }

    public function purge_expired_items(): int {
        $purged = 0;
        $expired_items = QuarantineRepository::get_expired_items( time(), 100 );

        foreach ( $expired_items as $item ) {
            $result = $this->purge_item( (int) ( $item['id'] ?? 0 ), 0, true );
            if ( is_wp_error( $result ) ) {
                continue;
            }
            $details = isset( $result['details'] ) && is_array( $result['details'] ) ? $result['details'] : [];
            \SiteFort\Modules\AuditLog\AuditLog::add_log( 'scan_action_quarantine_purge', $details, 'warning', 'scanner' );
            $purged++;
        }

        return $purged;
    }

    public function cleanup_old_purged_records( int $older_than_timestamp ): int {
        return QuarantineRepository::delete_old_purged_records( $older_than_timestamp, 100 );
    }

    public function cleanup_stale_staged_items( int $older_than_timestamp ): int {
        $discarded = 0;
        $stale_items = QuarantineRepository::get_stale_staged_items( $older_than_timestamp, 100 );

        foreach ( $stale_items as $item ) {
            $item_id = (int) ( $item['id'] ?? 0 );
            if ( $item_id <= 0 ) {
                continue;
            }

            if ( $this->discard_item( $item_id ) ) {
                $discarded++;
            }
        }

        return $discarded;
    }

    public static function get_retention_days(): int {
        if ( ! class_exists( ScannerSettings::class ) ) {
            require_once __DIR__ . '/ScannerSettings.php';
        }

        $days = (int) ScannerSettings::get( 'quarantine_retention_days' );
        if ( $days <= 0 ) {
            $days = 30;
        }

        return max( 1, min( 30, $days ) );
    }

    public function get_storage_directory(): string {
        return WP_CONTENT_DIR . '/sitefort-data/' . self::STORAGE_DIR_NAME;
    }

    /**
     * @return array<string,mixed>|\WP_Error
     */
    public function activate_item( int $item_id ) {
        if ( $item_id <= 0 ) {
            return new \WP_Error( 'quarantine_item_missing', 'The selected quarantine record could not be found.' );
        }

        if ( ! QuarantineRepository::mark_active( $item_id ) ) {
            return new \WP_Error( 'quarantine_activation_failed', 'SiteFort could not activate the quarantine record.' );
        }

        $item = QuarantineRepository::get_by_id( $item_id );
        if ( ! is_array( $item ) || ( $item['status'] ?? '' ) !== 'active' ) {
            return new \WP_Error( 'quarantine_activation_failed', 'SiteFort could not verify the quarantine record state.' );
        }

        return $item;
    }

    private function create_quarantine_item( array $issue_row, array $resolved, string $mode, string $source_action, int $user_id ) {
        $validated = $this->validate_candidate( $issue_row, $resolved, $source_action );
        if ( is_wp_error( $validated ) ) {
            return $validated;
        }

        $storage_ready = $this->ensure_storage_ready();
        if ( is_wp_error( $storage_ready ) ) {
            return $storage_ready;
        }

        $blob_rel_path = $this->generate_blob_relative_path();
        $blob_abs_path = trailingslashit( $this->get_storage_directory() ) . $blob_rel_path;
        $blob_dir = dirname( $blob_abs_path );
        if ( ! is_dir( $blob_dir ) && ! wp_mkdir_p( $blob_dir ) ) {
            return new \WP_Error( 'quarantine_destination_unwritable', 'SiteFort could not create the quarantine storage path.' );
        }

        $encrypted = QuarantineCipher::encrypt_file( (string) $validated['absolute_path'], $blob_abs_path );
        if ( is_wp_error( $encrypted ) ) {
            return $encrypted;
        }

        $meta = [
            'source_action' => $source_action,
            'provider'      => (string) ( $encrypted['provider'] ?? '' ),
        ];

        $created_at = time();
        $item_id = QuarantineRepository::create(
            [
                'issue_hash'             => (string) ( $issue_row['issue_hash'] ?? '' ),
                'scan_id'                => (string) ( $issue_row['scan_id'] ?? $issue_row['scan_run_id'] ?? '' ),
                'issue_type'             => (string) ( $issue_row['type'] ?? '' ),
                'original_relative_path' => (string) $validated['relative_path'],
                'blob_rel_path'          => $blob_rel_path,
                'quarantine_mode'        => $mode,
                'status'                 => 'staged',
                'original_sha256'        => (string) ( $encrypted['sha256'] ?? '' ),
                'original_size'          => (int) ( $encrypted['size'] ?? 0 ),
                'mime_type'              => '',
                'created_by'             => max( 0, $user_id ),
                'created_at'             => $created_at,
                'expires_at'             => $created_at + ( self::get_retention_days() * DAY_IN_SECONDS ),
                'issue_snapshot'         => $issue_row,
                'meta'                   => $meta,
            ]
        );

        if ( false === $item_id ) {
            @unlink( $blob_abs_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            return new \WP_Error( 'quarantine_manifest_write_failed', 'SiteFort could not save the quarantine record.' );
        }

        $item = QuarantineRepository::get_by_id( (int) $item_id );
        if ( ! is_array( $item ) ) {
            @unlink( $blob_abs_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            QuarantineRepository::delete_item( (int) $item_id );
            return new \WP_Error( 'quarantine_manifest_read_failed', 'SiteFort could not verify the new quarantine record.' );
        }

        return $item;
    }

    private function ensure_storage_ready() {
        $dir = $this->get_storage_directory();
        if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
            return new \WP_Error( 'quarantine_directory_create_failed', 'SiteFort could not create the quarantine directory.' );
        }

        $this->write_protection_files( $dir );

        return true;
    }

    private function write_protection_files( string $dir ): void {
        DataDirectoryProtection::ensure_local_protection( $dir );
    }

    private function generate_blob_relative_path(): string {
        return gmdate( 'Y/m' ) . '/' . wp_generate_uuid4() . '.qbin.php';
    }

    private function resolve_blob_path( string $blob_rel_path ) {
        $blob_rel_path = ltrim( wp_normalize_path( $blob_rel_path ), '/' );
        if ( '' === $blob_rel_path || preg_match( '#(?:^|/)\.\.(?:/|$)#', $blob_rel_path ) ) {
            return new \WP_Error( 'quarantine_blob_invalid_path', 'The quarantine blob path is invalid.' );
        }

        $base = wp_normalize_path( trailingslashit( $this->get_storage_directory() ) );
        $path = wp_normalize_path( $base . $blob_rel_path );
        if ( strpos( $path, $base ) !== 0 ) {
            return new \WP_Error( 'quarantine_blob_invalid_path', 'The quarantine blob path is invalid.' );
        }

        if ( ! file_exists( $path ) && str_ends_with( $path, '.php' ) ) {
            $legacy_path = substr( $path, 0, -4 );
            if ( file_exists( $legacy_path ) ) {
                $path = $legacy_path;
            }
        }

        if ( ! file_exists( $path ) ) {
            return new \WP_Error( 'quarantine_blob_missing', 'The encrypted quarantine blob is no longer available.' );
        }

        return $path;
    }

    private function resolve_restore_target_path( string $relative_path ) {
        $relative_path = ltrim( wp_normalize_path( $relative_path ), '/' );
        if ( '' === $relative_path || preg_match( '#(?:^|/)\.\.(?:/|$)#', $relative_path ) ) {
            return new \WP_Error( 'quarantine_restore_invalid_path', 'The recorded restore path is invalid.' );
        }

        if ( strpos( $relative_path, 'wp-content/sitefort-data/' ) === 0 ) {
            return new \WP_Error( 'quarantine_restore_invalid_path', 'SiteFort runtime paths cannot be used as quarantine restore targets.' );
        }

        if ( $this->is_protected_root_config_file( $relative_path ) ) {
            return new \WP_Error( 'quarantine_restore_invalid_path', 'This protected file cannot be restored automatically.' );
        }

        $wp_root = wp_normalize_path( trailingslashit( ABSPATH ) );
        $target = wp_normalize_path( $wp_root . $relative_path );
        if ( strpos( $target, $wp_root ) !== 0 ) {
            return new \WP_Error( 'quarantine_restore_invalid_path', 'The recorded restore path is outside the WordPress root.' );
        }

        $existing_parent = $this->resolve_existing_parent_directory( $target );
        if ( is_wp_error( $existing_parent ) ) {
            return $existing_parent;
        }

        return $target;
    }

    private function resolve_existing_parent_directory( string $target_path ) {
        $wp_root = wp_normalize_path( trailingslashit( ABSPATH ) );
        $cursor = dirname( $target_path );

        while ( $cursor !== '' && $cursor !== '.' && $cursor !== dirname( $cursor ) ) {
            if ( file_exists( $cursor ) ) {
                if ( is_link( $cursor ) ) {
                    return new \WP_Error( 'quarantine_restore_invalid_path', 'SiteFort will not restore into a symlinked directory.' );
                }

                $resolved = realpath( $cursor );
                if ( false === $resolved ) {
                    return new \WP_Error( 'quarantine_restore_invalid_path', 'SiteFort could not verify the restore destination.' );
                }

                $normalized_resolved = wp_normalize_path( trailingslashit( $resolved ) );
                if ( strpos( $normalized_resolved, $wp_root ) !== 0 ) {
                    return new \WP_Error( 'quarantine_restore_invalid_path', 'SiteFort will not restore outside the WordPress root.' );
                }

                return $resolved;
            }

            $cursor = dirname( $cursor );
        }

        return null;
    }

    /**
     * @param array<string,mixed> $issue_row
     * @param array<string,mixed> $resolved
     * @return array<string,string>|\WP_Error
     */
    private function validate_candidate( array $issue_row, array $resolved, string $source_action ) {
        $absolute_path = wp_normalize_path( (string) ( $resolved['absolute_path'] ?? '' ) );
        $relative_path = ltrim( wp_normalize_path( (string) ( $resolved['relative_path'] ?? '' ) ), '/' );

        if ( '' === $absolute_path || '' === $relative_path ) {
            return new \WP_Error( 'quarantine_missing_target', 'SiteFort could not resolve the file path for quarantine.' );
        }

        if ( ! is_file( $absolute_path ) || ! is_readable( $absolute_path ) ) {
            return new \WP_Error( 'quarantine_source_unreadable', 'The selected file could not be read for quarantine.' );
        }

        if ( is_link( $absolute_path ) ) {
            return new \WP_Error( 'quarantine_symlink_blocked', 'Symlinked files cannot be quarantined.' );
        }

        $real_path = realpath( $absolute_path );
        $wp_root = realpath( ABSPATH );
        if ( false === $real_path || false === $wp_root ) {
            return new \WP_Error( 'quarantine_path_resolution_failed', 'SiteFort could not resolve the file path for quarantine.' );
        }

        $normalized_real_path = wp_normalize_path( $real_path );
        $normalized_wp_root = wp_normalize_path( trailingslashit( $wp_root ) );
        if ( strpos( $normalized_real_path, $normalized_wp_root ) !== 0 ) {
            return new \WP_Error( 'quarantine_outside_root', 'Files outside the WordPress root cannot be quarantined.' );
        }

        $file_size = @filesize( $normalized_real_path );
        if ( false === $file_size ) {
            return new \WP_Error( 'quarantine_size_unknown', 'SiteFort could not determine the file size for quarantine.' );
        }

        if ( $file_size > $this->get_max_file_bytes() ) {
            return new \WP_Error( 'quarantine_file_too_large', 'This file is too large to quarantine safely on this host. Delete or repair it without creating a quarantine copy.' );
        }

        if ( strpos( $relative_path, 'wp-content/sitefort-data/' ) === 0 ) {
            return new \WP_Error( 'quarantine_internal_file_blocked', 'SiteFort runtime files cannot be quarantined.' );
        }

        if ( $this->is_protected_root_config_file( $relative_path ) ) {
            return new \WP_Error( 'quarantine_protected_file_blocked', 'This protected file cannot be quarantined automatically.' );
        }

        $category = $this->normalize_issue_category( $issue_row );
        if ( ! in_array( $category, [ 'malware', 'integrity' ], true ) ) {
            return new \WP_Error( 'quarantine_not_supported', 'Quarantine is only available for malware and integrity file findings.' );
        }

        if ( ! in_array( $source_action, [ 'delete', 'repair' ], true ) ) {
            return new \WP_Error( 'quarantine_invalid_action', 'This remediation action does not support quarantine.' );
        }

        return [
            'absolute_path' => $normalized_real_path,
            'relative_path' => $relative_path,
        ];
    }

    private function is_protected_root_config_file( string $relative_path ): bool {
        $protected = [ 'wp-config.php', '.htaccess', '.user.ini' ];
        $user_ini_filename = SitePathResolver::get_user_ini_filename();
        if ( $user_ini_filename !== '' ) {
            $protected[] = $user_ini_filename;
        }

        return in_array( basename( $relative_path ), array_values( array_unique( $protected ) ), true );
    }

    /**
     * @param array<string,mixed> $item
     * @return array{effect:string,issue_hash:string,issue_snapshot:?array}
     */
    private function get_issue_state_effect( array $item ): array {
        $issue_hash = sanitize_text_field( (string) ( $item['issue_hash'] ?? '' ) );
        $issue_snapshot = isset( $item['issue_snapshot'] ) && is_array( $item['issue_snapshot'] ) ? $item['issue_snapshot'] : null;

        return [
            'effect'         => $issue_hash !== '' && is_array( $issue_snapshot ) ? 'reopen' : 'dirty',
            'issue_hash'     => $issue_hash,
            'issue_snapshot' => $issue_snapshot,
        ];
    }

    private function can_restore_item( string $mode, string $status, string $file ): bool {
        return $status === 'active'
            && in_array( $mode, $this->get_listable_modes(), true )
            && $file !== ''
            && $this->has_restore_permission( $file );
    }

    private function has_restore_permission( string $file ): bool {
        return $file !== '' && Capabilities::can_modify_scanner_file( $file );
    }

    private function get_restore_permission_message( string $file ): string {
        switch ( Capabilities::infer_scanner_file_component_type( $file ) ) {
            case 'plugin':
                return 'You do not have permission to restore plugin files from quarantine.';
            case 'theme':
                return 'You do not have permission to restore theme files from quarantine.';
            case 'core':
                return 'You do not have permission to restore WordPress core files from quarantine.';
            default:
                return 'You do not have permission to restore this quarantine item.';
        }
    }

    private function get_restore_label( string $mode ): string {
        if ( $mode === self::MODE_REPAIR_SNAPSHOT ) {
            return 'Restore Backup';
        }

        return 'Restore';
    }

    private function get_mode_label( string $mode ): string {
        if ( $mode === self::MODE_REPAIR_SNAPSHOT ) {
            return 'Pre-repair backup';
        }

        return 'Deleted file backup';
    }

    private function get_mode_description( string $mode ): string {
        if ( $mode === self::MODE_REPAIR_SNAPSHOT ) {
            return 'Backup saved before repair. Restoring it will replace the current file.';
        }

        return 'Deleted file backup kept for recovery. Restore it only if the original path should exist again.';
    }

    /**
     * @return array<int,string>
     */
    private function get_listable_modes(): array {
        return [
            self::MODE_DELETE_QUARANTINE,
            self::MODE_REPAIR_SNAPSHOT,
        ];
    }

    private function build_purge_all_message( int $purged_count, int $failed_count, int $remaining ): string {
        if ( $purged_count <= 0 && $failed_count <= 0 ) {
            return 'No quarantine items were available to purge.';
        }

        if ( $failed_count > 0 ) {
            return sprintf(
                'Purged %d quarantine item%s; %d failed. %d remain.',
                $purged_count,
                $purged_count === 1 ? '' : 's',
                $failed_count,
                $remaining
            );
        }

        return sprintf(
            'Purged %d quarantine item%s.',
            $purged_count,
            $purged_count === 1 ? '' : 's'
        );
    }

    private function get_file_permissions( string $path ): ?int {
        $permissions = @fileperms( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fileperms
        if ( false === $permissions ) {
            return null;
        }

        return (int) ( $permissions & 0777 );
    }

    /**
     * @return string|\WP_Error
     */
    private function hash_file( string $path ) {
        $hash = @hash_file( 'sha256', $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_hash_file
        if ( ! is_string( $hash ) || $hash === '' ) {
            return new \WP_Error( 'quarantine_restore_hash_failed', 'SiteFort could not verify the current live file before restore.' );
        }

        return strtolower( $hash );
    }

    /**
     * @param array<string,mixed> $issue_row
     */
    private function normalize_issue_category( array $issue_row ): string {
        $type = strtolower( (string) ( $issue_row['type'] ?? '' ) );
        if ( in_array( $type, [ 'integrity', 'fake_image', 'core', 'modified' ], true ) ) {
            return 'integrity';
        }
        if ( 'malware' === $type ) {
            return 'malware';
        }

        return $type;
    }

    private function get_max_file_bytes(): int {
        $max = (int) apply_filters( 'sitefort_quarantine_max_file_bytes', self::MAX_FILE_BYTES );
        if ( $max <= 0 ) {
            $max = self::MAX_FILE_BYTES;
        }

        return $max;
    }

}
