<?php
/**
 * Integration Storage
 *
 * Handles secure storage of integration credentials with multi-account support.
 *
 * @package Sequensy
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy\Integrations;

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

/**
 * Class IntegrationStorage
 *
 * Manages integration credentials storage with encryption.
 * Supports multiple connections (accounts) per integration type.
 */
class IntegrationStorage {

	/**
	 * Option prefix for integration connections.
	 *
	 * @var string
	 */
	public const CONNECTIONS_PREFIX = 'sequensy_integration_connections_';

	/**
	 * Registered integrations with their metadata.
	 *
	 * @var array<string, array>
	 */
	private static array $integrations = array();

	/**
	 * Map of integration slugs to their class names.
	 *
	 * @var array<string, string>
	 */
	private static array $integration_classes = array();

	/**
	 * Registered API factory closures keyed by integration slug.
	 *
	 * @var array<string, callable>
	 */
	private static array $api_factories = array();

	/**
	 * Register an integration.
	 *
	 * @param string $slug        Integration slug (e.g., 'campaignmonitor').
	 * @param array  $integration Integration metadata including:
	 *                            - name: Display name
	 *                            - description: Short description
	 *                            - category: Category for grouping
	 *                            - credentials: Array of credential field definitions.
	 * @return void
	 */
	public static function register_integration( string $slug, array $integration ): void {
		self::$integrations[ $slug ] = $integration;
	}

	/**
	 * Register an integration class for lookup.
	 *
	 * @param string $slug       Integration slug.
	 * @param string $class_name Fully qualified class name.
	 * @return void
	 */
	public static function register_integration_class( string $slug, string $class_name ): void {
		self::$integration_classes[ $slug ] = $class_name;
	}

	/**
	 * Get the integration class name by slug.
	 *
	 * @param string $slug Integration slug.
	 * @return string|null Class name or null if not found.
	 */
	public static function get_integration_class( string $slug ): ?string {
		return self::$integration_classes[ $slug ] ?? null;
	}

	/**
	 * Register an API factory closure for an integration.
	 *
	 * Array-based packages provide an api_factory closure in package.php.
	 * This closure receives credentials array and returns an API client.
	 *
	 * @param string   $slug    Integration slug (e.g., 'campaignmonitor').
	 * @param callable $factory Callable that receives (array $credentials) and returns an API client.
	 * @return void
	 */
	public static function register_api_factory( string $slug, callable $factory ): void {
		self::$api_factories[ $slug ] = $factory;
	}

	/**
	 * Get the API factory for an integration.
	 *
	 * @param string $slug Integration slug.
	 * @return callable|null Factory callable or null if not registered.
	 */
	public static function get_api_factory( string $slug ): ?callable {
		return self::$api_factories[ $slug ] ?? null;
	}

	/**
	 * Get all registered integrations with their connection counts.
	 *
	 * @return array Array of integrations.
	 */
	public static function get_integrations(): array {
		$result = array();

		foreach ( self::$integrations as $slug => $integration ) {
			$connections = self::get_connections( $slug );
			$result[]    = array_merge(
				$integration,
				array(
					'slug'        => $slug,
					'connected'   => count( $connections ) > 0,
					'connections' => $connections,
				)
			);
		}

		return $result;
	}

	/**
	 * Get a specific integration's metadata.
	 *
	 * @param string $slug Integration slug.
	 * @return array|null Integration metadata or null if not found.
	 */
	public static function get_integration( string $slug ): ?array {
		if ( ! isset( self::$integrations[ $slug ] ) ) {
			return null;
		}

		$connections = self::get_connections( $slug );

		return array_merge(
			self::$integrations[ $slug ],
			array(
				'slug'        => $slug,
				'connected'   => count( $connections ) > 0,
				'connections' => $connections,
			)
		);
	}

	/**
	 * Check if an integration has at least one connection.
	 *
	 * @param string $slug Integration slug.
	 * @return bool Whether the integration has any connections.
	 */
	public static function is_connected( string $slug ): bool {
		$connections = self::get_connections( $slug );
		return count( $connections ) > 0;
	}

	// =========================================================================
	// Connection Management (Multi-Account Support)
	// =========================================================================

	/**
	 * Generate a unique connection ID.
	 *
	 * @return string Unique connection ID prefixed with 'conn_'.
	 */
	private static function generate_connection_id(): string {
		return 'conn_' . wp_generate_uuid4();
	}

