<?php
/** Sichere Einmal-Erzeugung einer lokalen Agent-Konfiguration. */

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

require_once __DIR__ . '/connection-mcpb.php';

define( 'WPAGENT_COMPANION_OPT_CONNECTIONS', 'wp_agent_companion_connections' );
define( 'WPAGENT_COMPANION_OPT_CONNECTION_LOCK', 'wp_agent_companion_connections_lock' );
define( 'WPAGENT_COMPANION_OPT_UNINSTALL_CLEANUP', 'wp_agent_companion_uninstall_cleanup' );

/**
 * Returns the explicit credential-cleanup choice for a future uninstall.
 *
 * Deleting credentials is the secure default. Only exact, persisted booleans
 * can opt out, so a malformed option can never silently leave a usable
 * connection behind after the plugin was removed.
 *
 * @return array{oauth:bool,application_passwords:bool,skills:bool,memory:bool}
 */
function wpagent_companion_uninstall_cleanup_preferences(): array {
	$stored   = get_option( WPAGENT_COMPANION_OPT_UNINSTALL_CLEANUP, array() );
	$stored   = is_array( $stored ) ? $stored : array();
	$defaults = array(
		'oauth'                 => true,
		'application_passwords' => true,
		'skills'                => true,
		'memory'                => true,
	);
	$result = array();
	foreach ( $defaults as $name => $default ) {
		$value = $stored[ $name ] ?? $default;
		$result[ $name ] = is_bool( $value ) ? $value : $default;
	}
	return $result;
}

/** @return string|WP_Error Lock-Token oder Fehler. */
function wpagent_companion_acquire_connection_lock() {
	$token = wpagent_companion_acquire_option_lock( WPAGENT_COMPANION_OPT_CONNECTION_LOCK, 30 );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_connection_busy', __( 'Eine andere Verbindungsänderung läuft bereits. Versuche es gleich erneut.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_companion_release_connection_lock( string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_COMPANION_OPT_CONNECTION_LOCK, $token );
}

/**
 * @param bool $require_application_passwords Ob für ein Application-Password-Bundle zusätzlich dessen Verfügbarkeit verlangt wird.
 * @return array<int,array{id:int,login:string,name:string,roles:string[]}>
 */
function wpagent_companion_connection_users( bool $require_application_passwords = false ): array {
	$eligible = array();
	foreach ( get_users( array( 'number' => 200, 'orderby' => 'registered', 'order' => 'DESC' ) ) as $user ) {
		if ( ! wpagent_companion_is_safe_connection_user( $user ) ) {
			continue;
		}
		if ( $require_application_passwords && function_exists( 'wp_is_application_passwords_available_for_user' ) && ! wp_is_application_passwords_available_for_user( $user ) ) {
			continue;
		}
		$eligible[] = array(
			'id'    => (int) $user->ID,
			'login' => (string) $user->user_login,
			'name'  => (string) $user->display_name,
			'roles' => array_values( (array) $user->roles ),
		);
	}
	return $eligible;
}

function wpagent_companion_is_safe_connection_user( WP_User $user ): bool {
	if ( ! $user->exists() || ! user_can( $user, 'edit_posts' ) || user_can( $user, 'manage_options' ) ) {
		return false;
	}
	// Ninja Forms bündelt sämtliche nativen Abilities hinter diesem einen
	// Provider-Recht. Ein Nutzer mit dauerhaftem Recht könnte die feineren
	// WPAgently-Gruppen und die Quarantäne über fremde MCP-Endpunkte umgehen.
	if ( ! empty( $user->allcaps['nf_edit_forms'] ) ) {
		return false;
	}
	return ! function_exists( 'is_super_admin' ) || ! is_super_admin( $user->ID );
}

/**
 * Prüft den Agent-Nutzer während determine_current_user, ohne den Filter
 * rekursiv erneut zu starten. Manche Capability-Filter, darunter Yoast SEO,
 * lesen selbst den aktuellen Nutzer. Der zeitweise gesetzte globale Nutzer
 * gibt ihnen den richtigen Kontext, ohne set_current_user auszulösen. Normale
 * WordPress-Rechtefilter bleiben dadurch vollständig wirksam.
 */
function wpagent_companion_is_safe_connection_user_during_authentication( WP_User $user ): bool {
	$had_current_user      = array_key_exists( 'current_user', $GLOBALS );
	$previous_current_user = $GLOBALS['current_user'] ?? null;
	$GLOBALS['current_user'] = $user;
	try {
		return wpagent_companion_is_safe_connection_user( $user );
	} finally {
		if ( $had_current_user ) {
			$GLOBALS['current_user'] = $previous_current_user;
		} else {
			unset( $GLOBALS['current_user'] );
		}
	}
}

