<?php

if(!defined('SOFTACULOUS')){
	die('Hacking Attempt');
}

interface AIIsolationProvider {
	public function isAvailable();
	public function getId();
	public function wrapCommand($command);
	public function wrapPhpCommand($php_cmd);
	public function isConfined();
}

class AICageFsIsolation implements AIIsolationProvider {
	private $cagefs_enter;
	private $available = null;

	private static $static_cache = array();

	public function __construct(){
		$this->cagefs_enter = '/bin/cagefs_enter';
	}

	public function getId(){
		return 'cagefs';
	}

	public function isAvailable(){
		if($this->available !== null){
			return $this->available;
		}

		$cache_key = 'cagefs';
		if(isset(self::$static_cache[$cache_key])){
			$this->available = self::$static_cache[$cache_key];
			return $this->available;
		}

		$this->available = false;

		if(!file_exists($this->cagefs_enter) || !is_executable($this->cagefs_enter)){
			self::$static_cache[$cache_key] = false;
			return false;
		}

		if(!$this->isCagefsActive()){
			self::$static_cache[$cache_key] = false;
			return false;
		}

		$this->available = true;
		self::$static_cache[$cache_key] = true;
		return true;
	}

	private function isCagefsActive(){
		$mountinfo = @file_get_contents('/proc/self/mountinfo');
		if($mountinfo === false){
			return false;
		}
		return strpos($mountinfo, 'cagefs') !== false;
	}

	public function wrapCommand($command){
		return escapeshellarg($this->cagefs_enter) . ' /bin/bash -c ' . escapeshellarg($command);
	}

	public function wrapPhpCommand($php_cmd){
		return escapeshellarg($this->cagefs_enter) . ' /bin/bash -c ' . escapeshellarg($php_cmd);
	}

	public function isConfined(){
		return true;
	}

}

class AIJailIsolation implements AIIsolationProvider {
	private $jail_script;
	private $user_home_dir;
	private $available = null;

	public function __construct($jail_script, $user_home_dir){
		$this->jail_script = $jail_script;
		$this->user_home_dir = rtrim($user_home_dir, '/');
	}

	public function getId(){
		return 'jail';
	}

	public function isAvailable(){
		if($this->available !== null){
			return $this->available;
		}
		$this->available = false;

		if(!file_exists($this->jail_script) || !is_executable($this->jail_script)){
			return false;
		}

		$test_cmd = sprintf('%s %s -- echo JAIL_TEST_OK 2>/dev/null',
			escapeshellarg($this->jail_script),
			escapeshellarg($this->user_home_dir)
		);

		$output = shell_exec($test_cmd);
		if(strpos($output, 'JAIL_TEST_OK') !== false){
			$this->available = true;
		}
		return $this->available;
	}

	public function wrapCommand($command){
		return sprintf('%s %s -- bash -c %s',
			escapeshellarg($this->jail_script),
			escapeshellarg($this->user_home_dir),
			escapeshellarg('cd ' . escapeshellarg($this->user_home_dir) . ' && ' . $command)
		);
	}

	public function wrapPhpCommand($php_cmd){
		return sprintf('%s %s -- %s',
			escapeshellarg($this->jail_script),
			escapeshellarg($this->user_home_dir),
			$php_cmd
		);
	}

	public function isConfined(){
		return true;
	}
}

class AINoIsolation implements AIIsolationProvider {
	public function isAvailable(){
		return true;
	}
	public function getId(){
		return 'none';
	}
	public function wrapCommand($command){
		return $command;
	}
	public function wrapPhpCommand($php_cmd){
		return $php_cmd;
	}
	public function isConfined(){
		return false;
	}
}

class ToolExecutor {
	private $file_manager;
	private $project_path;
	private $user_home_dir;
	private $mode = 'build';
	private $abort_file = '';
	private $isolation;
	private $process_user;
	private $permissions = array();
	private $username = '';

	public function __construct(AIFileManager $file_manager, $project_path, $user_home_dir = null, $mode = 'build', $username = ''){
		global $globals;

		$this->file_manager = $file_manager;
		$this->project_path = rtrim($project_path, '/');
		$this->user_home_dir = $user_home_dir ? rtrim($user_home_dir, '/') : $this->project_path;
		$this->mode = $mode;
		$this->username = (string)$username;
		$this->process_user = $this->resolveProcessUser();

		$providers = $this->buildIsolationProviders($globals);
		foreach($providers as $provider){
			if($provider->isAvailable()){
				$this->isolation = $provider;
				break;
			}
		}
		if(empty($this->isolation)){
			$this->isolation = new AINoIsolation();
		}

		# $this->debug_log_isolation();
	}

	public function set_permissions(array $permissions){
		$this->permissions = $permissions;
	}

	/**
	 * Detects if $path refers to a project context file (.softaculous-ai/project-bootstrap.md,
	 * project-state.md, project-memory.md) OR a simple context filename (project-bootstrap.md,
	 * project-state.md, project-memory.md). Returns the canonical context filename
	 * (e.g. "project-bootstrap.md") or '' if not a context path.
	 */
	private function is_context_file($path){
		$path = (string)$path;
		$ctx_files = array('project-bootstrap.md', 'project-state.md', 'project-memory.md');
		// Normalise: strip leading ./
		$norm = $path;
		if(substr($norm, 0, 2) === './'){
			$norm = substr($norm, 2);
		}
		// Strip absolute project_path prefix if present
		$pp_full = $this->project_path . '/.softaculous-ai/';
		if(strpos($path, $pp_full) === 0){
			$norm = '.softaculous-ai/' . substr($path, strlen($pp_full));
		}
		if(strpos($norm, '.softaculous-ai/') === 0){
			$name = substr($norm, strlen('.softaculous-ai/'));
			$name = trim($name, '/');
			if(in_array($name, $ctx_files, true)){
				return $name;
			}
		}
		// Also accept bare context filenames (e.g. "project-bootstrap.md")
		if(in_array($norm, $ctx_files, true)){
			return $norm;
		}
		return '';
	}

	/**
	 * Reads a context file from the persistent per-user context directory.
	 * Returns the content or '' if not found.
	 */
	private function read_context_file($name){
		if(empty($name) || !function_exists('ai_php_load_ctx_file')) return '';
		return ai_php_load_ctx_file($this->project_path, $name, $this->username);
	}

	/**
	 * Writes a context file directly to the persistent per-user context directory
	 * (~/.softaculous/ai/context/{project_id}/). The project root is never touched
	 * so no .softaculous-ai/ folder is created inside the installation.
	 */
	private function write_context_file($name, $content){
		if(empty($name) || !function_exists('ai_php_save_ctx_file')) return false;
		return ai_php_save_ctx_file($this->project_path, $name, (string)$content, $this->username);
	}

