<?php
/**
 * Sichere Freigabe der nativen Ninja-Forms-Abilities über den wpagently-MCP-Server.
 *
 * Ninja Forms besitzt eine breite native Abilities-API, verwendet dafür aber
 * ein einziges Recht. wpagently vermittelt dieses Recht ausschließlich während
 * eines passenden Aufrufs am eigenen MCP-Endpunkt. Es wird nie dauerhaft an
 * den Agent-Nutzer vergeben.
 *
 * @package WPAgentCompanion
 */

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

define( 'WPAGENT_COMPANION_OPT_NINJAFORMS_GRANTS', 'wp_agent_companion_ninjaforms_capability_grants' );

/**
 * Erkennt verschachtelte Query-Parameter am WordPress-Abilities-Endpunkt.
 *
 * Die Abilities-API transportiert Eingaben schreibgeschützter Abilities als
 * `input[...][...]` in der URL. PHP bildet diese Parameter bereits vor `init`
 * als Arrays in `$_GET` ab.
 */
function wpagent_companion_ninjaforms_is_nested_abilities_request(): bool {
	$has_array = false;
	foreach ( $_GET as $value ) {
		if ( is_array( $value ) ) {
			$has_array = true;
			break;
		}
	}
	if ( ! $has_array ) {
		return false;
	}

	$route = $_GET['rest_route'] ?? null;
	if ( is_string( $route ) && str_starts_with( untrailingslashit( $route ) . '/', '/wp-abilities/v1/' ) ) {
		return true;
	}

	$request_uri = isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] )
		? wp_unslash( $_SERVER['REQUEST_URI'] )
		: '';
	$path        = wp_parse_url( $request_uri, PHP_URL_PATH );
	if ( ! is_string( $path ) ) {
		return false;
	}
	$rest_prefix = '/' . trim( rest_get_url_prefix(), '/' ) . '/wp-abilities/v1/';
	return str_contains( trailingslashit( $path ), $rest_prefix );
}

/**
 * Unterdrückt einen bekannten Ninja-Forms-Warnpfad nur für verschachtelte
 * Abilities-Queries.
 *
 * Ninja Forms 3.14.10 registriert `NF_MergeTags_Other::init()` global und reicht
 * seine geschützte Methode `neutralize_shortcodes()` für Arraywerte an
 * `map_deep()` weiter. Der externe Callback kann die geschützte Methode nicht
 * aufrufen, landet in `__call()` und erzeugt für jeden verschachtelten
 * Abilities-Parameter Warnungen. Querystring-Merge-Tags werden an einem
 * REST-Abilities-Endpunkt nicht gerendert. Deshalb entfernen wir ausschließlich
 * in diesem Request den betroffenen `init`-Callback. Sobald Ninja Forms die
 * Methode öffentlich macht, bleibt der Callback automatisch unangetastet.
 */
function wpagent_companion_ninjaforms_disable_broken_query_merge_tags(): bool {
	if ( ! wpagent_companion_ninjaforms_is_nested_abilities_request() || ! class_exists( 'NF_MergeTags_Other' ) ) {
		return false;
	}

	try {
		$neutralize = new ReflectionMethod( 'NF_MergeTags_Other', 'neutralize_shortcodes' );
	} catch ( ReflectionException ) {
		return false;
	}
	if ( $neutralize->isPublic() ) {
		return false;
	}

	$hook = $GLOBALS['wp_filter']['init'] ?? null;
	if ( ! $hook instanceof WP_Hook ) {
		return false;
	}
	$callbacks = array();
	foreach ( $hook->callbacks as $priority => $entries ) {
		foreach ( $entries as $entry ) {
			$callback = $entry['function'] ?? null;
			if ( is_array( $callback ) && isset( $callback[0], $callback[1] ) && $callback[0] instanceof NF_MergeTags_Other && 'init' === $callback[1] ) {
				$callbacks[] = array( $callback, (int) $priority );
			}
		}
	}

	$removed = false;
	foreach ( $callbacks as list( $callback, $priority ) ) {
		$removed = remove_action( 'init', $callback, $priority ) || $removed;
	}
	return $removed;
}
add_action( 'init', 'wpagent_companion_ninjaforms_disable_broken_query_merge_tags', 9 );