/**
 * Verhindert, dass ein vom Companion erzeugtes Application Password nach
 * einer späteren Rechteausweitung seines Nutzers weiterverwendet wird.
 * Fremde Application Passwords, insbesondere die getrennten
 * Administrator-Zugangsdaten für Power, bleiben davon unberührt.
 */
function wpagent_companion_reject_unsafe_tracked_application_password( WP_Error $error, WP_User $user, array $item ): void {
	$uuid = is_string( $item['uuid'] ?? null ) ? $item['uuid'] : '';
	if ( ! wp_is_uuid( $uuid ) ) {
		return;
	}

	foreach ( wpagent_companion_connection_records() as $record ) {
		if ( (int) $user->ID !== $record['user_id'] || ! hash_equals( $record['app_uuid'], $uuid ) ) {
			continue;
		}
		if ( ! wpagent_companion_is_safe_connection_user_during_authentication( $user ) ) {
			$error->add(
				'wpagent_companion_unsafe_connection_user',
				__( 'Der gewählte Nutzer muss Inhalte bearbeiten dürfen und darf kein Administrator sein.', 'wp-agent-companion' )
			);
		}
		return;
	}
}
add_action( 'wp_authenticate_application_password_errors', 'wpagent_companion_reject_unsafe_tracked_application_password', 10, 3 );

/**
 * Legt einen dedizierten Redakteur mit zufälligem internen Login-Passwort an.
 * Das normale Passwort wird nicht ausgegeben. Agenten verwenden ausschließlich
 * eine widerrufbare OAuth-Verbindung oder ein optionales Application Password.
 *
 * @return array{id:int,login:string,role:string}|WP_Error
 */
function wpagent_companion_create_bot_user() {
	if ( ! wpagent_companion_gate_open() ) {
		return new WP_Error( 'wpagent_connection_gate_closed', __( 'Öffne zuerst das Companion-Gate und behebe die roten Systemdiagnosen.', 'wp-agent-companion' ) );
	}
	$base  = 'wpagently-agent';
	$login = $base;
	$index = 2;
	while ( username_exists( $login ) ) {
		$login = $base . '-' . $index;
		++$index;
		if ( $index > 1000 ) {
			return new WP_Error( 'wpagent_connection_user_limit', __( 'Es konnte kein freier Agent-Benutzername gefunden werden.', 'wp-agent-companion' ) );
		}
	}
	$user_id = wp_insert_user(
		array(
			'user_login'   => $login,
			'user_pass'    => wp_generate_password( 64, true, true ),
			'display_name' => __( 'WPAgently Agent', 'wp-agent-companion' ),
			'role'         => 'editor',
		)
	);
	if ( is_wp_error( $user_id ) ) {
		return $user_id;
	}
	$user = get_user_by( 'id', $user_id );
	if ( ! $user || ! in_array( 'editor', (array) $user->roles, true ) || user_can( $user, 'manage_options' ) ) {
		wp_delete_user( $user_id );
		return new WP_Error( 'wpagent_connection_bad_created_user', __( 'Der sichere Agent-Nutzer konnte nicht verifiziert werden.', 'wp-agent-companion' ) );
	}
	return array( 'id' => (int) $user_id, 'login' => $login, 'role' => 'editor' );
}

function wpagent_companion_toml_string( string $value ): string {
	$escaped = str_replace(
		array( '\\', '"', "\n", "\r", "\t" ),
		array( '\\\\', '\\"', '\\n', '\\r', '\\t' ),
		$value
	);
	return '"' . $escaped . '"';
}

function wpagent_companion_mcp_endpoint(): string {
	return untrailingslashit( rest_url( 'wp-agent/mcp' ) );
}

/**
 * Entfernt WHATWG-Punktsegmente vor dem Dekodieren und dekodiert anschließend
 * gültige UTF-8-Prozentfolgen einzeln. So wird ein kodierter Slash nicht
 * nachträglich zum Pfadtrenner. Ungültige Bytefolgen bleiben kanonisch kodiert.
 */
function wpagent_companion_connection_key_path( string $path ): string {
	$segments = explode( '/', $path );
	$normalized = array();
	$last_index = count( $segments ) - 1;
	foreach ( $segments as $index => $segment ) {
		$lower = strtolower( $segment );
		$is_single_dot = '.' === $lower || '%2e' === $lower;
		$is_double_dot = '..' === $lower || '.%2e' === $lower || '%2e.' === $lower || '%2e%2e' === $lower;
		if ( $is_single_dot ) {
			if ( $index === $last_index ) {
				$normalized[] = '';
			}
			continue;
		}
		if ( $is_double_dot ) {
			if ( count( $normalized ) > 1 ) {
				array_pop( $normalized );
			}
			if ( $index === $last_index ) {
				$normalized[] = '';
			}
			continue;
		}
		$normalized[] = $segment;
	}
	$canonical_path = implode( '/', $normalized );
	$result = preg_replace_callback(
		'/(?:%[0-9a-f]{2})+/i',
		static function ( array $matches ): string {
			$decoded = rawurldecode( $matches[0] );
			return 1 === preg_match( '//u', $decoded ) ? $decoded : strtoupper( $matches[0] );
		},
		$canonical_path
	);
	return is_string( $result ) ? $result : $canonical_path;
}

