<?php
/**
 * Hub — assembles the data the admin "Discovery Hub" screen renders: the live
 * envelope projected into a UI-friendly shape, the built-in adapters and whether
 * they're active, and the validation notices. Shared by the admin bootstrap and
 * the REST re-scan route so both see identical data.
 *
 * @package Agentimus
 */

namespace Agentimus\Discovery;

use Agentimus\Settings;

defined( 'ABSPATH' ) || exit;

final class Hub {

	/**
	 * Built-in adapters — first-party callers of the public registration hook.
	 * Each MUST expose a static info(): {id,title,available}.
	 *
	 * @var string[]
	 */
	const ADAPTERS = array(
		Adapters\RestApi::class,
		Adapters\AbilitiesApi::class,
	);

	/**
	 * Build the Discovery Hub payload.
	 *
	 * @param Settings $settings Settings store.
	 * @param Registry $registry Collector (collected as a side effect).
	 * @return array
	 */
	public static function data( Settings $settings, Registry $registry ) {
		$builder  = new Envelope( $settings, $registry );
		$envelope = $builder->build();
		// tools + mcp are not part of the public discovery.json core; pull them from
		// the builder for the admin screen and the mcp.json document.
		$surface  = $builder->mcp_surface();

		// The admin lists EVERY Resource (suppressed ones flagged, not dropped) so
		// the owner can re-enable them — unlike the served envelope, which excludes
		// them. apis[]/capabilities/counts below stay from the filtered envelope, so
		// they reflect what is actually published.
		$suppressed = $builder->suppressed_ids();
		$rows       = array_map(
			static function ( $resource ) use ( $suppressed ) {
				return self::resource_row( $resource, $suppressed );
			},
			$builder->all_resources()
		);

		$endpoints = array(
			'discovery' => home_url( '/.well-known/discovery.json' ),
			'agentCard' => home_url( '/.well-known/agent-card.json' ),
			'agentJson' => home_url( '/.well-known/agent.json' ),
			'mcp'       => home_url( '/.well-known/mcp.json' ),
			'rest'      => rest_url( 'agentimus/v1/discovery' ),
		);
		// The primary MCP server card is served only when a real server is detected
		// (it 404s otherwise), so surface it to the admin only when it's actually live.
		if ( ! empty( $surface['mcp']['available'] ) && ! empty( $surface['mcp']['servers'] ) ) {
			$endpoints['mcpServerCard'] = home_url( '/.well-known/mcp/server-card.json' );
		}

		return array(
			'endpoints'    => $endpoints,
			'site'         => $envelope['site'],
			'resources'    => $rows,
			'capabilities' => $envelope['capabilities'],
			'apis'         => $envelope['apis'],
			'agents'       => array_map(
				static function ( $agent ) {
					return array(
						'id'     => isset( $agent['id'] ) ? $agent['id'] : '',
						'name'   => isset( $agent['name'] ) ? $agent['name'] : '',
						'skills' => isset( $agent['skills'] ) ? count( (array) $agent['skills'] ) : 0,
					);
				},
				$envelope['agents']
			),
			'wellKnown'    => $envelope['well_known'],
			'tools'        => $surface['tools'],
			'mcp'          => $surface['mcp'],
			// The Abilities API's own REST listing — the OTHER door a signed-in agent can
			// call (every registered ability, not just the MCP-scoped ones). '' when the
			// Abilities API isn't available on this install.
			'abilitiesEndpoint' => self::abilities_endpoint(),
			'adapters'     => self::adapters(),
			'notices'      => $registry->notices(),
			'counts'       => array(
				'resources'    => count( $envelope['resources'] ),
				// The admin tile shows the REGISTERED number (matching the provider list
				// below it) with the published number as its qualifier — the owner was
				// reading "1 provider" against a 3-row list and rightly asking which lied.
				'resourcesRegistered' => count( $rows ),
				'suppressed'   => count(
					array_filter(
						$rows,
						static function ( $r ) {
							return ! empty( $r['suppressed'] );
						}
					)
				),
				'capabilities' => count( $envelope['capabilities'] ),
				'apis'         => count( $envelope['apis'] ),
				// `tools` counts what the site HAS, not what it anonymously publishes — this card is
				// the owner's inventory of their own site, and an AUTHORISED agent really can run all
				// of them. Counting only published tools would read 0 the moment abilities stopped
				// being advertised anonymously, which looks like they vanished. `toolsPublished` keeps
				// the published number available, and the Discovery screen flags which rows are held
				// back and why.
				'tools'        => array_sum(
					array_map(
						static function ( $r ) {
							return isset( $r['tools'] ) ? count( (array) $r['tools'] ) : 0;
						},
						$builder->all_resources()
					)
				),
				'toolsPublished' => count( $surface['tools'] ),
				'errors'       => count(
					array_filter(
						$registry->notices(),
						static function ( $n ) {
							return 'error' === $n['level'];
						}
					)
				),
			),
		);
	}

