<?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();

	public function __construct(AIFileManager $file_manager, $project_path, $user_home_dir = null, $mode = 'build'){
		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->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;
	}

	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, 0755, true);
		}
		$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);
	}

	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');
		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: ' . $tool_name . ' is not allowed. You can change this in Settings > Permissions.', 'is_error' => true, '_permission_deny' => 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 'apply_patch': return $this->tool_apply_patch($params);
			case 'question': return $this->tool_question($params);
			default: return array('output' => "Unknown tool: {$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);
		}

		$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);
		}

		$original = '';
		$existing = $this->file_manager->read_file($path);
		if(!empty($existing['content'])) $original = $existing['content'];

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

		$lines = substr_count($content, "\n") + 1;
		$return = array('output' => "Successfully wrote {$result['size']} bytes ({$lines} lines) to {$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);
		}

		$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 {$path}. Searched for: \"{$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 {$path}", 'is_error' => true);
		}

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

		$diff = $this->make_mini_diff($old_string, $new_string);
		$return = array('output' => "Edited {$path} ({$count} replacement".($count > 1 ? 's' : '').")\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);
		}

		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'] : '';

		$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: '.$_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);
		}

		$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: '.$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 '.count($todos).' items.', '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: '.$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: {$path}. Set overwrite=true to replace.", '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: {$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: '.($error ?: 'Unknown error'), 'is_error' => true);
		}

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

		$size_str = $this->format_bytes($size);
		return array('output' => "Downloaded {$size_str} to {$path} (HTTP {$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: {$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 {$destination} ({$count} files, {$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: {$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 {$count} files to {$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);
		}

		$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: '.$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: '.$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);
			$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){
				$write_result = $this->file_manager->write_file($rel_path, $new_content, true);
				if(empty($write_result['error'])){
					$results[] = "{$rel_path}: {$count} replacement(s)";
					$total_replacements += $count;
				}
			}
		}

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

		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: {$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 {$timeout}s", '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: {$return_code}]", 'is_error' => true);
		}

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

	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 {$timeout}s\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 {$max_idle_time}s with no output\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: {$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);
		}

		$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);
		}

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

		$lines = substr_count($patched, "\n") + 1;
		$return = array('output' => "Successfully applied patch to {$path} ({$lines} 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;
	}
}