/**
 * Normalisiert internationalisierte Hostnamen auf dieselbe ASCII-Form wie
 * WHATWG-URL in der Node-CLI. WordPress bringt Requests als sicheren Fallback
 * mit, falls die optionale Intl-Erweiterung nicht geladen ist.
 */
function wpagent_companion_connection_key_host( string $host ): string {
	$host = function_exists( 'mb_strtolower' ) ? mb_strtolower( $host, 'UTF-8' ) : strtolower( $host );
	if ( 1 !== preg_match( '/[^\x00-\x7f]/', $host ) ) {
		return strtolower( $host );
	}

	$ascii = false;
	if ( function_exists( 'idn_to_ascii' ) && defined( 'INTL_IDNA_VARIANT_UTS46' ) ) {
		$flags = defined( 'IDNA_DEFAULT' ) ? IDNA_DEFAULT : 0;
		foreach ( array( 'IDNA_USE_STD3_RULES', 'IDNA_CHECK_BIDI', 'IDNA_CHECK_CONTEXTJ', 'IDNA_NONTRANSITIONAL_TO_ASCII' ) as $constant ) {
			if ( defined( $constant ) ) {
				$flags |= constant( $constant );
			}
		}
		$ascii = idn_to_ascii( $host, $flags, INTL_IDNA_VARIANT_UTS46 );
	} elseif ( class_exists( '\\WpOrg\\Requests\\IdnaEncoder' ) ) {
		try {
			$ascii = \WpOrg\Requests\IdnaEncoder::encode( $host );
		} catch ( Throwable $error ) {
			$ascii = false;
		}
	}

	return is_string( $ascii ) && '' !== $ascii ? strtolower( $ascii ) : strtolower( $host );
}

/**
 * Liest Schema, Host und Port aus der unveränderten URL-Autorität. PHPs
 * parse_url ersetzt einzelne gültige UTF-8-Bytes im Host durch Platzhalter,
 * bevor ein IDN-Konverter sie verarbeiten kann.
 *
 * @return array{scheme:string,host:string,port:int}|null
 */
function wpagent_companion_connection_key_authority( string $url ): ?array {
	if ( 1 !== preg_match( '/^([a-z][a-z0-9+.-]*):\/\/([^\/?#]*)/i', $url, $matches ) ) {
		return null;
	}
	$authority = $matches[2];
	$at        = strrpos( $authority, '@' );
	if ( false !== $at ) {
		$authority = substr( $authority, $at + 1 );
	}

	$host = $authority;
	$port = 0;
	if ( str_starts_with( $authority, '[' ) ) {
		$end = strpos( $authority, ']' );
		if ( false === $end ) {
			return null;
		}
		$host      = substr( $authority, 0, $end + 1 );
		$remainder = substr( $authority, $end + 1 );
		if ( '' !== $remainder && 1 === preg_match( '/^:([0-9]+)$/', $remainder, $port_match ) ) {
			$port = absint( $port_match[1] );
		} elseif ( '' !== $remainder ) {
			return null;
		}
	} elseif ( 1 === preg_match( '/^(.*):([0-9]+)$/s', $authority, $host_port ) ) {
		$host = $host_port[1];
		$port = absint( $host_port[2] );
	}

	return array(
		'scheme' => strtolower( $matches[1] ),
		'host'   => wpagent_companion_connection_key_host( rawurldecode( $host ) ),
		'port'   => $port,
	);
}

/**
 * Stabiler, lesbarer Client-Schlüssel, damit mehrere WordPress-Sites in einer
 * gemeinsamen MCP-Konfiguration nicht denselben wp-agent-Eintrag überschreiben.
 */
