<?php
/**
 * Connector Interface
 *
 * Base contract that all AmbassadorHub connectors must implement.
 * Connectors are self-contained WordPress plugins that integrate
 * external services (PSPs, CRMs, marketing automation tools) with
 * the AmbassadorHub ecosystem via the ConnectorRegistry.
 *
 * @package AmbassadorHub\Connectors
 */

namespace AmbassadorHub\Connectors;

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

/**
 * Base interface for all AmbassadorHub connectors.
 *
 * Connectors self-register with the ConnectorRegistry on `plugins_loaded`.
 * They plug into the ambassador-core automations engine by registering
 * triggers (ambh_automation_triggers) and action types (ambh_automation_actions).
 * The automations engine calls them back via ambh_execute_action_{type} when
 * a rule fires.
 */
abstract class AbstractConnector implements ConnectorInterface {

	public const ID   = '';
	public const TYPE = '';
	public const LOGO = '';

	public function __construct() {

		if ( static::ID === '' || static::TYPE === '' ) {
			throw new \LogicException( 'Connector classes must define non-empty ID and TYPE constants.' );
		}

		if ( ! in_array( static::TYPE, array( 'psp', 'crm', 'marketing_automation' ), true ) ) {
			throw new \LogicException( 'Connector TYPE must be one of: psp, crm, marketing_automation.' );
		}
	}

	/**
	 * Initialize the connector module.
	 *
	 * Called from Plugin::load_components() after the ConnectorRegistry
	 * singleton is available (post ambh_loaded).
	 *
	 * @return void
	 */
	public static function init(): void {

		// Register the connector with the registry.
		ConnectorRegistry::register( new static() );
	}

	public static function register(): void {

		// Register settings (admin_init hook + settings tab).
		add_action( 'admin_init', array( static::class, 'register_settings' ) );
		add_filter( 'ambh_' . static::TYPE . '_settings_sections', array( static::class, 'register_settings_section' ) );
	}

	/**
	 * Whether this connector is available for use.
	 *
	 * Returns true only when the connector is fully configured and (if applicable)
	 * licensed / authenticated. The registry and the settings UI rely on this to
	 * determine which connectors are operational.
	 *
	 * Default implementation returns true. Override in connectors with configuration
	 * or licensing requirements if applicable.
	 *
	 * @return bool
	 */
	public static function is_available(): bool {
		return true;
	}

	/**
	 * Machine-readable connector ID.
	 *
	 * Must be unique across all registered connectors. Used as a slug in
	 * filter hook names (e.g. ambh_execute_action_sync_to_crm_{id}) and
	 * as the registry key.
	 *
	 * Examples: 'mollie', 'hubspot', 'mailchimp', 'campaignsuite'
	 *
	 * @return string
	 */
	public static function get_id(): string {
		return static::ID;
	}

	/**
	 * Connector type slug.
	 *
	 * Determines the registry slot this connector occupies. Recognised values:
	 * - 'psp'                  — payment service provider
	 * - 'crm'                  — customer relationship management
	 * - 'marketing_automation' — email / marketing automation
	 *
	 * @return string
	 */
	public static function get_type(): string {
		return static::TYPE;
	}

	/**
	 * Logo URL for this connector, shown in the settings UI.
	 *
	 * @return string
	 */
	public static function get_logo(): string {
		return static::LOGO;
	}

	/**
	 * Register the settings for the connector.
	 *
	 * @return void
	 */
	public static function register_settings(): void {

		$fields = static::get_settings_fields();

		if ( ! is_array( $fields ) || empty( $fields ) ) {
			return;
		}

		foreach ( $fields as $field ) {
			$id   = $field['id'] ?? null;
			$type = $field['type'] ?? 'string';

			if ( ! $id ) {
				// Skip invalid field definitions of description-only fields (which don't have an ID).
				continue;
			}

			$sanitize_callback = match ( $type ) {
				'text'     => 'sanitize_text_field',
				'password' => array( self::class, 'sanitize_secret' ),
				'email'    => 'sanitize_email',
				'url'      => 'esc_url_raw',
				'select'   => 'sanitize_key',
				'checkbox' => 'rest_sanitize_boolean',
				default    => 'sanitize_text_field',
			};

			register_setting(
				static::get_settings_group(),
				$id,
				array(
					'sanitize_callback' => $sanitize_callback,
					'type'              => in_array( $type, array( 'checkbox' ), true ) ? 'boolean' : 'string',
					'default'           => $field['default'] ?? ( in_array( $type, array( 'checkbox' ), true ) ? false : '' ),
				)
			);
		}
	}

