<?php

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

function wpagent_companion_can_verify_private_mode( string $directory_separator = DIRECTORY_SEPARATOR ): bool {
	// PHPs chmod bildet unter Windows keine owner-only ACL ab. Ein positives
	// Ergebnis wäre dort kein Beleg dafür, dass andere lokale Nutzer ausgesperrt
	// sind. Die Generierung scheitert deshalb auf diesem Serverbetriebssystem.
	return '\\' !== $directory_separator;
}

function wpagent_companion_set_private_mode( string $path, int $mode ): bool {
	if ( ! wpagent_companion_can_verify_private_mode() ) {
		return false;
	}
	if ( ! @chmod( $path, $mode ) ) {
		return false;
	}
	clearstatcache( true, $path );
	$permissions = @fileperms( $path );
	return false !== $permissions && $mode === ( $permissions & 0777 );
}

/** @return string|WP_Error */
function wpagent_companion_create_mcpb_temp_directory() {
	if ( ! wpagent_companion_can_verify_private_mode() ) {
		return new WP_Error(
			'wpagent_connection_mcpb_private_acl_unavailable',
			__( 'MCPB kann auf diesem Server nicht sicher erzeugt werden, weil PHP keine private Dateisystem-ACL setzen und verifizieren kann. Verwende stattdessen die empfohlene OAuth-Verbindung in einem kompatiblen Client.', 'wp-agent-companion' )
		);
	}
	$temp_directory = get_temp_dir();
	if ( ! is_string( $temp_directory ) || ! is_dir( $temp_directory ) || ! is_writable( $temp_directory ) ) {
		return new WP_Error( 'wpagent_connection_mcpb_temp_failed', __( 'Temporäre MCPB-Datei konnte nicht angelegt werden.', 'wp-agent-companion' ) );
	}

	for ( $attempt = 0; $attempt < 10; ++$attempt ) {
		try {
			$suffix = bin2hex( random_bytes( 16 ) );
		} catch ( Throwable ) {
			return new WP_Error( 'wpagent_connection_mcpb_temp_failed', __( 'Temporäre MCPB-Datei konnte nicht angelegt werden.', 'wp-agent-companion' ) );
		}
		$root = trailingslashit( $temp_directory ) . 'wpagently-mcpb-' . $suffix;
		// mkdir ist hier absichtlich nicht rekursiv. Das atomare Anlegen darf
		// weder ein vorhandenes Verzeichnis noch einen Symlink übernehmen.
		if ( ! @mkdir( $root, 0700 ) ) {
			continue;
		}
		if ( is_link( $root ) || ! is_dir( $root ) || ! wpagent_companion_set_private_mode( $root, 0700 ) ) {
			@rmdir( $root );
			return new WP_Error( 'wpagent_connection_mcpb_temp_failed', __( 'Temporäres MCPB-Verzeichnis konnte nicht angelegt werden.', 'wp-agent-companion' ) );
		}
		return $root;
	}

	return new WP_Error( 'wpagent_connection_mcpb_temp_failed', __( 'Temporäres MCPB-Verzeichnis konnte nicht angelegt werden.', 'wp-agent-companion' ) );
}

function wpagent_companion_write_private_file( string $path, string $content ): bool {
	// x verweigert vorhandene reguläre Dateien und Symlinks gleichermaßen.
	$handle = @fopen( $path, 'xb' );
	if ( false === $handle ) {
		return false;
	}

	$complete = false;
	try {
		// Erst die Rechte verengen, dann sensible Daten schreiben. Dadurch
		// bleibt selbst bei einer großzügigen Prozess-umask kein Lesefenster.
		if ( ! wpagent_companion_set_private_mode( $path, 0600 ) ) {
			return false;
		}
		$length = strlen( $content );
		$offset = 0;
		while ( $offset < $length ) {
			$written = fwrite( $handle, substr( $content, $offset ) );
			if ( false === $written || 0 === $written ) {
				return false;
			}
			$offset += $written;
		}
		if ( ! fflush( $handle ) ) {
			return false;
		}
		$complete = true;
		return true;
	} finally {
		fclose( $handle );
		if ( ! $complete ) {
			@unlink( $path );
		}
	}
}

function wpagent_companion_create_private_file( string $path ): bool {
	$handle = @fopen( $path, 'xb' );
	if ( false === $handle ) {
		return false;
	}
	$private = wpagent_companion_set_private_mode( $path, 0600 );
	fclose( $handle );
	if ( ! $private ) {
		@unlink( $path );
	}
	return $private;
}

/**
 * @param array<string,mixed> $manifest MCPB manifest.
 * @return string|WP_Error
 */