	/**
	 * Trim a full envelope resource to what the UI shows.
	 *
	 * @param array    $resource   Envelope resource.
	 * @param string[] $suppressed Owner-suppressed Resource ids.
	 * @return array
	 */
	private static function resource_row( $resource, array $suppressed = array() ) {
		$provider = isset( $resource['provider']['plugin'] ) ? $resource['provider']['plugin'] : '';
		$ours     = function_exists( 'plugin_basename' ) ? plugin_basename( AGENTIMUS_FILE ) : 'agentimus/agentimus.php';
		$auto     = ( '' !== $provider && $provider === $ours );
		// Which built-in engine found an auto resource — so the UI can show
		// "Found via the REST API / Abilities API" and link it to the engine status.
		// The AbilitiesApi adapter mints ids as `abilities-<ns>`; everything else
		// auto comes from the REST adapter (wordpress-core + namespace stubs).
		$engine = '';
		if ( $auto ) {
			$engine = ( 0 === strpos( (string) $resource['id'], 'abilities-' ) ) ? 'Abilities API' : 'REST API';
		}
		return array(
			'id'           => $resource['id'],
			'title'        => $resource['title'],
			'type'         => $resource['type'],
			'description'  => $resource['description'],
			'version'      => $resource['version'],
			'provider'     => $provider,
			// True when Agentimus's own adapter registered it (auto-discovery), not a third-party plugin declaring itself.
			'auto'         => $auto,
			'engine'       => $engine,
			// True when the owner has suppressed this Resource from served output.
			'suppressed'   => in_array( $resource['id'], $suppressed, true ),
			// True when the PROVIDER kept it out of the served documents because nobody anonymous
			// could use it anyway (see Resource::normalize()'s `public`, and AbilitiesApi). Distinct from
			// `suppressed`, which is the owner's own choice and is theirs to reverse.
			//
			// This flag is not decoration. Without it the admin lists a Resource with no indication
			// that no agent can see it — and a row shown without a caveat reads as "this is live".
			'notPublic'    => isset( $resource['public'] ) && ! $resource['public'],
			'capabilities' => $resource['capabilities'],
			'endpoints'    => array_map(
				static function ( $endpoint ) {
					return array(
						'url'  => $endpoint['url'],
						'type' => $endpoint['type'],
						'auth' => '' !== $endpoint['auth'] ? $endpoint['auth'] : 'none',
					);
				},
				$resource['endpoints']
			),
			'hasAgent'     => ! empty( $resource['agent'] ),
			'tools'        => count( $resource['tools'] ),
			'abilities'    => $resource['abilities'],
		);
	}

	/**
	 * The Abilities API's REST listing URL — '' when the API isn't on this install,
	 * so the admin never renders a dead endpoint.
	 *
	 * @return string
	 */
	private static function abilities_endpoint() {
		foreach ( self::adapters() as $adapter ) {
			if ( isset( $adapter['id'] ) && 'wp-abilities' === $adapter['id'] && ! empty( $adapter['available'] ) ) {
				return esc_url_raw( rest_url( 'wp-abilities/v1/abilities' ) );
			}
		}
		return '';
	}

	/**
	 * Built-in adapter descriptors.
	 *
	 * @return array[]
	 */
	private static function adapters() {
		$out = array();
		foreach ( self::ADAPTERS as $class ) {
			if ( is_callable( array( $class, 'info' ) ) ) {
				$out[] = call_user_func( array( $class, 'info' ) );
			}
		}
		return $out;
	}
}