	public static function register_settings_section( array $sections ): array {

		$sections[] = array(
			'connector_id' => static::ID,
			'render'       => array( static::class, 'render_settings' ),
		);

		return $sections;
	}

	/**
	 * Settings fields definition for the Integrations admin tab.
	 *
	 * Returns an array of field descriptors compatible with the ambassador-core
	 * settings renderer. Each descriptor is an associative array with at minimum:
	 * - 'id'    string  Option key (stored in wp_options)
	 * - 'type'  string  Field type: 'text', 'password', 'select', 'checkbox', …
	 * - 'label' string  Human-readable field label
	 *
	 * Return an empty array if the connector has no configurable settings.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public static function get_settings_fields(): array {
		return array();
	}

	/**
	 * Called by the Privacy eraser when a GDPR erasure request is processed.
	 *
	 * PSP connectors (Mollie, PayPal, Stripe) do not store personal data in
	 * the provider; they only keep transaction IDs. Override in CRM and
	 * marketing automation connectors to delete or anonymise the contact.
	 *
	 * @param int    $entity_id   WordPress user ID ('user') or donor table ID ('donor').
	 * @param string $entity_type 'user' (ambassador WP account) or 'donor' (payment donor).
	 *
	 * @return void
	 */
	public static function on_erasure( int $entity_id, string $entity_type ): void {
		// No-op default — override in CRM/marketing connectors.
	}

	protected static function get_settings_group(): string {
		return static::ID . '_settings';
	}

	public static function render_settings() {

		if ( ! is_array( static::get_settings_fields() ) || empty( static::get_settings_fields() ) ) {
			return;
		}

		?>

		<form method="post" action="options.php">
			<?php settings_fields( static::get_settings_group() ); ?>

			<table class="form-table" role="presentation">
				<tbody>

					<?php
					foreach ( static::get_settings_fields() as $field ) {
						$type        = $field['type'] ?? 'text';
						$id          = $field['id'] ?? '';
						$title       = $field['title'] ?? '';
						$label       = $field['label'] ?? '';
						$description = $field['description'] ?? '';
						$text        = $field['text'] ?? '';

						switch ( $type ) {
							case 'text':
							case 'email':
							case 'url':
								\AmbassadorHub\Admin\Settings\Fields::text( $id, $title, $description, $type );
								break;
							case 'password':
								\AmbassadorHub\Admin\Settings\Fields::password( $id, $title, $description );
								break;
							case 'select':
								$options = $field['options'] ?? array();
								\AmbassadorHub\Admin\Settings\Fields::select( $id, $title, $options, $description );
								break;
							case 'checkbox':
								\AmbassadorHub\Admin\Settings\Fields::checkbox( $id, $title, $label, $description );
								break;
							case 'description':
								\AmbassadorHub\Admin\Settings\Fields::description( $text );
								break;
							default:
								break;
						}
					}
					?>

				</tbody>
			</table>
			<?php submit_button(); ?>
		</form>

		<?php
	}

	/**
	 * Sanitize a password/secret settings field (API keys, webhook signing secrets).
	 *
	 * Deliberately not sanitize_text_field(): that function strips anything
	 * from a stray "<" onward, treating it as an HTML tag start — for an
	 * opaque, auto-generated token that would silently truncate the secret
	 * with no error, and the resulting auth/signature failures would be very
	 * hard to trace back to this. These fields are never rendered as HTML
	 * (the settings UI always masks/re-renders them as a password input), so
	 * there's no HTML-safety need to satisfy either.
	 *
	 * @param mixed $value Raw submitted value.
	 *
	 * @return string
	 */
	public static function sanitize_secret( $value ): string {
		return is_string( $value ) ? trim( $value ) : '';
	}
}