function wpagent_companion_archive_mcpb( array $manifest, string $server_path ) {
	$root = wpagent_companion_create_mcpb_temp_directory();
	if ( is_wp_error( $root ) ) {
		return $root;
	}
	// Ein zusätzlicher Versuch am Request-Ende deckt kurzlebige Dateisperren ab,
	// nachdem der synchrone, verifizierte Cleanup bereits fail-closed meldete.
	register_shutdown_function( 'wpagent_companion_remove_temp_tree', $root );

	try {
		$result = ( static function () use ( $manifest, $root, $server_path ) {
			$server_directory = $root . '/server';
			if ( ! @mkdir( $server_directory, 0700 ) || is_link( $server_directory ) || ! wpagent_companion_set_private_mode( $server_directory, 0700 ) ) {
				return new WP_Error( 'wpagent_connection_mcpb_temp_failed', __( 'Temporäres MCPB-Verzeichnis konnte nicht angelegt werden.', 'wp-agent-companion' ) );
			}

			$manifest_json = wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
			$server_code   = file_get_contents( $server_path );
			if ( ! is_string( $manifest_json ) || ! is_string( $server_code ) ) {
				return new WP_Error( 'wpagent_connection_mcpb_write_failed', __( 'MCPB-Dateien konnten nicht geschrieben werden.', 'wp-agent-companion' ) );
			}
			$manifest_json .= "\n";
			if ( ! wpagent_companion_write_private_file( $root . '/manifest.json', $manifest_json )
				|| ! wpagent_companion_write_private_file( $server_directory . '/server.mjs', $server_code ) ) {
				return new WP_Error( 'wpagent_connection_mcpb_write_failed', __( 'MCPB-Dateien konnten nicht geschrieben werden.', 'wp-agent-companion' ) );
			}

			$zip_path = $root . '/bundle.mcpb';
			if ( ! wpagent_companion_create_private_file( $zip_path ) ) {
				return new WP_Error( 'wpagent_connection_mcpb_archive_failed', __( 'MCPB-Archiv konnte nicht erstellt werden.', 'wp-agent-companion' ) );
			}

			$created = false;
			if ( class_exists( 'ZipArchive' ) ) {
				$zip = new ZipArchive();
				if ( true === $zip->open( $zip_path, ZipArchive::OVERWRITE ) ) {
					$files_added = $zip->addFile( $root . '/manifest.json', 'manifest.json' )
						&& $zip->addFile( $server_directory . '/server.mjs', 'server/server.mjs' );
					$closed  = $zip->close();
					$created = $files_added && $closed;
				}
			} else {
				require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
				$zip = new PclZip( $zip_path );
				$created = 0 !== $zip->create( array( $root . '/manifest.json', $server_directory . '/server.mjs' ), PCLZIP_OPT_REMOVE_PATH, $root );
			}
			if ( ! $created || is_link( $zip_path ) || ! is_file( $zip_path ) || ! wpagent_companion_set_private_mode( $zip_path, 0600 ) ) {
				return new WP_Error( 'wpagent_connection_mcpb_archive_failed', __( 'MCPB-Archiv konnte nicht erstellt werden.', 'wp-agent-companion' ) );
			}
			$content = file_get_contents( $zip_path );
			return is_string( $content ) && '' !== $content
				? $content
				: new WP_Error( 'wpagent_connection_mcpb_archive_failed', __( 'MCPB-Archiv konnte nicht erstellt werden.', 'wp-agent-companion' ) );
		} )();
	} catch ( Throwable ) {
		$result = new WP_Error( 'wpagent_connection_mcpb_archive_failed', __( 'MCPB-Archiv konnte nicht erstellt werden.', 'wp-agent-companion' ) );
	}

	return wpagent_companion_finalize_mcpb_archive_result( $result, $root );
}

/** @return string|WP_Error */
function wpagent_companion_finalize_mcpb_archive_result( $result, string $root ) {
	$cleanup = wpagent_companion_remove_temp_tree( $root );
	return is_wp_error( $cleanup ) ? $cleanup : $result;
}

function wpagent_companion_temp_path_exists( string $path ): bool {
	clearstatcache( true, $path );
	return false !== @lstat( $path );
}

function wpagent_companion_temp_root_removed( string $root ): bool {
	clearstatcache( true, $root );
	if ( false !== @lstat( $root ) ) {
		return false;
	}

	// lstat liefert sowohl bei ENOENT als auch bei Dateisystemfehlern false.
	// Erst ein erfolgreiches Listing des Elternverzeichnisses beweist, dass der
	// zufällige Workspace-Name tatsächlich verschwunden ist.
	$parent_entries = @scandir( dirname( $root ) );
	return is_array( $parent_entries ) && ! in_array( basename( $root ), $parent_entries, true );
}

/** @return true|WP_Error */
function wpagent_companion_remove_temp_tree( string $root ) {
	$entries = array(
		'server/server.mjs' => $root . '/server/server.mjs',
		'manifest.json'     => $root . '/manifest.json',
		'bundle.mcpb'       => $root . '/bundle.mcpb',
		'server'            => $root . '/server',
		'root'              => $root,
	);

	for ( $attempt = 1; $attempt <= 3; ++$attempt ) {
		foreach ( array_slice( $entries, 0, 3, true ) as $file ) {
			if ( wpagent_companion_temp_path_exists( $file ) ) {
				@unlink( $file );
			}
		}
		if ( wpagent_companion_temp_path_exists( $entries['server'] ) ) {
			@rmdir( $entries['server'] );
		}
		if ( wpagent_companion_temp_path_exists( $entries['root'] ) ) {
			@rmdir( $entries['root'] );
		}

		if ( wpagent_companion_temp_root_removed( $root ) ) {
			return true;
		}

		$remaining = array();
		foreach ( $entries as $name => $path ) {
			if ( wpagent_companion_temp_path_exists( $path ) ) {
				$remaining[] = $name;
			}
		}
		if ( ! in_array( 'root', $remaining, true ) ) {
			$remaining[] = 'root';
		}
		if ( $attempt < 3 ) {
			usleep( 10000 * $attempt );
		}
	}

	return new WP_Error(
		'wpagent_connection_mcpb_cleanup_failed',
		__( 'Temporäre MCPB-Zugangsdaten konnten nach mehreren Versuchen nicht entfernt werden. Der Download wurde sicher abgebrochen. Prüfe die Dateirechte und temporären Dateien des Servers.', 'wp-agent-companion' ),
		array( 'status' => 500, 'attempts' => 3, 'remaining_entries' => $remaining )
	);
}