	private function resolveProcessUser(){
		if(function_exists('posix_getpwuid') && function_exists('posix_getuid')){
			$pw = posix_getpwuid(posix_getuid());
			if(is_array($pw) && !empty($pw['name'])){
				return $pw['name'];
			}
		}
		return '';
	}

	private function debug_log_isolation(){
		$log_dir = $this->user_home_dir . '/.softaculous/ai';
		if(!is_dir($log_dir)){
			@mkdir($log_dir, 0711, true);
		}
		@chmod($log_dir, 0711);
		$log_file = $log_dir . '/isolation_debug.log';
		$timestamp = date('Y-m-d H:i:s');
		$iso_id = $this->isolation->getId();
		$confined = $this->isolation->isConfined() ? 'yes' : 'no';
		$username = $this->process_user;

		$details = '';
		if($iso_id === 'cagefs'){
			$details = 'cagefs_enter=' . (file_exists('/bin/cagefs_enter') && is_executable('/bin/cagefs_enter') ? 'exists' : 'missing');
			$mountinfo = @file_get_contents('/proc/self/mountinfo');
			$details .= ' cagefs_mount=' . (strpos($mountinfo, 'cagefs') !== false ? 'yes' : 'no');
		}elseif($iso_id === 'jail'){
			$jail = '';
			global $globals;
			if(!empty($globals['path'])) $jail = $globals['path'].'/bin/ai_jail.sh';
			$details = 'jail_script=' . ($jail && file_exists($jail) && is_executable($jail) ? 'exists' : 'missing');
		}else{
			$details = 'cagefs_enter=' . (file_exists('/bin/cagefs_enter') ? 'exists' : 'missing');
			$mountinfo = @file_get_contents('/proc/self/mountinfo');
			$details .= ' cagefs_mount=' . (strpos($mountinfo, 'cagefs') !== false ? 'yes' : 'no');
			$jail = '';
			if(!empty($globals['path'])) $jail = $globals['path'].'/bin/ai_jail.sh';
			$details .= ' jail_script=' . ($jail && file_exists($jail) ? 'exists' : 'missing');
			$details .= ' max_user_namespaces=' . @file_get_contents('/proc/sys/user/max_user_namespaces');
		}

		$line = "[{$timestamp}] isolation={$iso_id} confined={$confined} user={$username} project={$this->project_path} {$details}\n";
		@file_put_contents($log_file, $line, FILE_APPEND | LOCK_EX);
		@chmod($log_file, 0600);
	}

	private function buildIsolationProviders($globals){
		$providers = array();

		$providers[] = new AICageFsIsolation();

		$jail_script = '';
		if(!empty($globals['path'])){
			$jail_script = $globals['path'].'/bin/ai_jail.sh';
		}
		if(!empty($jail_script)){
			$providers[] = new AIJailIsolation($jail_script, $this->user_home_dir);
		}

		return $providers;
	}

	public function set_abort_file($filepath){
		$this->abort_file = $filepath;
	}

	private function should_abort(){
		if(empty($this->abort_file)) return false;
		if(file_exists($this->abort_file)) return true;
		if(function_exists('connection_aborted') && connection_aborted()) return true;
		return false;
	}

	public function execute($tool_name, array $params){
		if($this->should_abort()){
			return array('output' => __('Aborted by user'), 'is_error' => true);
		}
		$read_only_tools = array('read_file', 'glob', 'grep', 'list_directory', 'web_fetch', 'todo_write', 'question', 'php_lint');
		if($this->mode === 'plan' && !in_array($tool_name, $read_only_tools)){
			return array('output' => __('Permission denied: write operations are not allowed in Plan mode.'), 'is_error' => true);
		}

		$write_tools = array('write_file', 'edit_file', 'bash', 'file_download', 'archive', 'search_replace', 'apply_patch');
		if(in_array($tool_name, $write_tools)){
			$perm_key = $tool_name;
			if(isset($this->permissions[$perm_key])){
				$perm_val = $this->permissions[$perm_key];
				if($perm_val === 'deny'){
					return array('output' => __('Permission denied: $0 is not allowed. You can change this in Settings > Permissions.', array($tool_name)), 'is_error' => true, '_permission_deny' => true);
				}
			}
		}
	$read_only_tools = array('read_file', 'glob', 'grep', 'list_directory', 'web_fetch', 'todo_write', 'question', 'php_lint');
	if($this->mode === 'plan' && !in_array($tool_name, $read_only_tools)){
		return array('output' => __('Permission denied: write operations are not allowed in Plan mode.'), 'is_error' => true);
	}

	$write_tools = array('write_file', 'edit_file', 'bash', 'file_download', 'archive', 'search_replace', 'apply_patch');
	if(in_array($tool_name, $write_tools)){
		$perm_key = $tool_name;
		if(isset($this->permissions[$perm_key])){
			$perm_val = $this->permissions[$perm_key];
			if($perm_val === 'deny'){
				return array('output' => __('Permission denied: $0 is not allowed. You can change this in Settings > Permissions.', array($tool_name)), 'is_error' => true, '_permission_deny' => true);
			}
			if($perm_val === 'ask'){
				return array('output' => __('Permission required for $0', array($tool_name)), 'is_error' => false, '_permission_ask' => true);
			}
		}
	}

		switch($tool_name){
			case 'read_file': return $this->tool_read_file($params);
			case 'write_file': return $this->tool_write_file($params);
			case 'edit_file': return $this->tool_edit_file($params);
			case 'bash': return $this->tool_bash($params);
			case 'glob': return $this->tool_glob($params);
			case 'grep': return $this->tool_grep($params);
			case 'list_directory': return $this->tool_list_directory($params);
			case 'web_fetch': return $this->tool_web_fetch($params);
			case 'todo_write': return $this->tool_todo_write($params);
			case 'file_download': return $this->tool_file_download($params);
			case 'archive': return $this->tool_archive($params);
			case 'search_replace': return $this->tool_search_replace($params);
			case 'php_eval': return $this->tool_php_eval($params);
			case 'php_lint': return $this->tool_php_lint($params);
			case 'apply_patch': return $this->tool_apply_patch($params);
			case 'question': return $this->tool_question($params);
			default: return array('output' => __('Unknown tool: $0', array($tool_name)), 'is_error' => true);
		}
	}