function wpagent_companion_connection_server_key( string $prefix = 'wp-agent', ?string $url = null ): string {
	$url      = null === $url ? home_url( '/' ) : $url;
	$parts    = wp_parse_url( $url );
	$authority = wpagent_companion_connection_key_authority( $url );
	$scheme   = is_array( $authority ) ? $authority['scheme'] : ( is_array( $parts ) && is_string( $parts['scheme'] ?? null ) ? strtolower( $parts['scheme'] ) : '' );
	$host     = is_array( $authority ) ? $authority['host'] : ( is_array( $parts ) && is_string( $parts['host'] ?? null ) ? wpagent_companion_connection_key_host( $parts['host'] ) : '' );
	$number   = is_array( $authority ) ? $authority['port'] : ( is_array( $parts ) && isset( $parts['port'] ) ? absint( $parts['port'] ) : 0 );
	$port     = $number > 0 && ! ( 80 === $number && 'http' === $scheme ) && ! ( 443 === $number && 'https' === $scheme ) ? ':' . $number : '';
	$path     = is_array( $parts ) && is_string( $parts['path'] ?? null ) ? untrailingslashit( wpagent_companion_connection_key_path( $parts['path'] ) ) : '';
	$identity_source = $host . $port . $path;
	$digest_source   = ( '' !== $scheme ? $scheme . '://' : '' ) . $identity_source;
	$identity        = trim( (string) preg_replace( '/[^a-z0-9]+/', '-', strtolower( $identity_source ) ), '-' );
	if ( '' === $identity ) {
		$identity = 'site';
	}
	$identity = rtrim( substr( $identity, 0, 39 ), '-' ) . '-' . substr( hash( 'sha256', $digest_source ), 0, 8 );
	$prefix = trim( (string) preg_replace( '/[^a-z0-9-]+/', '-', strtolower( $prefix ) ), '-' );
	return ( '' !== $prefix ? $prefix : 'wp-agent' ) . '-' . $identity;
}

function wpagent_companion_normalize_connection_client( string $client ): string {
	return 'claude' === $client ? 'claude-code' : $client;
}

/** @return array<string,string> */
function wpagent_companion_connection_client_labels(): array {
	return array(
		'chatgpt'        => 'ChatGPT',
		'claude-code'    => 'Claude Code',
		'claude-desktop' => 'Claude Desktop',
		'codex'          => 'Codex',
		'cursor'         => 'Cursor',
		'windsurf'       => 'Windsurf',
		'vscode'         => 'Visual Studio Code',
		'gemini-cli'     => 'Gemini CLI',
		'opencode'       => 'OpenCode',
		'cline'          => 'Cline',
		'roo-code'       => 'Roo Code',
		'kilo-code'      => 'Kilo Code',
		'zed'            => 'Zed',
		'github-copilot' => 'GitHub Copilot',
		'amazon-q'       => 'Amazon Q',
		'antigravity'    => 'Antigravity',
	);
}

/** @return string[] */
function wpagent_companion_connection_oauth_clients(): array {
	return array( 'chatgpt', 'claude-code', 'codex', 'cursor', 'vscode' );
}

/** @return string[] */
function wpagent_companion_connection_oauth_only_clients(): array {
	return array( 'chatgpt' );
}

function wpagent_companion_connection_client_supports_application_password( string $client ): bool {
	$client = wpagent_companion_normalize_connection_client( $client );
	return in_array( $client, wpagent_companion_connection_clients(), true )
		&& ! in_array( $client, wpagent_companion_connection_oauth_only_clients(), true );
}

/** @return string[] */
function wpagent_companion_connection_clients(): array {
	return array_keys( wpagent_companion_connection_client_labels() );
}

/** @return array<string,array<string,mixed>> */
function wpagent_companion_connection_records(): array {
	$stored  = get_option( WPAGENT_COMPANION_OPT_CONNECTIONS, array() );
	$records = array();
	foreach ( is_array( $stored ) ? $stored : array() as $id => $record ) {
		if ( ! is_string( $id ) || ! wp_is_uuid( $id ) || ! is_array( $record ) ) {
			continue;
		}
		$user_id  = absint( $record['user_id'] ?? 0 );
		$app_uuid = is_string( $record['app_uuid'] ?? null ) ? $record['app_uuid'] : '';
		$client   = is_string( $record['client'] ?? null ) ? wpagent_companion_normalize_connection_client( sanitize_key( $record['client'] ) ) : '';
		if ( ! $user_id || ! wp_is_uuid( $app_uuid ) || ! in_array( $client, wpagent_companion_connection_clients(), true ) ) {
			continue;
		}
		$records[ $id ] = array(
			'id'         => $id,
			'user_id'    => $user_id,
			'app_uuid'   => $app_uuid,
			'client'     => $client,
			'label'      => sanitize_text_field( (string) ( $record['label'] ?? '' ) ),
			'created_at' => sanitize_text_field( (string) ( $record['created_at'] ?? '' ) ),
			'revoked_at' => sanitize_text_field( (string) ( $record['revoked_at'] ?? '' ) ),
		);
	}
	return $records;
}