/** @return array{forms:string,submissions:string,write:string,delivery:string} */
function wpagent_companion_ninjaforms_access_capabilities(): array {
	return array(
		'forms'       => 'wpagently_ninjaforms_forms_access',
		'submissions' => 'wpagently_ninjaforms_submissions_access',
		'write'       => 'wpagently_ninjaforms_write_access',
		'delivery'    => 'wpagently_ninjaforms_delivery_access',
	);
}

/**
 * Die bewusst freigegebenen, aktuell dokumentierten Ninja-Forms-Abilities.
 * Provider-Annotationen sind kein ausreichendes Gruppensignal, da Ninja Forms
 * auch seine Lese-Abilities als readonly=false registriert.
 *
 * @return array{forms:string[],submissions:string[],write:string[],delivery:string[]}
 */
function wpagent_companion_ninjaforms_ability_groups(): array {
	return array(
		'forms'       => array(
			'ninjaforms/list-forms',
			'ninjaforms/get-form',
			'ninjaforms/list-field-types',
			'ninjaforms/list-actions',
			'ninjaforms/list-calculations',
			'ninjaforms/export-form-definition',
		),
		'submissions' => array(
			'ninjaforms/get-submissions',
			'ninjaforms/get-submission',
			'ninjaforms/get-submission-fields',
			'ninjaforms/export-submissions',
		),
		'write'       => array(
			'ninjaforms/create-form',
			'ninjaforms/add-field',
			'ninjaforms/update-form',
			'ninjaforms/update-field',
			'ninjaforms/reorder-fields',
			'ninjaforms/duplicate-form',
			'ninjaforms/add-action',
			'ninjaforms/update-action',
			'ninjaforms/add-calculation',
			'ninjaforms/update-calculation',
			'ninjaforms/import-form',
			'ninjaforms/embed-form',
			'ninjaforms/get-public-link',
		),
		'delivery'    => array(
			'ninjaforms/process-submission',
		),
	);
}

/**
 * Abilities, die wegen fehlender Bestätigung, Geheimniszugriff oder globaler
 * Nebenwirkungen nicht über eine native Gruppenfreigabe angeboten werden.
 *
 * @return string[]
 */
function wpagent_companion_ninjaforms_quarantined_ability_names(): array {
	return array(
		'ninjaforms/delete-action',
		'ninjaforms/delete-calculation',
		'ninjaforms/delete-form',
		'ninjaforms/delete-submission',
		'ninjaforms/get-plugin-settings',
		'ninjaforms/remove-field',
		'ninjaforms/update-plugin-settings',
		'ninjaforms/update-submission',
	);
}

/** @return string[] */
function wpagent_companion_ninjaforms_known_ability_names(): array {
	$groups = wpagent_companion_ninjaforms_ability_groups();
	return array_values(
		array_unique(
			array_merge(
				$groups['forms'],
				$groups['submissions'],
				$groups['write'],
				$groups['delivery'],
				wpagent_companion_ninjaforms_quarantined_ability_names()
			)
		)
	);
}

/** @return array<int,string[]> */
function wpagent_companion_ninjaforms_capability_grants(): array {
	$stored  = get_option( WPAGENT_COMPANION_OPT_NINJAFORMS_GRANTS, array() );
	$allowed = array_values( wpagent_companion_ninjaforms_access_capabilities() );
	$grants  = array();
	foreach ( is_array( $stored ) ? $stored : array() as $user_id => $capabilities ) {
		$user_id = absint( $user_id );
		if ( ! $user_id || ! is_array( $capabilities ) ) {
			continue;
		}
		$capabilities = array_values( array_unique( array_intersect( $allowed, array_map( 'sanitize_key', $capabilities ) ) ) );
		if ( $capabilities ) {
			$grants[ $user_id ] = $capabilities;
		}
	}
	return array_slice( $grants, 0, 200, true );
}

/** @param array<int,string[]> $grants */
function wpagent_companion_ninjaforms_store_capability_grants( array $grants ): bool {
	if ( $grants ) {
		update_option( WPAGENT_COMPANION_OPT_NINJAFORMS_GRANTS, $grants, false );
	} else {
		delete_option( WPAGENT_COMPANION_OPT_NINJAFORMS_GRANTS );
	}
	return wpagent_companion_ninjaforms_capability_grants() === $grants;
}