	private function tool_read_file(array $params){
		$path = isset($params['path']) ? $params['path'] : '';
		$offset = isset($params['offset']) ? intval($params['offset']) : 1;
		$limit = isset($params['limit']) ? intval($params['limit']) : 2000;
		
		// We do not allow it to read the settings file.
		if(strpos($path, 'ai/settings.json.php') != 0){
			return array('output' => __('Access denied'), 'is_error' => true);
		}

		// For context files, read from the persistent context dir
		$ctx_name = $this->is_context_file($path);
		if($ctx_name){
			$content = $this->read_context_file($ctx_name);
			if($content === ''){
				return array('output' => __('File not found'), 'is_error' => true);
			}
		}else{
			$result = $this->file_manager->read_file($path);
			if(!empty($result['error'])){
				return array('output' => $result['error'], 'is_error' => true);
			}
			$content = $result['content'];
		}

		$lines = explode("\n", $content);

		if($offset > 1 || $limit < count($lines)){
			$lines = array_slice($lines, max(0, $offset - 1), $limit);
		}

		$output = '';
		$line_no = max(1, $offset);
		foreach($lines as $line){
			$output .= str_pad($line_no, 5, ' ', STR_PAD_LEFT) . ' | ' . $line . "\n";
			$line_no++;
		}

		return array('output' => rtrim($output), 'is_error' => false);
	}

	private function tool_write_file(array $params){
		$path = isset($params['path']) ? $params['path'] : '';
		$content = isset($params['content']) ? $params['content'] : '';

		if(empty($path)){
			return array('output' => __('Path is required'), 'is_error' => true);
		}

		// If this is a context file (.softaculous-ai/project-*.md), write it
		// directly to the persistent per-user context directory without ever
		// touching the project root (no .softaculous-ai/ folder is created).
		$ctx_name = $this->is_context_file($path);
		$original = '';
		if($ctx_name){
			$original = $this->read_context_file($ctx_name);
			if(!$this->write_context_file($ctx_name, $content)){
				return array('output' => __('Failed to save context file'), 'is_error' => true);
			}
		}else{
			$existing = $this->file_manager->read_file($path);
			if(!empty($existing['content'])) $original = $existing['content'];

			$result = $this->file_manager->write_file($path, $content, false);
			if(!empty($result['error'])){
				return array('output' => $result['error'], 'is_error' => true);
			}
		}

		$lines = substr_count($content, "\n") + 1;
		$display_path = $ctx_name ? $ctx_name : $path;
		$return = array('output' => __('Successfully wrote $0 bytes ($1 lines) to $2', array(strlen($content), $lines, $display_path)), 'is_error' => false);
		if($original !== '' && $original !== $content){
			$return['diff'] = $this->compute_unified_diff($original, $content);
		}
		return $return;
	}

	private function tool_edit_file(array $params){
		$path = isset($params['path']) ? $params['path'] : '';
		$old_string = isset($params['old_string']) ? $params['old_string'] : '';
		$new_string = isset($params['new_string']) ? $params['new_string'] : '';
		$replace_all = !empty($params['replace_all']);

		if(empty($path) || empty($old_string)){
			return array('output' => __('Path and old_string are required'), 'is_error' => true);
		}

		// For context files, read from the persistent context dir
		$ctx_name = $this->is_context_file($path);
		if($ctx_name){
			$content = $this->read_context_file($ctx_name);
		}else{
			$result = $this->file_manager->read_file($path);
			if(!empty($result['error'])){
				return array('output' => $result['error'], 'is_error' => true);
			}
			$content = $result['content'];
		}

		if(strpos($content, $old_string) === false){
			$fuzzy_result = $this->fuzzy_find($content, $old_string);
			if($fuzzy_result !== false){
				$old_string = $fuzzy_result;
			}else{
				$snippet = mb_substr($old_string, 0, 100);
				return array('output' => __('Could not find the specified text in $0. Searched for: "$1..."', array($path, $snippet)), 'is_error' => true);
			}
		}

		$count = 0;
		if($replace_all){
			$new_content = str_replace($old_string, $new_string, $content, $count);
		}else{
			$pos = strpos($content, $old_string);
			if($pos !== false){
				$new_content = substr($content, 0, $pos) . $new_string . substr($content, $pos + strlen($old_string));
				$count = 1;
			}else{
				$new_content = $content;
			}
		}

		if($count === 0){
			return array('output' => __('No replacements made in $0', array($path)), 'is_error' => true);
		}

		// Context files live in the persistent per-user context directory, so
		// persist the edit there instead of the project root.
		if($ctx_name){
			if(!$this->write_context_file($ctx_name, $new_content)){
				return array('output' => __('Failed to save context file'), 'is_error' => true);
			}
		}else{
			$result2 = $this->file_manager->write_file($path, $new_content, false);
			if(!empty($result2['error'])){
				return array('output' => $result2['error'], 'is_error' => true);
			}
		}

		$diff = $this->make_mini_diff($old_string, $new_string);
		$display_path = $ctx_name ? $ctx_name : $path;
		$return = array('output' => __('Edited $0 ($1 replacement(s))', array($display_path, $count))."\n{$diff}", 'is_error' => false);
		$return['diff'] = $this->compute_unified_diff($content, $new_content);
		return $return;
	}

	private function tool_bash(array $params){
		$command = isset($params['command']) ? $params['command'] : '';
		$timeout = isset($params['timeout']) ? intval($params['timeout']) : 30;
		$timeout = max(5, min($timeout, 120));

		if(empty($command)){
			return array('output' => __('Command is required'), 'is_error' => true);
		}

		// Respect the OS / control-panel login-shell policy. If the account
		// does not have shell access (nologin / noshell / false), the bash
		// tool is disabled too — otherwise it would bypass the account's
		// shell restriction, which is exactly the policy the AI shell feature
		// must not undermine.
		if(!empty($this->username) && function_exists('ai_user_has_shell_access') && !ai_user_has_shell_access($this->username)){
			return array('output' => __('Command execution is disabled because shell access is not enabled for this account.'), 'is_error' => true);
		}

		if($this->is_dangerous_command($command)){
			return array('output' => __('This command is blocked for safety reasons.'), 'is_error' => true);
		}

		if(!$this->isolation->isConfined()){
			return array('output' => __('Command execution is disabled: no isolation mechanism available.'), 'is_error' => true);
		}

		$env = array();
		if(!empty($this->user_home_dir)){
			$env['HOME'] = $this->user_home_dir;
		}
		$env['PATH'] = '/usr/local/bin:/usr/bin:/bin';
		$env['TERM'] = 'dumb';

		$wrapped = $this->isolation->wrapCommand('cd ' . escapeshellarg($this->project_path) . ' && ' . $command);

		return $this->run_process($wrapped, $timeout, $env);
	}

	private function tool_glob(array $params){
		$pattern = isset($params['pattern']) ? $params['pattern'] : '**/*';
		$base_path = isset($params['path']) ? $params['path'] : '';

		if(!$this->isolation->isConfined()){
			return array('output' => __('File search is disabled: no isolation mechanism available.'), 'is_error' => true);
		}

		$full_base = $this->project_path;
		if(!empty($base_path)){
			$resolved = $this->file_manager->resolve_path($base_path);
			if(strpos($resolved, $this->user_home_dir) === 0){
				$full_base = $resolved;
			}
		}

		$pattern = escapeshellarg($pattern);
		$cmd = 'cd ' . escapeshellarg($full_base) . ' && find . -path ' . $pattern . ' -type f 2>/dev/null | head -200';

		$cmd = $this->isolation->wrapCommand($cmd);

		$output = trim(shell_exec($cmd));

		if(empty($output)){
			$_pattern = isset($params['pattern']) ? $params['pattern'] : $pattern;
			return array('output' => __('No files found matching pattern: $0', array($_pattern)), 'is_error' => false);
		}

		$files = explode("\n", $output);
		$files = array_map(function($f){ return ltrim($f, './'); }, $files);

		return array('output' => implode("\n", $files), 'is_error' => false);
	}