/** @return array<int,array<string,mixed>> */
function wpagent_companion_connections(): array {
	$result = array();
	foreach ( wpagent_companion_connection_records() as $record ) {
		$user = get_user_by( 'id', $record['user_id'] );
		$app  = class_exists( 'WP_Application_Passwords' )
			? WP_Application_Passwords::get_user_application_password( $record['user_id'], $record['app_uuid'] )
			: null;
		$result[] = array(
			'id'         => $record['id'],
			'user_id'    => $record['user_id'],
			'user_login' => $user ? (string) $user->user_login : '',
			'client'     => $record['client'],
			'label'      => $record['label'],
			'created_at' => $record['created_at'],
			'status'     => $app && '' === $record['revoked_at'] ? 'active' : 'revoked',
			'revoked_by_user' => '' !== $record['revoked_at'],
			'last_used'  => is_array( $app ) && isset( $app['last_used'] ) ? (string) $app['last_used'] : '',
			'auth_type'  => 'application_password',
			'client_name' => '',
		);
	}
	if ( function_exists( 'wpagent_companion_oauth_connections' ) ) {
		$result = array_merge( $result, wpagent_companion_oauth_connections() );
	}
	usort( $result, static fn( array $a, array $b ): int => strcmp( $b['created_at'], $a['created_at'] ) );
	return $result;
}

/** @return true|WP_Error */
function wpagent_companion_track_connection( int $user_id, string $app_uuid, string $client, string $label ) {
	$lock = wpagent_companion_acquire_connection_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$records = wpagent_companion_connection_records();
		if ( count( $records ) >= 200 ) {
			foreach ( $records as $id => $record ) {
				$app = class_exists( 'WP_Application_Passwords' )
					? WP_Application_Passwords::get_user_application_password( $record['user_id'], $record['app_uuid'] )
					: null;
				if ( '' !== $record['revoked_at'] || ! $app ) {
					unset( $records[ $id ] );
				}
			}
			if ( count( $records ) >= 200 ) {
				return new WP_Error( 'wpagent_connection_limit', __( 'Es sind bereits 200 aktive Verbindungen registriert. Widerrufe zuerst eine nicht mehr benötigte Verbindung.', 'wp-agent-companion' ) );
			}
		}
		$id = wp_generate_uuid4();
		$records[ $id ] = array(
			'id'         => $id,
			'user_id'    => $user_id,
			'app_uuid'   => $app_uuid,
			'client'     => $client,
			'label'      => $label,
			'created_at' => gmdate( 'c' ),
			'revoked_at' => '',
		);
		update_option( WPAGENT_COMPANION_OPT_CONNECTIONS, $records, false );
		$fresh = wpagent_companion_connection_records();
		return isset( $fresh[ $id ] ) && $fresh[ $id ] === $records[ $id ]
			? true
			: new WP_Error( 'wpagent_connection_track_failed', __( 'Die Verbindung konnte nicht sicher registriert werden.', 'wp-agent-companion' ) );
	} finally {
		wpagent_companion_release_connection_lock( $lock );
	}
}