	/**
	 * Create a new connection for an integration.
	 *
	 * @param string $slug        Integration slug.
	 * @param string $label       User-defined label for this connection.
	 * @param array  $credentials Credentials to save.
	 * @param string $api_version API version for this connection (e.g., 'v1', 'v2').
	 * @return string|\WP_Error Connection ID on success, WP_Error on failure.
	 */
	public static function create_connection(
		string $slug,
		string $label,
		array $credentials,
		string $api_version = 'v1'
	): string|\WP_Error {
		// Check if creating another connection is allowed.
		$connections   = self::get_connections_raw( $slug );
		$current_count = count( $connections );

		/**
		 * Filter to check if creating a new connection is allowed.
		 * Extensions can hook here to allow unlimited accounts.
		 *
		 * @param bool   $allow_creation Default behavior (limit to 1)
		 * @param string $slug           Integration slug
		 * @param int    $current_count  Number of existing connections
		 */
		$allow_creation = apply_filters(
			'sequensy_allow_create_connection',
			$current_count < 1, // Free default: max 1 account
			$slug,
			$current_count
		);

		if ( ! $allow_creation ) {
			return new \WP_Error(
				'multi_account_limit_reached',
				__( 'Free version limited to one account per integration.', 'sequensy' ),
				array( 'status' => 403 )
			);
		}

		// Check for duplicate label.
		if ( self::label_exists( $slug, $label ) ) {
			return new \WP_Error(
				'duplicate_label',
				__( 'A connection with this label already exists.', 'sequensy' ),
				array( 'status' => 400 )
			);
		}

		$connection_id = self::generate_connection_id();
		$encrypted     = CredentialEncryption::encrypt( $credentials );

		if ( false === $encrypted ) {
			return new \WP_Error(
				'encryption_failed',
				__( 'Failed to encrypt credentials.', 'sequensy' ),
				array( 'status' => 500 )
			);
		}

		$connections[] = array(
			'id'          => $connection_id,
			'label'       => sanitize_text_field( $label ),
			'credentials' => $encrypted,
			'api_version' => sanitize_text_field( $api_version ),
			'created_at'  => current_time( 'mysql' ),
			'updated_at'  => current_time( 'mysql' ),
		);

		$saved = update_option( self::CONNECTIONS_PREFIX . $slug, $connections );

		if ( ! $saved ) {
			return new \WP_Error(
				'save_failed',
				__( 'Failed to save connection.', 'sequensy' ),
				array( 'status' => 500 )
			);
		}

		return $connection_id;
	}

	/**
	 * Update an existing connection.
	 *
	 * @param string $slug          Integration slug.
	 * @param string $connection_id Connection ID.
	 * @param array  $data          Data to update (label, api_version, and/or credentials).
	 * @return bool|\WP_Error True on success, WP_Error on failure.
	 */
	public static function update_connection( string $slug, string $connection_id, array $data ): bool|\WP_Error {
		// Check for duplicate label if updating label.
		if ( isset( $data['label'] ) && self::label_exists( $slug, $data['label'], $connection_id ) ) {
			return new \WP_Error(
				'duplicate_label',
				__( 'A connection with this label already exists.', 'sequensy' ),
				array( 'status' => 400 )
			);
		}

		$connections = self::get_connections_raw( $slug );
		$found       = false;

		foreach ( $connections as $index => $connection ) {
			if ( $connection['id'] === $connection_id ) {
				if ( isset( $data['label'] ) ) {
					$connections[ $index ]['label'] = sanitize_text_field( $data['label'] );
				}

				if ( isset( $data['api_version'] ) ) {
					$connections[ $index ]['api_version'] = sanitize_text_field( $data['api_version'] );
				}

				if ( isset( $data['credentials'] ) ) {
					$encrypted = CredentialEncryption::encrypt( $data['credentials'] );
					if ( false === $encrypted ) {
						return new \WP_Error(
							'encryption_failed',
							__( 'Failed to encrypt credentials.', 'sequensy' ),
							array( 'status' => 500 )
						);
					}
					$connections[ $index ]['credentials'] = $encrypted;
				}

				$connections[ $index ]['updated_at'] = current_time( 'mysql' );
				$found                               = true;
				break;
			}
		}

		if ( ! $found ) {
			return new \WP_Error(
				'not_found',
				__( 'Connection not found.', 'sequensy' ),
				array( 'status' => 404 )
			);
		}

		return update_option( self::CONNECTIONS_PREFIX . $slug, $connections );
	}

	/**
	 * Delete a connection.
	 *
	 * @param string $slug          Integration slug.
	 * @param string $connection_id Connection ID to delete.
	 * @return bool Whether the deletion was successful.
	 */
	public static function delete_connection( string $slug, string $connection_id ): bool {
		$connections = self::get_connections_raw( $slug );
		$original    = count( $connections );

		$connections = array_values(
			array_filter(
				$connections,
				fn( $conn ) => $conn['id'] !== $connection_id
			)
		);

		if ( count( $connections ) === $original ) {
			return false; // Connection not found.
		}

		if ( empty( $connections ) ) {
			delete_option( self::CONNECTIONS_PREFIX . $slug );
			return true; // Connection removed, regardless of whether option existed.
		}

		return update_option( self::CONNECTIONS_PREFIX . $slug, $connections );
	}

