<?php
/**
 * Metadata REST API Controller
 *
 * Provides REST API endpoints for triggers, actions, and system info.
 *
 * @package Sequensy
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy\Rest;

use Sequensy\Container;
use Sequensy\Triggers\TriggerRegistry;
use Sequensy\Actions\ActionRegistry;
use Sequensy\Integrations\IntegrationMetadataRegistry;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class MetadataController
 *
 * REST API controller for triggers, actions, and system info metadata.
 */
class MetadataController extends WP_REST_Controller {

	use RestControllerTrait;

	/**
	 * Trigger registry instance.
	 *
	 * @var TriggerRegistry
	 */
	private TriggerRegistry $trigger_registry;

	/**
	 * Action registry instance.
	 *
	 * @var ActionRegistry
	 */
	private ActionRegistry $action_registry;

	/**
	 * Factory method for creating controller instance with container.
	 *
	 * @param Container $container Service container instance.
	 * @return self Controller instance.
	 */
	public static function create( Container $container ): self {
		return new self(
			$container->get( 'trigger_registry' ),
			$container->get( 'action_registry' )
		);
	}

	/**
	 * Constructor.
	 *
	 * @param TriggerRegistry|null $trigger_registry Optional trigger registry.
	 * @param ActionRegistry|null  $action_registry  Optional action registry.
	 */
	public function __construct(
		?TriggerRegistry $trigger_registry = null,
		?ActionRegistry $action_registry = null
	) {
		$this->namespace = 'sequensy/v1';

		// Use provided dependencies or get from container.
		$container              = Container::getInstance();
		$this->trigger_registry = $trigger_registry ?? $container->get( 'trigger_registry' );
		$this->action_registry  = $action_registry ?? $container->get( 'action_registry' );
	}

	/**
	 * Register REST API routes.
	 */
	public function register_routes(): void {
		// Get system information.
		register_rest_route(
			$this->namespace,
			'/system/info',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_system_info' ),
				'permission_callback' => array( $this, 'permissions_check' ),
			)
		);