/** @return array{revoked:bool}|WP_Error */
function wpagent_companion_revoke_connection( string $id ) {
	$application_records = wpagent_companion_connection_records();
	if ( ! isset( $application_records[ $id ] ) && function_exists( 'wpagent_companion_oauth_revoke_grant' ) ) {
		return wpagent_companion_oauth_revoke_grant( $id );
	}
	$lock = wpagent_companion_acquire_connection_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$records = $application_records;
		if ( ! isset( $records[ $id ] ) ) {
			return new WP_Error( 'wpagent_connection_not_found', __( 'Verbindung nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		}
		$record = $records[ $id ];
		if ( ! class_exists( 'WP_Application_Passwords' ) ) {
			return new WP_Error( 'wpagent_connection_revoke_unavailable', __( 'Application Passwords sind nicht verfügbar. Die Verbindung wurde nicht als widerrufen markiert.', 'wp-agent-companion' ) );
		}
		$deleted = WP_Application_Passwords::delete_application_password( $record['user_id'], $record['app_uuid'] );
		if ( is_wp_error( $deleted ) ) {
			return $deleted;
		}
		if ( null !== WP_Application_Passwords::get_user_application_password( $record['user_id'], $record['app_uuid'] ) ) {
			return new WP_Error( 'wpagent_connection_revoke_failed', __( 'Das Application Password ist weiterhin aktiv. Die Verbindung wurde nicht als widerrufen markiert.', 'wp-agent-companion' ) );
		}
		$records[ $id ]['revoked_at'] = gmdate( 'c' );
		update_option( WPAGENT_COMPANION_OPT_CONNECTIONS, $records, false );
		$fresh = wpagent_companion_connection_records();
		if ( ! isset( $fresh[ $id ] ) || $fresh[ $id ] !== $records[ $id ] ) {
			return new WP_Error( 'wpagent_connection_revoke_state_failed', __( 'Die Verbindung wurde technisch widerrufen, ihr Status konnte aber nicht gespeichert werden.', 'wp-agent-companion' ) );
		}
		return array( 'revoked' => true );
	} finally {
		wpagent_companion_release_connection_lock( $lock );
	}
}

/** @return array<string,mixed> */
function wpagent_companion_support_report(): array {
	$connections = array_map(
		static fn( array $connection ): array => array(
			'client' => $connection['client'],
			'label' => $connection['label'],
			'user_login' => $connection['user_login'],
			'created_at' => $connection['created_at'],
			'status' => $connection['status'],
			'revoked_by_user' => $connection['revoked_by_user'],
			'last_used' => $connection['last_used'],
		),
		wpagent_companion_connections()
	);
	$routes = function_exists( 'rest_get_server' ) ? rest_get_server()->get_routes() : array();
	return array(
		'generated_at' => gmdate( 'c' ),
		'wpagently_version' => defined( 'WPAGENT_COMPANION_VERSION' ) ? WPAGENT_COMPANION_VERSION : '',
		'wordpress_version' => get_bloginfo( 'version' ),
		'php_version' => PHP_VERSION,
		'environment' => wp_get_environment_type(),
		'site_url' => site_url(),
		'home_url' => home_url(),
		'endpoint_registered' => isset( $routes['/wp-agent/mcp'] ),
		'diagnostics' => wpagent_companion_diagnostics(),
		'connection_warnings' => function_exists( 'wpagent_companion_connection_warnings' ) ? wpagent_companion_connection_warnings() : array(),
		'connection_diagnostics' => function_exists( 'wpagent_companion_latest_connection_diagnostics' ) ? wpagent_companion_latest_connection_diagnostics() : array(),
		'connections' => $connections,
		'active_plugins' => array_values( array_map( 'sanitize_text_field', (array) get_option( 'active_plugins', array() ) ) ),
	);
}

function wpagent_companion_mcpb_site_label( ?string $url = null ): string {
	$url       = null === $url ? home_url( '/' ) : $url;
	$parts     = wp_parse_url( $url );
	$authority = wpagent_companion_connection_key_authority( $url );
	$host      = is_array( $authority ) ? $authority['host'] : ( is_array( $parts ) ? (string) ( $parts['host'] ?? '' ) : '' );
	$path      = is_array( $parts ) && is_string( $parts['path'] ?? null ) ? wpagent_companion_connection_key_path( $parts['path'] ) : '';
	$segments  = array_values(
		array_filter(
			array_map( 'sanitize_title', explode( '/', trim( $path, '/' ) ) ),
			static fn( string $segment ): bool => '' !== $segment
		)
	);
	$label = '' !== $host ? $host : 'WordPress';
	if ( ! empty( $segments ) ) {
		$label .= '/' . implode( '/', $segments );
	}
	return wp_html_excerpt( $label, 96, '' );
}

/** @return string|WP_Error */
function wpagent_companion_build_mcpb( string $endpoint, string $authorization ) {
	$server_path = WPAGENT_COMPANION_DIR . 'assets/mcpb/server.mjs';
	if ( ! is_readable( $server_path ) ) {
		return new WP_Error( 'wpagent_connection_mcpb_runtime_missing', __( 'Die Claude-Desktop-Laufzeit fehlt im Plugin-Paket.', 'wp-agent-companion' ) );
	}
	$manifest = array(
		'manifest_version' => '0.3',
		'name' => 'wpagently-' . substr( hash( 'sha256', $endpoint ), 0, 12 ),
		'display_name' => 'WPAgently ' . wpagent_companion_mcpb_site_label(),
		'version' => defined( 'WPAGENT_COMPANION_VERSION' ) ? WPAGENT_COMPANION_VERSION : '0.0.0',
		'description' => 'Secure local MCP transport to a WPAgently WordPress connection.',
		'author' => array( 'name' => 'WPAgently', 'url' => 'https://wpagently.com' ),
		'homepage' => 'https://wpagently.com',
		'documentation' => 'https://wpagently.com/#ablauf',
		'privacy_policies' => array( 'https://wpagently.com/datenschutz/' ),
		'tools_generated' => true,
		'prompts_generated' => true,
		'compatibility' => array( 'platforms' => array( 'darwin', 'win32', 'linux' ), 'runtimes' => array( 'node' => '>=20' ) ),
		'server' => array(
			'type' => 'node',
			'entry_point' => 'server/server.mjs',
			'mcp_config' => array(
				'command' => 'node',
				'args' => array( '${__dirname}/server/server.mjs' ),
				'env' => array(
					// Site-spezifisch fest binden: ein editierbarer fremder Host dürfte
					// niemals das vorausgefüllte WordPress-Passwort erhalten.
					'WP_AGENT_ENDPOINT' => $endpoint,
					'WP_AGENT_AUTHORIZATION' => '${user_config.authorization}',
				),
			),
		),
		'user_config' => array(
			'authorization' => array( 'type' => 'string', 'title' => 'WordPress authorization', 'required' => true, 'sensitive' => true, 'default' => $authorization ),
		),
	);
	return wpagent_companion_archive_mcpb( $manifest, $server_path );
}

/**
 * Entfernt ein während eines fehlgeschlagenen Setups erzeugtes Passwort.
 * Bleibt es aktiv, ersetzt ein klarer Sicherheitsfehler den ursprünglichen
 * Setup-Fehler, damit der Administrator es manuell aus dem Nutzerprofil löscht.
 */
function wpagent_companion_cleanup_connection_password( int $user_id, string $app_uuid, WP_Error $original_error ): WP_Error {
	if ( class_exists( 'WP_Application_Passwords' ) ) {
		WP_Application_Passwords::delete_application_password( $user_id, $app_uuid );
		if ( null === WP_Application_Passwords::get_user_application_password( $user_id, $app_uuid ) ) {
			return $original_error;
		}
	}
	return new WP_Error(
		'wpagent_connection_cleanup_failed',
		__( 'Die Verbindung konnte nicht fertiggestellt werden und ihr Application Password ließ sich nicht entfernen. Lösche das neu angelegte Application Password manuell im Profil des gewählten Nutzers.', 'wp-agent-companion' ),
		array( 'status' => 500, 'original_error' => $original_error->get_error_code() )
	);
}

/**
 * Erstellt ein WordPress Application Password und liefert dessen lokale
 * Client-Konfiguration genau einmal an den Aufrufer zurück.
 *
 * @return array{filename:string,content_type:string,content:string,app_uuid:string}|WP_Error
 */
function wpagent_companion_create_connection_bundle( int $user_id, string $client, string $label ) {
	$client = wpagent_companion_normalize_connection_client( $client );
	if ( ! in_array( $client, wpagent_companion_connection_clients(), true ) ) {
		return new WP_Error( 'wpagent_connection_bad_client', __( 'Unbekannter Client.', 'wp-agent-companion' ) );
	}
	if ( ! wpagent_companion_connection_client_supports_application_password( $client ) ) {
		return new WP_Error( 'wpagent_connection_oauth_only', __( 'Dieser Client verwendet ausschließlich den browsergebundenen OAuth-Flow und erhält kein Application Password.', 'wp-agent-companion' ) );
	}
	if ( ! wpagent_companion_gate_open() ) {
		return new WP_Error( 'wpagent_connection_gate_closed', __( 'Öffne zuerst das Companion-Gate und behebe die roten Systemdiagnosen.', 'wp-agent-companion' ) );
	}
	$user = get_user_by( 'id', $user_id );
	if ( ! $user || ! wpagent_companion_is_safe_connection_user( $user ) ) {
		return new WP_Error( 'wpagent_connection_bad_user', __( 'Der gewählte Nutzer muss Inhalte bearbeiten dürfen und darf kein Administrator sein.', 'wp-agent-companion' ) );
	}
	if ( ! class_exists( 'WP_Application_Passwords' ) || ( function_exists( 'wp_is_application_passwords_available_for_user' ) && ! wp_is_application_passwords_available_for_user( $user ) ) ) {
		return new WP_Error( 'wpagent_connection_unavailable', __( 'Application Passwords sind auf dieser WordPress-Installation nicht verfügbar.', 'wp-agent-companion' ) );
	}

	$name    = sanitize_text_field( $label );
	$client_labels = wpagent_companion_connection_client_labels();
	$name    = '' !== $name ? wp_html_excerpt( $name, 255, '' ) : 'WPAgently ' . $client_labels[ $client ];
	$app_id  = wp_generate_uuid4();
	$created = WP_Application_Passwords::create_new_application_password(
		$user_id,
		array( 'name' => $name, 'app_id' => $app_id )
	);
	if ( is_wp_error( $created ) ) {
		return $created;
	}

	$password = preg_replace( '/\s+/', '', (string) $created[0] );
	$app_uuid = isset( $created[1]['uuid'] ) ? (string) $created[1]['uuid'] : '';
	$endpoint = wpagent_companion_mcp_endpoint();
	$authorization = 'Basic ' . base64_encode( $user->user_login . ':' . $password );
	$bundle = null;
	if ( 'claude-desktop' === $client ) {
		$content = wpagent_companion_build_mcpb( $endpoint, $authorization );
		if ( is_wp_error( $content ) ) {
			return wpagent_companion_cleanup_connection_password( $user_id, $app_uuid, $content );
		}
		$filename = wpagent_companion_connection_server_key( 'wpagently', home_url( '/' ) ) . '.mcpb';
		$bundle = array( 'filename' => $filename, 'content_type' => 'application/zip', 'content' => $content, 'app_uuid' => $app_uuid );
	} elseif ( in_array( $client, array( 'claude-code', 'cursor', 'windsurf', 'vscode', 'gemini-cli', 'opencode', 'cline', 'roo-code', 'kilo-code', 'zed', 'github-copilot', 'amazon-q', 'antigravity' ), true ) ) {
		$entry = array(
			'url'     => $endpoint,
			'headers' => array( 'Authorization' => $authorization ),
		);
		if ( 'claude-code' === $client ) {
			$entry = array_merge( array( 'type' => 'http' ), $entry );
		} elseif ( 'windsurf' === $client ) {
			$entry['serverUrl'] = $entry['url'];
			unset( $entry['url'] );
		} elseif ( 'vscode' === $client ) {
			$entry = array_merge( array( 'type' => 'http' ), $entry );
		} elseif ( 'gemini-cli' === $client ) {
			$entry['httpUrl'] = $entry['url'];
			$entry['trust']   = false;
			unset( $entry['url'] );
		} elseif ( 'opencode' === $client ) {
			$entry = array_merge( array( 'type' => 'remote' ), $entry, array( 'enabled' => true, 'oauth' => false ) );
		} elseif ( 'cline' === $client ) {
			$entry = array_merge( array( 'type' => 'streamableHttp' ), $entry, array( 'autoApprove' => array(), 'disabled' => false ) );
		} elseif ( 'roo-code' === $client ) {
			$entry = array_merge( array( 'type' => 'streamable-http' ), $entry, array( 'alwaysAllow' => array(), 'disabled' => false ) );
		} elseif ( 'kilo-code' === $client ) {
			$entry = array_merge( array( 'type' => 'remote' ), $entry, array( 'enabled' => true, 'oauth' => false ) );
		} elseif ( 'github-copilot' === $client ) {
			$entry = array_merge( array( 'type' => 'http' ), $entry, array( 'tools' => array( '*' ) ) );
		} elseif ( 'amazon-q' === $client ) {
			$entry = array_merge( array( 'type' => 'http' ), $entry );
		} elseif ( 'antigravity' === $client ) {
			$entry['serverUrl'] = $entry['url'];
			unset( $entry['url'] );
		}
		$root_key  = 'vscode' === $client
			? 'servers'
			: ( in_array( $client, array( 'opencode', 'kilo-code' ), true )
				? 'mcp'
				: ( 'zed' === $client ? 'context_servers' : 'mcpServers' ) );
		$filenames = array(
			'claude-code' => '.mcp.json',
			'cursor' => 'cursor-mcp.json',
			'windsurf' => 'windsurf-mcp_config.json',
			'vscode' => 'vscode-mcp.json',
			'gemini-cli' => 'gemini-settings.json',
			'opencode' => 'opencode.json',
			'cline' => 'cline-mcp.json',
			'roo-code' => 'roo-mcp.json',
			'kilo-code' => 'kilo-mcp.json',
			'zed' => 'zed-settings.json',
			'github-copilot' => 'github-copilot-mcp.json',
			'amazon-q' => 'amazon-q-mcp.json',
			'antigravity' => 'antigravity-mcp_config.json',
		);
		$server_key = wpagent_companion_connection_server_key();
		$content = wp_json_encode(
			array( $root_key => array( $server_key => $entry ) ),
			JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
		);
		if ( ! is_string( $content ) ) {
			$error = new WP_Error( 'wpagent_connection_config_encode_failed', __( 'Die Client-Konfiguration konnte nicht erstellt werden.', 'wp-agent-companion' ) );
			return wpagent_companion_cleanup_connection_password( $user_id, $app_uuid, $error );
		}
		$content .= "\n";
		$bundle = array( 'filename' => $filenames[ $client ], 'content_type' => 'application/json', 'content' => $content, 'app_uuid' => $app_uuid );
	} else {
		$server_key = str_replace( '-', '_', wpagent_companion_connection_server_key() );
		$content = implode(
		"\n",
		array(
			'# Enthält ein geheimes WordPress Application Password. Nicht committen.',
				'[mcp_servers.' . $server_key . ']',
			'url = ' . wpagent_companion_toml_string( $endpoint ),
			'http_headers = { Authorization = ' . wpagent_companion_toml_string( $authorization ) . ' }',
			'default_tools_approval_mode = "writes"',
		)
	) . "\n";
		$bundle = array( 'filename' => 'codex-mcp-wp-agent.toml', 'content_type' => 'application/toml', 'content' => $content, 'app_uuid' => $app_uuid );
	}

	$tracked = wpagent_companion_track_connection( $user_id, $app_uuid, $client, $name );
	if ( is_wp_error( $tracked ) ) {
		return wpagent_companion_cleanup_connection_password( $user_id, $app_uuid, $tracked );
	}
	return $bundle;
}