	private function tool_grep(array $params){
		$pattern = isset($params['pattern']) ? $params['pattern'] : '';
		$base_path = isset($params['path']) ? $params['path'] : '.';
		$include = isset($params['include']) ? $params['include'] : '';
		$max_results = isset($params['max_results']) ? intval($params['max_results']) : 50;

		if(empty($pattern)){
			return array('output' => __('Pattern is required'), 'is_error' => true);
		}

		if(!$this->isolation->isConfined()){
			return array('output' => __('File search is disabled: no isolation mechanism available.'), 'is_error' => true);
		}

		$full_base = $this->file_manager->resolve_path($base_path);
		if(strpos($full_base, $this->user_home_dir) !== 0){
			return array('output' => __('Path outside allowed directory'), 'is_error' => true);
		}

		$cmd = 'cd ' . escapeshellarg($this->project_path) . ' && grep -rn --binary-files=without-match';
		if(!empty($include)){
			$cmd .= ' --include=' . escapeshellarg($include);
		}
		$cmd .= ' ' . escapeshellarg($pattern) . ' ' . escapeshellarg($full_base) . ' 2>/dev/null | head -' . $max_results;

		$cmd = $this->isolation->wrapCommand($cmd);

		$output = trim(shell_exec($cmd));

		if(empty($output)){
			return array('output' => __('No matches found for pattern: $0', array($pattern)), 'is_error' => false);
		}

		$output = preg_replace('/^' . preg_quote($this->project_path, '/') . '\//', '', $output);
		return array('output' => $output, 'is_error' => false);
	}

	private function tool_list_directory(array $params){
		$path = isset($params['path']) ? $params['path'] : '/';
		$depth = isset($params['depth']) ? intval($params['depth']) : 2;

		$result = $this->file_manager->list_directory($path, $depth);
		if(!empty($result['error'])){
			return array('output' => $result['error'], 'is_error' => true);
		}

		return array('output' => json_encode($result, JSON_PRETTY_PRINT), 'is_error' => false);
	}

	private function tool_web_fetch(array $params){
		$url = isset($params['url']) ? $params['url'] : '';
		if(empty($url)){
			return array('output' => __('URL is required'), 'is_error' => true);
		}

		if(!filter_var($url, FILTER_VALIDATE_URL)){
			return array('output' => __('Invalid URL'), 'is_error' => true);
		}

		$ch = curl_init($url);
		curl_setopt_array($ch, array(
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_FOLLOWLOCATION => true,
			CURLOPT_TIMEOUT => 15,
			CURLOPT_SSL_VERIFYPEER => false,
			CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; Softaculous AI/1.0)'
		));
		$response = curl_exec($ch);
		$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		curl_close($ch);

		if($response === false){
			return array('output' => __('Failed to fetch URL'), 'is_error' => true);
		}

		$response = strip_tags($response);
		$response = preg_replace('/\s+/', ' ', $response);
		$response = trim($response);

		if(strlen($response) > 30000){
			$response = substr($response, 0, 30000) . '... [truncated]';
		}