	/**
	 * Get a specific connection by ID.
	 *
	 * @param string $slug          Integration slug.
	 * @param string $connection_id Connection ID.
	 * @return array|null Connection data (without decrypted credentials) or null.
	 */
	public static function get_connection( string $slug, string $connection_id ): ?array {
		$connections = self::get_connections_raw( $slug );

		foreach ( $connections as $connection ) {
			if ( $connection['id'] === $connection_id ) {
				return array(
					'id'          => $connection['id'],
					'label'       => $connection['label'],
					'api_version' => $connection['api_version'] ?? 'v1',
					'created_at'  => $connection['created_at'],
					'updated_at'  => $connection['updated_at'],
				);
			}
		}

		return null;
	}

	/**
	 * Get all connections for an integration (without credentials).
	 *
	 * @param string $slug Integration slug.
	 * @return array Array of connections (without credentials).
	 */
	public static function get_connections( string $slug ): array {
		$connections = self::get_connections_raw( $slug );

		return array_map(
			fn( $conn ) => array(
				'id'          => $conn['id'],
				'label'       => $conn['label'],
				'api_version' => $conn['api_version'] ?? 'v1',
				'created_at'  => $conn['created_at'],
				'updated_at'  => $conn['updated_at'],
			),
			$connections
		);
	}

	/**
	 * Get raw connections data from storage (with encrypted credentials).
	 *
	 * @param string $slug Integration slug.
	 * @return array Array of raw connection data.
	 */
	private static function get_connections_raw( string $slug ): array {
		$connections = get_option( self::CONNECTIONS_PREFIX . $slug, array() );
		return is_array( $connections ) ? $connections : array();
	}

	/**
	 * Get decrypted credentials for a specific connection.
	 *
	 * @param string $slug          Integration slug.
	 * @param string $connection_id Connection ID.
	 * @return array|null Decrypted credentials array or null if not found.
	 */
	public static function get_connection_credentials( string $slug, string $connection_id ): ?array {
		$connections = self::get_connections_raw( $slug );

		foreach ( $connections as $connection ) {
			if ( $connection['id'] === $connection_id ) {
				$decrypted = CredentialEncryption::decrypt( $connection['credentials'] );
				if ( false === $decrypted ) {
					return null;
				}
				return $decrypted;
			}
		}

		return null;
	}

	/**
	 * Get masked credentials for a specific connection (for display).
	 *
	 * @param string $slug          Integration slug.
	 * @param string $connection_id Connection ID.
	 * @return array|null Connection info with masked credentials or null.
	 */
	public static function get_masked_connection( string $slug, string $connection_id ): ?array {
		$connections = self::get_connections_raw( $slug );

		foreach ( $connections as $connection ) {
			if ( $connection['id'] === $connection_id ) {
				$decrypted = CredentialEncryption::decrypt( $connection['credentials'] );
				if ( false === $decrypted ) {
					return null;
				}

				$masked = array();
				foreach ( $decrypted as $key => $value ) {
					if ( is_string( $value ) && strlen( $value ) > 4 ) {
						$masked[ $key ] = str_repeat( '*', strlen( $value ) - 4 ) . substr( $value, -4 );
					} else {
						$masked[ $key ] = '****';
					}
				}

				return array(
					'id'          => $connection['id'],
					'label'       => $connection['label'],
					'credentials' => $masked,
					'api_version' => $connection['api_version'] ?? 'v1',
					'created_at'  => $connection['created_at'],
					'updated_at'  => $connection['updated_at'],
				);
			}
		}

		return null;
	}

	/**
	 * Check if a connection label already exists for an integration.
	 *
	 * Comparison is case-insensitive and whitespace is trimmed.
	 *
	 * @param string      $slug       Integration slug.
	 * @param string      $label      Label to check.
	 * @param string|null $exclude_id Connection ID to exclude (for updates).
	 * @return bool Whether the label already exists.
	 */
	public static function label_exists( string $slug, string $label, ?string $exclude_id = null ): bool {
		$connections = self::get_connections( $slug );
		$label_lower = strtolower( trim( $label ) );

		foreach ( $connections as $connection ) {
			if ( $exclude_id && $connection['id'] === $exclude_id ) {
				continue;
			}
			if ( strtolower( $connection['label'] ) === $label_lower ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if an integration has exactly one connection.
	 *
	 * Useful for auto-selecting when only one account exists.
	 *
	 * @param string $slug Integration slug.
	 * @return bool Whether exactly one connection exists.
	 */
	public static function has_single_connection( string $slug ): bool {
		$connections = self::get_connections( $slug );
		return count( $connections ) === 1;
	}

	/**
	 * Get the first (and possibly only) connection for an integration.
	 *
	 * Useful for auto-selection when only one connection exists.
	 *
	 * @param string $slug Integration slug.
	 * @return array|null First connection or null if none exist.
	 */
	public static function get_first_connection( string $slug ): ?array {
		$connections = self::get_connections( $slug );
		return $connections[0] ?? null;
	}

	/**
	 * Get the API version for a specific connection.
	 *
	 * @param string $slug          Integration slug.
	 * @param string $connection_id Connection ID.
	 * @return string|null API version or null if connection not found.
	 */
	public static function get_connection_version( string $slug, string $connection_id ): ?string {
		$connection = self::get_connection( $slug, $connection_id );
		return $connection['api_version'] ?? null;
	}
}