/** @return array{enabled:bool,capability:string,tracked:bool}|WP_Error */
function wpagent_companion_ninjaforms_user_access( int $user_id, string $group ) {
	$capabilities = wpagent_companion_ninjaforms_access_capabilities();
	if ( ! isset( $capabilities[ $group ] ) ) {
		return new WP_Error( 'wpagent_ninjaforms_invalid_group', __( 'Unbekannte Ninja-Forms-Rechtegruppe.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$user = get_user_by( 'id', $user_id );
	if ( ! $user instanceof WP_User || ! wpagent_companion_is_safe_connection_user( $user ) ) {
		return new WP_Error( 'wpagent_ninjaforms_unsafe_user', __( 'Wähle einen Redakteur ohne Administrator- oder Super-Administratorrechte.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$grants     = wpagent_companion_ninjaforms_capability_grants();
	$capability = $capabilities[ $group ];
	return array(
		'enabled'    => user_can( $user, $capability ),
		'capability' => $capability,
		'tracked'    => in_array( $capability, $grants[ $user_id ] ?? array(), true ),
	);
}

/** @return array{enabled:bool,capability:string,tracked:bool}|WP_Error */
function wpagent_companion_ninjaforms_set_user_access( int $user_id, string $group, bool $enabled ) {
	$capabilities = wpagent_companion_ninjaforms_access_capabilities();
	if ( ! isset( $capabilities[ $group ] ) ) {
		return new WP_Error( 'wpagent_ninjaforms_invalid_group', __( 'Unbekannte Ninja-Forms-Rechtegruppe.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$user = get_user_by( 'id', $user_id );
	if ( ! $user instanceof WP_User || ! wpagent_companion_is_safe_connection_user( $user ) ) {
		return new WP_Error( 'wpagent_ninjaforms_unsafe_user', __( 'Wähle einen Redakteur ohne Administrator- oder Super-Administratorrechte.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$status = wpagent_companion_ninjaforms_integration_status();
	if ( $enabled && empty( $status[ 'available_' . $group ] ) ) {
		return new WP_Error( 'wpagent_ninjaforms_missing', __( 'Ninja Forms ist nicht aktiv oder stellt die gewählte native Ability-Gruppe nicht bereit.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}

	$lock = wpagent_companion_acquire_connection_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}

	try {
		$capability      = $capabilities[ $group ];
		$grants          = wpagent_companion_ninjaforms_capability_grants();
		$original_grants = $grants;
		$tracked         = $grants[ $user_id ] ?? array();
		$original_direct = ! empty( $user->caps[ $capability ] );
		$expected_direct = $original_direct;

		if ( $enabled ) {
			if ( in_array( $capability, $tracked, true ) && ! $original_direct ) {
				$user->add_cap( $capability );
				$expected_direct = true;
			} elseif ( ! user_can( $user, $capability ) ) {
				$user->add_cap( $capability );
				$tracked[]       = $capability;
				$expected_direct = true;
			}
		} elseif ( in_array( $capability, $tracked, true ) ) {
			$user->remove_cap( $capability );
			$tracked         = array_values( array_diff( $tracked, array( $capability ) ) );
			$expected_direct = false;
		}

		if ( $tracked ) {
			$grants[ $user_id ] = array_values( array_unique( $tracked ) );
		} else {
			unset( $grants[ $user_id ] );
		}
		clean_user_cache( $user_id );

		$fresh_user    = get_user_by( 'id', $user_id );
		$actual_direct = $fresh_user instanceof WP_User && ! empty( $fresh_user->caps[ $capability ] );
		if ( $actual_direct !== $expected_direct || ! wpagent_companion_ninjaforms_store_capability_grants( $grants ) ) {
			if ( $original_direct ) {
				$user->add_cap( $capability );
			} else {
				$user->remove_cap( $capability );
			}
			wpagent_companion_ninjaforms_store_capability_grants( $original_grants );
			clean_user_cache( $user_id );
			return new WP_Error( 'wpagent_ninjaforms_grant_failed', __( 'Die Ninja-Forms-Rechte konnten nicht sicher gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}

		return wpagent_companion_ninjaforms_user_access( $user_id, $group );
	} finally {
		wpagent_companion_release_connection_lock( $lock );
	}
}

/** @return array<string,string> Ability-Name auf benötigtes wpagently-Recht. */
function wpagent_companion_ninjaforms_ability_capability_map(): array {
	$map          = array();
	$capabilities = wpagent_companion_ninjaforms_access_capabilities();
	foreach ( wpagent_companion_ninjaforms_ability_groups() as $group => $names ) {
		foreach ( $names as $name ) {
			$map[ $name ] = $capabilities[ $group ];
		}
	}
	return $map;
}

/** @param string[] $names */
function wpagent_companion_ninjaforms_set_request_ability_names( array $names ): void {
	$GLOBALS['wpagent_companion_ninjaforms_request_abilities'] = array_values(
		array_unique(
			array_filter(
				$names,
				static fn( $name ): bool => is_string( $name ) && str_starts_with( $name, 'ninjaforms/' )
			)
		)
	);
}

/** @return string[] */
function wpagent_companion_ninjaforms_current_request_ability_names(): array {
	$names = $GLOBALS['wpagent_companion_ninjaforms_request_abilities'] ?? array();
	return is_array( $names ) ? array_slice( array_values( $names ), 0, 100 ) : array();
}

/** @return string[] */
function wpagent_companion_ninjaforms_request_ability_names( WP_REST_Request $request ): array {
	if ( '/wp-agent/mcp' !== untrailingslashit( $request->get_route() ) ) {
		return array();
	}
	$payload = $request->get_json_params();
	if ( ! is_array( $payload ) ) {
		return array();
	}
	$messages = array_is_list( $payload ) ? $payload : array( $payload );
	if ( count( $messages ) > 100 ) {
		return array( 'ninjaforms/__invalid-oversized-batch' );
	}
	$names    = array();
	foreach ( $messages as $message ) {
		if ( ! is_array( $message ) || 'tools/call' !== ( $message['method'] ?? null ) ) {
			continue;
		}
		$tool_name = $message['params']['name'] ?? null;
		if ( ! is_string( $tool_name ) || ! str_starts_with( $tool_name, 'ninjaforms-' ) ) {
			$names[] = 'ninjaforms/__invalid-mixed-call';
			continue;
		}
		$ability_name = 'ninjaforms/' . substr( $tool_name, strlen( 'ninjaforms-' ) );
		if ( preg_match( '#^ninjaforms/[a-z0-9][a-z0-9-]*$#', $ability_name ) ) {
			$names[] = $ability_name;
		} else {
			$names[] = 'ninjaforms/__invalid-mixed-call';
		}
	}
	return array_values( array_unique( $names ) );
}

/** @param mixed $response @param mixed $handler @return mixed */
function wpagent_companion_ninjaforms_capture_rest_request( $response, $handler, WP_REST_Request $request ) {
	wpagent_companion_ninjaforms_set_request_ability_names( wpagent_companion_ninjaforms_request_ability_names( $request ) );
	return $response;
}
add_filter( 'rest_request_before_callbacks', 'wpagent_companion_ninjaforms_capture_rest_request', 1, 3 );

/** @param mixed $response @param mixed $handler @return mixed */
function wpagent_companion_ninjaforms_clear_rest_request( $response, $handler, WP_REST_Request $request ) {
	if ( '/wp-agent/mcp' === untrailingslashit( $request->get_route() ) ) {
		wpagent_companion_ninjaforms_set_request_ability_names( array() );
	}
	return $response;
}
add_filter( 'rest_request_after_callbacks', 'wpagent_companion_ninjaforms_clear_rest_request', 999, 3 );

/**
 * Vermittelt nf_edit_forms nur für die aktuell angeforderten, freigegebenen
 * Ninja-Werkzeuge am exakten wpagently-Endpunkt. Bei JSON-RPC-Batches müssen
 * sämtliche enthaltenen Ninja-Aufrufe durch ihre jeweilige Gruppe erlaubt sein.
 *
 * @param bool[] $allcaps
 * @param string[] $caps
 * @param mixed[] $args
 * @return bool[]
 */
function wpagent_companion_ninjaforms_user_has_cap( array $allcaps, array $caps, array $args, WP_User $user ): array {
	if ( ! in_array( 'nf_edit_forms', $caps, true ) || ! wpagent_companion_wpforms_is_mcp_request() ) {
		return $allcaps;
	}
	$names = wpagent_companion_ninjaforms_current_request_ability_names();
	$map   = wpagent_companion_ninjaforms_ability_capability_map();
	if ( ! $names ) {
		return $allcaps;
	}

	// Am eigenen MCP-Endpunkt darf auch ein versehentlich bereits vorhandenes
	// breites Provider-Recht die Gruppierung und Quarantäne nicht umgehen.
	$allcaps['nf_edit_forms'] = false;
	if ( ! wpagent_companion_is_safe_connection_user( $user ) ) {
		return $allcaps;
	}
	foreach ( $names as $name ) {
		if ( ! isset( $map[ $name ] ) || empty( $allcaps[ $map[ $name ] ] ) ) {
			return $allcaps;
		}
	}
	$allcaps['nf_edit_forms'] = true;
	return $allcaps;
}
add_filter( 'user_has_cap', 'wpagent_companion_ninjaforms_user_has_cap', 20, 4 );

/**
 * @return array{detected:bool,registered_count:int,available_count:int,registered_forms:string[],available_forms:string[],registered_submissions:string[],available_submissions:string[],registered_write:string[],available_write:string[],registered_delivery:string[],available_delivery:string[],quarantined_registered:string[]}
 */
function wpagent_companion_ninjaforms_integration_status(): array {
	$groups     = wpagent_companion_ninjaforms_ability_groups();
	$registered = array( 'forms' => array(), 'submissions' => array(), 'write' => array(), 'delivery' => array() );
	$available  = $registered;
	$all_count  = 0;
	$public     = 0;
	if ( function_exists( 'wp_get_abilities' ) ) {
		foreach ( wp_get_abilities() as $name => $ability ) {
			if ( is_string( $name ) && str_starts_with( $name, 'ninjaforms/' ) ) {
				++$all_count;
				if ( wpagent_companion_ability_is_mcp_public( $ability ) ) {
					++$public;
				}
			}
		}
	}
	foreach ( $groups as $group => $names ) {
		foreach ( $names as $name ) {
			$ability = wpagent_companion_get_registered_ability( $name );
			if ( ! $ability ) {
				continue;
			}
			$registered[ $group ][] = $name;
			if ( wpagent_companion_ability_is_mcp_public( $ability ) ) {
				$available[ $group ][] = $name;
			}
		}
	}
	$quarantined = array_values(
		array_filter(
			wpagent_companion_ninjaforms_quarantined_ability_names(),
			static fn( string $name ): bool => (bool) wpagent_companion_get_registered_ability( $name )
		)
	);
	return array(
		'detected'                 => $all_count > 0,
		'registered_count'         => $all_count,
		'available_count'          => $public,
		'registered_forms'         => $registered['forms'],
		'available_forms'          => $available['forms'],
		'registered_submissions'   => $registered['submissions'],
		'available_submissions'    => $available['submissions'],
		'registered_write'         => $registered['write'],
		'available_write'          => $available['write'],
		'registered_delivery'      => $registered['delivery'],
		'available_delivery'       => $available['delivery'],
		'quarantined_registered'   => $quarantined,
	);
}

/**
 * @param mixed[] $submitted Generisch ausgewählte externe Abilities.
 * @return string[]
 */
function wpagent_companion_merge_ninjaforms_trust( array $submitted, bool $enable_forms, bool $enable_submissions, bool $enable_write, bool $enable_delivery ): array {
	$catalog = wpagent_companion_external_ability_catalog();
	$known   = array_flip( wpagent_companion_ninjaforms_known_ability_names() );
	$trusted = array();
	foreach ( $submitted as $name ) {
		if ( ! is_string( $name ) ) {
			continue;
		}
		$name = sanitize_text_field( $name );
		if ( isset( $catalog[ $name ] ) && ! isset( $known[ $name ] ) ) {
			$trusted[] = $name;
		}
	}
	$groups = wpagent_companion_ninjaforms_ability_groups();
	foreach ( array( 'forms' => $enable_forms, 'submissions' => $enable_submissions, 'write' => $enable_write, 'delivery' => $enable_delivery ) as $group => $enabled ) {
		if ( ! $enabled ) {
			continue;
		}
		foreach ( $groups[ $group ] as $name ) {
			$ability = wpagent_companion_get_registered_ability( $name );
			if ( isset( $catalog[ $name ] ) && wpagent_companion_ability_is_mcp_public( $ability ) ) {
				$trusted[] = $name;
			}
		}
	}
	$trusted = array_values( array_unique( $trusted ) );
	sort( $trusted, SORT_STRING );
	return $trusted;
}