		return array('output' => $response, 'is_error' => false);
	}

	private function tool_todo_write(array $params){
		$todos = isset($params['todos']) ? $params['todos'] : array();
		return array('output' => __('Todo list updated with $0 items.', array(count($todos))), 'is_error' => false, 'todos' => $todos);
	}

	private function fuzzy_find($content, $search){
		$search_trimmed = trim($search);
		$search_lines = explode("\n", $search_trimmed);

		if(count($search_lines) < 2) return null;

		$max_file_lines = 5000;
		$content_lines = explode("\n", $content);
		if(count($content_lines) > $max_file_lines){
			$content_lines = array_slice($content_lines, 0, $max_file_lines);
		}

		$first_line = trim($search_lines[0]);
		$last_line = trim($search_lines[count($search_lines) - 1]);

		$first_positions = array();
		foreach($content_lines as $i => $line){
			if(trim($line) === $first_line) $first_positions[] = $i;
		}

		foreach($first_positions as $start){
			$end = $start + count($search_lines) - 1;
			if($end >= count($content_lines)) continue;
			if(trim($content_lines[$end]) === $last_line){
				$match = implode("\n", array_slice($content_lines, $start, count($search_lines)));
				return $match;
			}
		}

		$max_similar_checks = 50;
		$checks = 0;
		foreach($content_lines as $i => $line){
			if($checks >= $max_similar_checks) break;
			if(strpos(trim($line), $first_line) !== false || strpos($first_line, trim($line)) !== false){
				$end = min($i + count($search_lines), count($content_lines));
				$candidate = implode("\n", array_slice($content_lines, $i, $end - $i));
				if(strlen($candidate) > 100000) continue;
				similar_text($search_trimmed, $candidate, $percent);
				$checks++;
				if($percent > 70) return $candidate;
			}
		}

		return null;
	}

	private function make_mini_diff($old, $new){
		$diff = "";
		$old_lines = explode("\n", $old);
		$new_lines = explode("\n", $new);
		foreach($old_lines as $l) $diff .= "- " . $l . "\n";
		foreach($new_lines as $l) $diff .= "+ " . $l . "\n";
		return trim($diff);
	}

	private function compute_unified_diff($old, $new){
		$old_lines = explode("\n", $old);
		$new_lines = explode("\n", $new);
		$max = max(count($old_lines), count($new_lines));
		$diff = array();
		$ctx = 3;
		$buffer = array();
		$in_change = false;

		for($i = 0; $i < $max || !empty($buffer); $i++){
			$o = isset($old_lines[$i]) ? $old_lines[$i] : null;
			$n = isset($new_lines[$i]) ? $new_lines[$i] : null;

			if($o !== null && $n !== null && $o === $n){
				if($in_change){
					$buffer[] = ' ' . $o;
					if(count($buffer) > $ctx * 2 + 10){
						$diff = array_merge($diff, array_slice($buffer, 0, count($buffer) - $ctx));
						$buffer = array_slice($buffer, -$ctx);
						$in_change = false;
					}
				}else{
					$buffer[] = ' ' . $o;
					if(count($buffer) > $ctx){
						$buffer = array_slice($buffer, -$ctx);
					}
				}
			}else{
				if(!$in_change && !empty($buffer)){
					$keep = min(count($buffer), $ctx);
					$diff = array_merge($diff, array_slice($buffer, -$keep));
					$buffer = array();
				}
				$in_change = true;
				if($o !== null) $diff[] = '- ' . $o;
				if($n !== null) $diff[] = '+ ' . $n;
			}
		}

		if(!empty($buffer)){
			$keep = min(count($buffer), $ctx);
			$diff = array_merge($diff, array_slice($buffer, -$keep));
		}

		return implode("\n", $diff);
	}

	private function tool_file_download(array $params){
		$url = isset($params['url']) ? $params['url'] : '';
		$path = isset($params['path']) ? $params['path'] : '';
		$overwrite = !empty($params['overwrite']);

		if(empty($url) || empty($path)){
			return array('output' => __('URL and path are required'), 'is_error' => true);
		}

		if(!filter_var($url, FILTER_VALIDATE_URL)){
			return array('output' => __('Invalid URL: $0', array($url)), 'is_error' => true);
		}

		$scheme = parse_url($url, PHP_URL_SCHEME);
		if(!in_array($scheme, array('http', 'https'))){
			return array('output' => __('Only http/https URLs are allowed'), 'is_error' => true);
		}

		$resolved = $this->file_manager->resolve_path($path);
		if(strpos($resolved, $this->project_path) !== 0){
			return array('output' => __('Destination path is outside the project directory'), 'is_error' => true);
		}

		if(file_exists($resolved) && !$overwrite){
			return array('output' => __('File already exists: $0. Set overwrite=true to replace.', array($path)), 'is_error' => true);
		}

		$dir = dirname($resolved);
		if(!is_dir($dir)){
			@mkdir($dir, 0755, true);
		}

		$fp = @fopen($resolved, 'w');
		if(!$fp){
			return array('output' => __('Cannot write to: $0', array($path)), 'is_error' => true);
		}

		$ch = curl_init($url);
		curl_setopt_array($ch, array(
			CURLOPT_FILE => $fp,
			CURLOPT_FOLLOWLOCATION => true,
			CURLOPT_TIMEOUT => 120,
			CURLOPT_SSL_VERIFYPEER => false,
			CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; Softaculous AI/1.0)',
			CURLOPT_MAXFILESIZE => 52428800
		));

		$result = curl_exec($ch);
		$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		$size = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD);
		$error = curl_error($ch);
		curl_close($ch);
		fclose($fp);

		if(!$result){
			@unlink($resolved);
			return array('output' => __('Download failed: $0', array($error ?: __('Unknown error'))), 'is_error' => true);
		}

		if($http_code >= 400){
			@unlink($resolved);
			return array('output' => __('Download failed: HTTP $0', array($http_code)), 'is_error' => true);
		}

		$size_str = $this->format_bytes($size);
		return array('output' => __('Downloaded $0 to $1 (HTTP $2)', array($size_str, $path, $http_code)), 'is_error' => false);
	}

	private function tool_archive(array $params){
		$action = isset($params['action']) ? $params['action'] : '';
		$source = isset($params['source']) ? $params['source'] : '';
		$destination = isset($params['destination']) ? $params['destination'] : '';

		if(empty($action) || empty($source)){
			return array('output' => __('action and source are required'), 'is_error' => true);
		}

		if(!in_array($action, array('zip', 'unzip'))){
			return array('output' => __('Action must be "zip" or "unzip"'), 'is_error' => true);
		}

		if($action === 'zip'){
			return $this->archive_zip($source, $destination);
		}else{
			return $this->archive_unzip($source, $destination);
		}
	}

	private function archive_zip($source, $destination){
		$src_resolved = $this->file_manager->resolve_path($source);
		if(strpos($src_resolved, $this->user_home_dir) !== 0){
			return array('output' => __('Source path is outside the allowed directory'), 'is_error' => true);
		}
		if(!file_exists($src_resolved)){
			return array('output' => __('Source not found: $0', array($source)), 'is_error' => true);
		}

		if(empty($destination)){
			$destination = $source . '.zip';
		}
		if(substr($destination, -4) !== '.zip') $destination .= '.zip';

		$dst_resolved = $this->file_manager->resolve_path($destination);
		if(strpos($dst_resolved, $this->user_home_dir) !== 0){
			return array('output' => __('Destination path is outside the allowed directory'), 'is_error' => true);
		}

		$zip = new ZipArchive();
		if($zip->open($dst_resolved, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true){
			return array('output' => __('Failed to create zip archive'), 'is_error' => true);
		}

		$count = 0;
		if(is_dir($src_resolved)){
			$base = basename($src_resolved);
			$files = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator($src_resolved, RecursiveDirectoryIterator::SKIP_DOTS),
				RecursiveIteratorIterator::SELF_FIRST
			);
			foreach($files as $file){
				$local = $base . '/' . substr($file->getPathname(), strlen($src_resolved) + 1);
				if($file->isDir()){
					$zip->addEmptyDir($local);
				}else{
					if($file->getSize() > 52428800){
						continue;
					}
					$zip->addFile($file->getPathname(), $local);
					$count++;
				}
			}
		}else{
			$zip->addFile($src_resolved, basename($src_resolved));
			$count = 1;
		}

		$zip->close();

		$zip_size = $this->format_bytes(filesize($dst_resolved));
		return array('output' => __('Created archive $0 ($1 files, $2)', array($destination, $count, $zip_size)), 'is_error' => false);
	}

	private function archive_unzip($source, $destination){
		$src_resolved = $this->file_manager->resolve_path($source);
		if(strpos($src_resolved, $this->user_home_dir) !== 0){
			return array('output' => __('Source path is outside the allowed directory'), 'is_error' => true);
		}
		if(!file_exists($src_resolved)){
			return array('output' => __('Source not found: $0', array($source)), 'is_error' => true);
		}

		if(empty($destination)){
			$destination = dirname($source) ?: '.';
		}
		$dst_resolved = $this->file_manager->resolve_path($destination);
		if(strpos($dst_resolved, $this->user_home_dir) !== 0){
			return array('output' => __('Destination path is outside the allowed directory'), 'is_error' => true);
		}

		$zip = new ZipArchive();
		if($zip->open($src_resolved) !== true){
			return array('output' => __('Failed to open zip archive'), 'is_error' => true);
		}

		$count = $zip->numFiles;
		$zip->extractTo($dst_resolved);
		$zip->close();

		return array('output' => __('Extracted $0 files to $1', array($count, $destination)), 'is_error' => false);
	}

	private function tool_search_replace(array $params){
		$search = isset($params['search']) ? $params['search'] : '';
		$replace = isset($params['replace']) ? $params['replace'] : '';
		$include = isset($params['include']) ? $params['include'] : '';
		$base_path = isset($params['path']) ? $params['path'] : '.';
		$max_files = isset($params['max_files']) ? intval($params['max_files']) : 50;
		$is_regex = !empty($params['regex']);

		$max_files = max(1, min($max_files, 100));

		if(empty($search)){
			return array('output' => __('search pattern is required'), 'is_error' => true);
		}

		if(!$this->isolation->isConfined()){
			return array('output' => __('Search and replace is disabled: no isolation mechanism available.'), 'is_error' => true);
		}

		$full_base = $this->file_manager->resolve_path($base_path);
		if(strpos($full_base, $this->user_home_dir) !== 0){
			return array('output' => __('Path outside allowed directory'), 'is_error' => true);
		}

		if($is_regex){
			set_error_handler(function(){});
			$test = preg_match($search, '');
			restore_error_handler();
			if($test === false){
				return array('output' => __('Invalid regex pattern: $0', array($search)), 'is_error' => true);
			}
		}

		$cmd = 'cd '.escapeshellarg($this->project_path).' && grep -rl --binary-files=without-match';
		if(!empty($include)){
			$cmd .= ' --include='.escapeshellarg($include);
		}
		$cmd .= ' '.escapeshellarg($search).' '.escapeshellarg($full_base).' 2>/dev/null | head -'.($max_files + 1);

		$cmd = $this->isolation->wrapCommand($cmd);

		$output = trim(shell_exec($cmd));
		if(empty($output)){
			return array('output' => __('No files found matching: $0', array($search)), 'is_error' => false);
		}

		$files = explode("\n", $output);
		$truncated = false;
		if(count($files) > $max_files){
			$files = array_slice($files, 0, $max_files);
			$truncated = true;
		}

		$results = array();
		$total_replacements = 0;

		foreach($files as $file){
			$filepath = trim($file);
			if(!file_exists($filepath) || !is_readable($filepath) || !is_writable($filepath)) continue;

			$rel_path = str_replace($this->project_path . '/', '', $filepath);
			$ctx_name = $this->is_context_file($rel_path);
			if($ctx_name){
				$content = $this->read_context_file($ctx_name);
			}else{
				$read_result = $this->file_manager->read_file($rel_path);
				if(!empty($read_result['error'])) continue;
				$content = $read_result['content'];
			}

			$count = 0;
			if($is_regex){
				$new_content = preg_replace($search, $replace, $content, -1, $count);
			}else{
				$new_content = str_replace($search, $replace, $content, $count);
			}

			if($count > 0 && $new_content !== null){
				if($ctx_name){
					$ok = $this->write_context_file($ctx_name, $new_content);
				}else{
					$write_result = $this->file_manager->write_file($rel_path, $new_content, false);
					$ok = empty($write_result['error']);
				}
				if($ok){
					$results[] = "{$rel_path}: {$count} replacement(s)";
					$total_replacements += $count;
				}
			}
		}

		$output = __('Replaced "$0" in $1 file(s) ($2 total replacements)', array($search, count($results), $total_replacements))."\n";
		$output .= implode("\n", $results);
		if($truncated){
			$output .= "\n".__('[Only processed first $0 files. Use max_files to increase.]', array($max_files));
		}

		return array('output' => $output, 'is_error' => false);
	}

	private function tool_php_eval(array $params){
		$code = isset($params['code']) ? $params['code'] : '';

		if(empty($code)){
			return array('output' => __('code is required'), 'is_error' => true);
		}

		$forbidden = array('exec(', 'shell_exec(', 'system(', 'passthru(', 'popen(', 'proc_open(',
			'pcntl_', 'putenv(', 'apache_', 'ini_set(', 'ini_restore(',
			'mail(', 'header(', 'setcookie(', 'move_uploaded_file(',
			'chmod(', 'chown(', 'chgrp(', 'unlink(', 'rmdir(',
			'file_put_contents(', 'fwrite(', 'fputs(', 'rename(',
			'eval(', 'assert(', 'preg_replace(', 'create_function(',
			'call_user_func(', 'call_user_func_array(');
		$code_lower = strtolower($code);
		foreach($forbidden as $f){
			if(strpos($code_lower, strtolower($f)) !== false){
				return array('output' => __('Function/statement not allowed: $0', array($f)), 'is_error' => true);
			}
		}

		if(!$this->isolation->isConfined()){
			return array('output' => __('PHP execution is disabled: no isolation mechanism available.'), 'is_error' => true);
		}

		$tmp = tempnam(sys_get_temp_dir(), 'ai_php_');
		$wrapped = "<?php\nchdir(".var_export($this->project_path, true).");\n";
		if(file_exists($this->project_path.'/wp-load.php')){
			$wrapped .= "define('ABSPATH', ".var_export($this->project_path.'/', true).");\n";
			$wrapped .= "@include_once(".var_export($this->project_path.'/wp-load.php', true).");\n";
		}
		$wrapped .= "\n".$code."\n";
		file_put_contents($tmp, $wrapped);

		$env = array();
		if(!empty($this->user_home_dir)){
			$env['HOME'] = $this->user_home_dir;
		}
		$env['PATH'] = '/usr/local/bin:/usr/bin:/bin';

		$open_basedir = $this->user_home_dir . ':' . sys_get_temp_dir();
		$disable_functions = 'exec,shell_exec,system,passthru,popen,proc_open,pcntl_fork,pcntl_exec,putenv,ini_set,ini_restore,dl';

		$php_cmd = sprintf('php -d open_basedir=%s -d disable_functions=%s %s',
			escapeshellarg($open_basedir),
			escapeshellarg($disable_functions),
			escapeshellarg($tmp)
		);

		$full_cmd = $this->isolation->wrapPhpCommand($php_cmd);

		$descriptors = array(
			0 => array('pipe', 'r'),
			1 => array('pipe', 'w'),
			2 => array('pipe', 'w')
		);

		$process = proc_open($full_cmd, $descriptors, $pipes, $this->project_path, $env);

		if(!is_resource($process)){
			@unlink($tmp);
			return array('output' => __('Failed to execute PHP'), 'is_error' => true);
		}

		fclose($pipes[0]);
		stream_set_blocking($pipes[1], false);
		stream_set_blocking($pipes[2], false);

		$stdout = '';
		$stderr = '';
		$start = microtime(true);
		$timeout = 10;

		while(true){
			if($this->should_abort()){
				proc_terminate($process, 9);
				fclose($pipes[1]);
				fclose($pipes[2]);
				proc_close($process);
				@unlink($tmp);
				return array('output' => __('PHP execution aborted by user'), 'is_error' => true);
			}
			$status = proc_get_status($process);
			if(!$status['running']){
				while(($buf = fread($pipes[1], 8192)) !== '') $stdout .= $buf;
				while(($buf = fread($pipes[2], 8192)) !== '') $stderr .= $buf;
				break;
			}
			if(microtime(true) - $start >= $timeout){
				proc_terminate($process, 9);
				fclose($pipes[1]);
				fclose($pipes[2]);
				proc_close($process);
				@unlink($tmp);
				return array('output' => __('PHP execution timed out after $0s', array($timeout)), 'is_error' => true);
			}
			$got_data = false;
			while(($buf = fread($pipes[1], 8192)) !== ''){
				$stdout .= $buf;
				$got_data = true;
			}
			while(($buf = fread($pipes[2], 8192)) !== ''){
				$stderr .= $buf;
				$got_data = true;
			}
			if($got_data) $last_activity = microtime(true);
			usleep(50000);
		}

		fclose($pipes[1]);
		fclose($pipes[2]);
		$return_code = proc_close($process);
		@unlink($tmp);

		$output = trim($stdout);
		if(!empty($stderr)){
			$output .= ($output ? "\n" : '') . trim($stderr);
		}

		if(strlen($output) > 20000){
			$output = substr($output, 0, 20000) . "\n... [output truncated]";
		}

		if($return_code !== 0){
			return array('output' => $output."\n".__('[Exit code: $0]', array($return_code)), 'is_error' => true);
		}

		return array('output' => $output ?: __('[PHP executed successfully with no output]'), 'is_error' => false);
	}

	/**
	 * Runs `php -l` (syntax-only check) on a PHP file in the project. The lint
	 * command never executes the file, so it is safe to run even when no shell
	 * isolation is available. Lets the AI verify PHP files before declaring a
	 * task complete.
	 */
	private function tool_php_lint(array $params){
		$path = isset($params['path']) ? $params['path'] : '';
		if(empty($path)){
			return array('output' => __('Path is required'), 'is_error' => true);
		}

		$resolved = $this->file_manager->resolve_path($path);
		if(empty($resolved) || strpos($resolved, $this->user_home_dir) !== 0){
			return array('output' => __('Path outside allowed directory'), 'is_error' => true);
		}
		if(!is_file($resolved)){
			return array('output' => __('File not found: $0', array($path)), 'is_error' => true);
		}
		if(substr($resolved, -4) !== '.php'){
			return array('output' => __('Not a PHP file: $0', array($path)), 'is_error' => true);
		}

		if(!$this->isolation->isConfined()){
			return array('output' => __('PHP lint is disabled: no isolation mechanism available.'), 'is_error' => true);
		}

		$cmd = 'php -l ' . escapeshellarg($resolved) . ' 2>&1';
		// Route through the isolation layer (jail/cagefs) for consistency,
		// even though php -l is syntax-only and does not execute the file.
		$cmd = $this->isolation->wrapCommand($cmd);
		$output = trim((string)@shell_exec($cmd));
		if(empty($output)){
			return array('output' => __('Failed to run the PHP linter (php CLI not available?)'), 'is_error' => true);
		}

		$ok = (strpos($output, 'No syntax errors detected') !== false);
		return array('output' => $output, 'is_error' => !$ok);
	}

	private function run_process($cmd, $timeout, $env){
		$descriptors = array(
			0 => array('pipe', 'r'),
			1 => array('pipe', 'w'),
			2 => array('pipe', 'w')
		);

		$process = proc_open($cmd, $descriptors, $pipes, null, $env);

		if(!is_resource($process)){
			return array('output' => __('Failed to execute command'), 'is_error' => true);
		}

		fclose($pipes[0]);
		stream_set_blocking($pipes[1], false);
		stream_set_blocking($pipes[2], false);

		$stdout = '';
		$stderr = '';
		$start = microtime(true);
		$last_activity = $start;
		$max_idle_time = 120;

		while(true){
			if($this->should_abort()){
				proc_terminate($process, 9);
				fclose($pipes[1]);
				fclose($pipes[2]);
				proc_close($process);
				return array('output' => __('Command aborted by user'), 'is_error' => true);
			}

			$status = proc_get_status($process);
			if(!$status['running']){
				while(($buf = fread($pipes[1], 8192)) !== '') $stdout .= $buf;
				while(($buf = fread($pipes[2], 8192)) !== '') $stderr .= $buf;
				break;
			}

			$elapsed = microtime(true) - $start;
			if($elapsed >= $timeout){
				proc_terminate($process, 9);
				fclose($pipes[1]);
				fclose($pipes[2]);
				proc_close($process);
				$output = $stdout . ($stdout && $stderr ? "\n" : '') . $stderr;
				return array('output' => __('Command timed out after $0s', array($timeout))."\n{$output}", 'is_error' => true);
			}

			$got_data = false;
			while(($buf = fread($pipes[1], 8192)) !== ''){
				$stdout .= $buf;
				$got_data = true;
			}
			while(($buf = fread($pipes[2], 8192)) !== ''){
				$stderr .= $buf;
				$got_data = true;
			}
			if($got_data){
				$last_activity = microtime(true);
			}elseif((microtime(true) - $last_activity) > $max_idle_time){
				proc_terminate($process, 9);
				fclose($pipes[1]);
				fclose($pipes[2]);
				proc_close($process);
				$output = $stdout . ($stdout && $stderr ? "\n" : '') . $stderr;
				return array('output' => __('Command idle timeout after $0s with no output', array($max_idle_time))."\n{$output}", 'is_error' => true);
			}
			usleep(50000);
		}

		fclose($pipes[1]);
		fclose($pipes[2]);
		$return_code = proc_close($process);

		$output = '';
		if(!empty($stdout)) $output .= $stdout;
		if(!empty($stderr)) $output .= ($output ? "\n" : '') . $stderr;

		if(strlen($output) > 30000){
			$output = mb_substr($output, 0, 30000) . "\n... [output truncated]";
		}

		if($return_code !== 0){
			$output .= "\n".__('[Exit code: $0]', array($return_code));
			return array('output' => $output, 'is_error' => true);
		}

		return array('output' => $output ? $output : __('[Command completed successfully with no output]'), 'is_error' => false);
	}

	private function tool_apply_patch(array $params){
		$path = isset($params['path']) ? $params['path'] : '';
		$patch = isset($params['patch']) ? $params['patch'] : '';

		if(empty($path)){
			return array('output' => __('Path is required'), 'is_error' => true);
		}
		if(empty($patch)){
			return array('output' => __('Patch content is required'), 'is_error' => true);
		}

		// For context files, read from the persistent context dir
		$ctx_name = $this->is_context_file($path);
		if($ctx_name){
			$original = $this->read_context_file($ctx_name);
			if($original === ''){
				return array('output' => __('File not found'), 'is_error' => true);
			}
		}else{
			$result = $this->file_manager->read_file($path);
			if(!empty($result['error'])){
				return array('output' => $result['error'], 'is_error' => true);
			}
			$original = $result['content'];
		}
		$patched = $this->apply_unified_patch($original, $patch);
		if($patched === false){
			return array('output' => __('Failed to apply patch. The patch may not match the file content.'), 'is_error' => true);
		}

		// Context files live in the persistent per-user context directory, so
		// persist the patch there instead of the project root.
		if($ctx_name){
			if(!$this->write_context_file($ctx_name, $patched)){
				return array('output' => __('Failed to save context file'), 'is_error' => true);
			}
		}else{
			$write_result = $this->file_manager->write_file($path, $patched, false);
			if(!empty($write_result['error'])){
				return array('output' => $write_result['error'], 'is_error' => true);
			}
		}

		$lines = substr_count($patched, "\n") + 1;
		$display_path = $ctx_name ? $ctx_name : $path;
		$return = array('output' => __('Successfully applied patch to $0 ($1 lines)', array($display_path, $lines)), 'is_error' => false);
		if($original !== $patched){
			$return['diff'] = $this->compute_unified_diff($original, $patched);
		}
		return $return;
	}

	private function apply_unified_patch($content, $patch){
		$lines = explode("\n", $content);
		$patch_lines = explode("\n", $patch);
		$result = $lines;
		$offset = 0;
		$i = 0;
		$patch_lines_count = count($patch_lines);

		while($i < $patch_lines_count){
			$line = $patch_lines[$i];
			if(preg_match('/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/', $line, $m)){
				$old_start = intval($m[1]);
				$old_count = isset($m[2]) ? intval($m[2]) : 1;
				$new_start = intval($m[3]);
				$new_count = isset($m[4]) ? intval($m[4]) : 1;
				$i++;
				$old_lines = array();
				$new_lines = array();
				while($i < $patch_lines_count){
					$pl = $patch_lines[$i];
					if(strlen($pl) === 0 && $i === $patch_lines_count - 1) break;
					if(preg_match('/^@@\s+/', $pl)) break;
					$first_char = strlen($pl) > 0 ? $pl[0] : ' ';
					$rest = strlen($pl) > 0 ? substr($pl, 1) : '';
					if($first_char === '-'){
						$old_lines[] = $rest;
					}elseif($first_char === '+'){
						$new_lines[] = $rest;
					}elseif($first_char === ' ' || $first_char === ''){
						$old_lines[] = $rest;
						$new_lines[] = $rest;
					}elseif($first_char === '\\'){
						// "\ No newline at end of file" - skip
					}else{
						break;
					}
					$i++;
				}

				$found = false;
				$search_start = max(0, $old_start - 1 - 5 + $offset);
				$search_end = min(count($result), $old_start - 1 + 5 + $offset);
				for($s = $search_start; $s <= $search_end; $s++){
					$match = true;
					for($j = 0; $j < count($old_lines); $j++){
						if($s + $j >= count($result) || rtrim($result[$s + $j]) !== rtrim($old_lines[$j])){
							$match = false;
							break;
						}
					}
					if($match){
						array_splice($result, $s, count($old_lines), $new_lines);
						$offset += count($new_lines) - count($old_lines);
						$found = true;
						break;
					}
				}
				if(!$found){
					for($s = 0; $s < count($result); $s++){
						$match = true;
						for($j = 0; $j < count($old_lines); $j++){
							if($s + $j >= count($result) || rtrim($result[$s + $j]) !== rtrim($old_lines[$j])){
								$match = false;
								break;
							}
						}
						if($match){
							array_splice($result, $s, count($old_lines), $new_lines);
							$offset += count($new_lines) - count($old_lines);
							$found = true;
							break;
						}
					}
					if(!$found){
						return false;
					}
				}
			}else{
				$i++;
			}
		}
		return implode("\n", $result);
	}

	private function tool_question(array $params){
		$question = isset($params['question']) ? $params['question'] : '';
		$options = isset($params['options']) ? $params['options'] : array();
		$multiple = !empty($params['multiple']);
		$allow_custom = isset($params['custom']) ? $params['custom'] : true;

		if(empty($question)){
			return array('output' => __('Question text is required'), 'is_error' => true);
		}

		$q_data = array(
			'question' => $question,
			'options' => $options,
			'multiple' => $multiple,
			'custom' => $allow_custom
		);

		return array(
			'output' => json_encode($q_data),
			'is_error' => false,
			'_question' => true
		);
	}

	private function format_bytes($bytes){
		if($bytes >= 1048576) return round($bytes / 1048576, 1).'MB';
		if($bytes >= 1024) return round($bytes / 1024, 1).'KB';
		return $bytes.'B';
	}

	private function is_dangerous_command($command){
		$protected_dirs = array('.softaculous', '.ssh', '.gnupg', '.softaculous-pro', 'softaculous-pro');
		foreach($protected_dirs as $dir){
			if(preg_match('#/(?:' . preg_quote($dir, '#') . ')(?:/|$)#i', $command)){
				if(preg_match('/\b(rm|rmdir|mv|chmod|chown|chgrp|ln|unlink)\b/', $command)){
					return true;
				}
			}
		}

		$dangerous = array(
			'/\brm\s+-[a-zA-Z]*r[a-zA-Z]*f\s+\/\s*$/',
			'/\brm\s+-[a-zA-Z]*f[a-zA-Z]*r\s+\/\s*$/',
			'/\brm\s+-rf\s+~\s*$/',
			'/\brm\s+-fr\s+~\s*$/',
			'/:\(\)\{.*;\}\s*;&/',
			'/\bdd\s+if=/',
			'/\bmkfs\b/',
			'/\bformat\s+[a-z]:/i',
			'/\bshutdown\b/',
			'/\breboot\b/',
			'/\bhalt\b/',
			'/\binit\s+[06]\b/',
			'/>\s*\/dev\/sda/',
			'/\bchmod\s+-R\s+777\s+\//',
			'/\bchown\s+-R\s+.*\s+\//',
			'/\bcurl\b.*\|\s*(ba)?sh/',
			'/\bwget\b.*\|\s*(ba)?sh/',
			'/\bsudo\b/',
			'/\bsu\s+/',
			'/\bssh\b/',
			'/\bscp\b/',
			'/\brsync\b/',
			'/\bnc\s/',
			'/\bncat\b/',
			'/\bsocat\b/',
			'/\bcrontab\b/',
			'/\bat\s+[a-z]+\s/',
			'/\bsystemctl\b/',
			'/\bservice\b/',
			'/\biptables\b/',
			'/\bip\s+route\b/',
			'/\bmysql\s/',
			'/\bpsql\s/',
			'/\bmongosh\b/',
			'/\bpython\d?\s+-c\s/',
			'/\bperl\s+-e\s/',
			'/\bruby\s+-e\s/',
			'/\bkill\s+-9\s+1\b/',
			'/\bkillall\b/',
			'/\bpkill\b/',
			'/\bshutdown\b/',
			'/\bpoweroff\b/',
		);
		foreach($dangerous as $pattern){
			if(preg_match($pattern, $command)) return true;
		}
		return false;
	}
}