		// Get available triggers.
		register_rest_route(
			$this->namespace,
			'/triggers',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_triggers' ),
				'permission_callback' => array( $this, 'permissions_check' ),
			)
		);

		// Get available actions.
		register_rest_route(
			$this->namespace,
			'/actions',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_actions' ),
				'permission_callback' => array( $this, 'permissions_check' ),
			)
		);
	}

	/**
	 * Get system information including Action Scheduler version.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function get_system_info( WP_REST_Request $request ): WP_REST_Response {
		$as_version = 'Unknown';

		// Get Action Scheduler version.
		if ( class_exists( 'ActionScheduler_Versions' ) ) {
			$as_version = \ActionScheduler_Versions::instance()->latest_version();
		}

		$info = array(
			'action_scheduler_version' => $as_version,
			'plugin_version'           => SEQUENSY_VERSION,
			'wordpress_version'        => get_bloginfo( 'version' ),
			'php_version'              => phpversion(),
		);

		return rest_ensure_response( $info );
	}

	/**
	 * Get available triggers with metadata.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function get_triggers( WP_REST_Request $request ): WP_REST_Response {
		// Check for cached triggers.
		$cached = get_transient( 'sequensy_trigger_metadata' );
		if ( false !== $cached ) {
			return rest_ensure_response( $cached );
		}

		// Build trigger metadata array from both sources:
		// 1. Registered triggers (from trigger registry - fully initialized integrations)
		// 2. Trigger definitions (from metadata registry - all integrations including uninitialized)
		$triggers_data = array();
		$seen_types    = array();

		// First, get triggers from the trigger registry (fully registered handlers).
		$triggers = $this->trigger_registry->get_all_triggers();

		foreach ( $triggers as $trigger ) {
			$trigger_type                = $trigger->get_type();
			$seen_types[ $trigger_type ] = true;

			$trigger_data = array(
				'type'        => $trigger_type,
				'label'       => $trigger->get_label(),
				'description' => $trigger->get_description(),
			);

			// Get available fields if method exists.
			if ( method_exists( $trigger, 'get_available_fields' ) ) {
				$trigger_data['available_fields'] = $trigger->get_available_fields();
			} else {
				$trigger_data['available_fields'] = array();
			}

			// Get integration identifier if method exists.
			if ( method_exists( $trigger, 'get_integration' ) ) {
				$trigger_data['integration'] = $trigger->get_integration() ?? 'WordPress';
			} else {
				$trigger_data['integration'] = 'WordPress';
			}

			$trigger_data['is_available']        = true;
			$trigger_data['availability_reason'] = '';
			$trigger_data['plugin_type']         = '';
			$trigger_data['install_url']         = '';

			$triggers_data[] = $trigger_data;
		}

		// Second, get trigger definitions from metadata registry for integrations not yet initialized.
		$all_integrations = IntegrationMetadataRegistry::get_all();

		foreach ( $all_integrations as $slug => $integration_data ) {
			$trigger_definitions = $integration_data['triggers'] ?? array();

			foreach ( $trigger_definitions as $definition ) {
				$trigger_type = $definition->type ?? '';
				if ( isset( $seen_types[ $trigger_type ] ) || empty( $trigger_type ) ) {
					continue;
				}

				$triggers_data[] = array(
					'type'                => $trigger_type,
					'label'               => $definition->label ?? '',
					'description'         => $definition->description ?? '',
					'integration'         => $definition->integration ?? $slug,
					'available_fields'    => $definition->available_fields ?? array(),
					'is_available'        => true,
					'availability_reason' => '',
					'plugin_type'         => '',
					'install_url'         => '',
				);
			}
		}

		// Sort triggers alphabetically by label.
		usort(
			$triggers_data,
			function ( $a, $b ) {
				return strcmp( $a['label'], $b['label'] );
			}
		);

		/**
		 * Filter trigger metadata before caching and returning.
		 *
		 * Allows Pro or extensions to modify trigger metadata (e.g., add pro_only flags).
		 *
		 * @since 2.1.0
		 *
		 * @param array $triggers_data Array of trigger metadata.
		 */
		$triggers_data = apply_filters( 'sequensy_trigger_metadata', $triggers_data );

		// Cache the result for 1 hour.
		set_transient( 'sequensy_trigger_metadata', $triggers_data, HOUR_IN_SECONDS );

		return rest_ensure_response( $triggers_data );
	}

	/**
	 * Get available actions with metadata.
	 *
	 * Returns metadata for all action handlers including type, label,
	 * description, configuration schema, output fields, and connection status.
	 * Actions from ALL integrations are returned, even those not yet connected.
	 * Response is cached for 1 hour to optimize performance.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object with array of action metadata.
	 */
	public function get_actions( WP_REST_Request $request ): WP_REST_Response {
		// Check for cached actions.
		$cached = get_transient( 'sequensy_action_metadata' );
		if ( false !== $cached ) {
			return rest_ensure_response( $cached );
		}

		// Build action metadata array from both sources:
		// 1. Registered actions (from action registry - fully initialized integrations)
		// 2. Action definitions (from metadata registry - all integrations including unconnected)
		$actions_data = array();
		$seen_types   = array();

		// First, get actions from the action registry (fully registered handlers).
		$actions = $this->action_registry->get_all_actions();

		foreach ( $actions as $action ) {
			$action_type                = $action->get_type();
			$seen_types[ $action_type ] = true;

			$action_data = array(
				'type'        => $action_type,
				'label'       => $action->get_label(),
				'description' => $action->get_description(),
			);

			// Get integration identifier if method exists.
			if ( method_exists( $action, 'get_integration' ) ) {
				$action_data['integration'] = $action->get_integration();
			} else {
				$action_data['integration'] = 'WordPress';
			}

			// Get configuration schema if method exists.
			if ( method_exists( $action, 'get_schema' ) ) {
				$action_data['schema'] = $this->normalize_schema( $action->get_schema() );
			} else {
				$action_data['schema'] = new \stdClass();
			}

			// Get output fields (required by ActionHandler interface).
			$action_data['output_fields'] = $action->get_output_fields();

			$action_data['is_available']        = true;
			$action_data['availability_reason'] = '';
			$action_data['plugin_type']         = '';
			$action_data['install_url']         = '';

			// Add connection status for third-party integrations.
			$action_data['is_connected']        = $this->is_integration_connected( $action_data['integration'] );
			$action_data['requires_connection'] = $this->integration_requires_connection( $action_data['integration'] );

			$actions_data[] = $action_data;
		}

		// Second, get action definitions from metadata registry for integrations not yet initialized.
		$all_integrations = IntegrationMetadataRegistry::get_all();

		foreach ( $all_integrations as $slug => $integration_data ) {
			$action_definitions = $integration_data['actions'] ?? array();

			foreach ( $action_definitions as $definition ) {
				// Skip if we already have this action from the registry.
				$action_type = $definition->type ?? '';
				if ( isset( $seen_types[ $action_type ] ) || empty( $action_type ) ) {
					continue;
				}

				$action_data = array(
					'type'          => $action_type,
					'label'         => $definition->label ?? '',
					'description'   => $definition->description ?? '',
					'integration'   => $definition->integration ?? $slug,
					'schema'        => $this->normalize_schema( $definition->schema ?? array() ),
					'output_fields' => $definition->output_fields ?? array(),
				);

				$action_data['is_available']        = true;
				$action_data['availability_reason'] = '';
				$action_data['plugin_type']         = '';
				$action_data['install_url']         = '';

				// Add connection status for third-party integrations.
				$action_data['is_connected']        = $this->is_integration_connected( $action_data['integration'] );
				$action_data['requires_connection'] = $this->integration_requires_connection(
					$action_data['integration']
				);

				$actions_data[] = $action_data;
			}
		}

		// Sort actions alphabetically by label.
		usort(
			$actions_data,
			function ( $a, $b ) {
				return strcmp( $a['label'], $b['label'] );
			}
		);

		/**
		 * Filter action metadata before caching and returning.
		 *
		 * Allows Pro or extensions to modify action metadata (e.g., add pro_only flags).
		 *
		 * @since 2.1.0
		 *
		 * @param array $actions_data Array of action metadata.
		 */
		$actions_data = apply_filters( 'sequensy_action_metadata', $actions_data );

		// Cache the result for 1 hour.
		set_transient( 'sequensy_action_metadata', $actions_data, HOUR_IN_SECONDS );

		return rest_ensure_response( $actions_data );
	}

	/**
	 * Normalize an indexed schema array into a keyed object.
	 *
	 * Converts array format `[{name: 'field1', ...}, ...]` to keyed format
	 * `{field1: {...}, ...}` expected by the frontend ActionSchema type.
	 *
	 * If the schema is already keyed (non-sequential string keys), it is returned as-is.
	 *
	 * @param array $schema Schema array to normalize.
	 * @return array|\stdClass Keyed schema object, or empty stdClass for empty schemas.
	 */
	private function normalize_schema( array $schema ): array|\stdClass {
		if ( empty( $schema ) ) {
			return new \stdClass();
		}

		// Check if already keyed (first key is a non-numeric string).
		$first_key = array_key_first( $schema );
		if ( is_string( $first_key ) && ! is_numeric( $first_key ) ) {
			return $schema;
		}

		// Convert indexed array to keyed object.
		$keyed = array();
		foreach ( $schema as $field ) {
			if ( isset( $field['name'] ) ) {
				$name = $field['name'];
				unset( $field['name'] );
				$keyed[ $name ] = $field;
			}
		}

		return ! empty( $keyed ) ? $keyed : new \stdClass();
	}
}
