<?php
/**
 * Integration Utilities
 *
 * Common utility functions for native integrations to reduce code duplication.
 *
 * @package SureForms
 * @since 1.13.0
 */

namespace SRFM_Pro\Inc\Pro\Native_Integrations\Utils;

use SRFM\Inc\Helper;
use SRFM_Pro\Inc\Pro\Database\Tables\Integrations;
use SRFM_Pro\Inc\Pro\Native_Integrations\OAuth_Handler;
use SRFM_Pro\Inc\Pro\Native_Integrations\Provider_Factory;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Integration Utilities class.
 *
 * @since 1.13.0
 */
class Integration_Utils {
	/**
	 * Universal placeholder replacement with special processing
	 *
	 * Consolidates replace_placeholders, replace_credential_placeholders, and process_template_variables
	 * into a single implementation that handles all cases.
	 *
	 * @param string $template Template string with placeholders like {{variable}}.
	 * @param array  $replacements Replacement values.
	 * @return string Processed string.
	 * @since 1.13.0
	 */
	public static function replace_placeholders( $template, $replacements ) {
		// Ensure template is a string.
		$template = Helper::get_string_value( $template );

		// Handle special cases and regular placeholders.
		$processed = preg_replace_callback(
			'/\{\{([^}]+)\}\}/',
			static function ( $matches ) use ( $replacements, $template ) {
				$placeholder = trim( $matches[1] );

				// Handle special case for base64 encoding.
				if ( preg_match( '/^base64\(([^)]+)\)$/', $placeholder, $base64_matches ) ) {
					$expression = $base64_matches[1];

					// Handle username:password pattern.
					if ( false !== strpos( $expression, ':' ) ) {
						$parts    = explode( ':', $expression, 2 );
						$username = $replacements[ trim( $parts[0] ) ] ?? '';
						$password = $replacements[ trim( $parts[1] ) ] ?? '';
						// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Used for HTTP Basic Auth encoding, not obfuscation.
						return base64_encode( $username . ':' . $password );
					}
				}

				// Handle special case for Canada domain.
				if ( 'domain' === $placeholder && isset( $replacements['domain'] ) && '.zohocloud.ca' === $replacements['domain'] ) {
					// Return appropriate domain based on the URL context.
					// For accounts URLs (auth/token), use zohocloud.ca.
					if ( false !== strpos( $template, 'accounts.zoho{{domain}}' ) ) {
						return 'cloud.ca';
					}

					// For API URLs, use .ca.
					if ( false !== strpos( $template, 'zohoapis{{domain}}' ) ) {
						return '.ca';
					}
				}

				// Regular placeholder replacement.
				return $replacements[ $placeholder ] ?? $matches[0];
			},
			$template
		);

		return Helper::get_string_value( $processed );
	}

	/**
	 * Get nested value from array using dot notation
	 *
	 * @param array  $data Data array.
	 * @param string $path Dot notation path (e.g., 'data.fields').
	 * @return mixed Value at path or null if not found.
	 * @since 1.13.0
	 */
	public static function get_nested_value( $data, $path ) {
		if ( ! is_array( $data ) ) {
			return null;
		}

		// Handle empty path - return the data as-is for direct arrays.
		if ( empty( $path ) ) {
			return $data;
		}

		$keys  = explode( '.', $path );
		$value = $data;

		foreach ( $keys as $key ) {
			// Handle array indices (e.g., "0", "1", etc.).
			if ( is_numeric( $key ) ) {
				$key = (int) $key;
			}

			if ( is_array( $value ) && array_key_exists( $key, $value ) ) {
				$value = $value[ $key ];
			} else {
				return null;
			}
		}

		return $value;
	}

	/**
	 * Set nested value in array using dot notation path
	 *
	 * @param array  $data Array to modify.
	 * @param string $path Dot notation path (e.g., 'merge_fields.BIRTHDAY').
	 * @param mixed  $value Value to set.
	 * @return array Modified array.
	 * @since 1.13.0
	 */
	public static function set_nested_value( $data, $path, $value ) {
		if ( ! is_array( $data ) ) {
			$data = [];
		}

		$keys    = explode( '.', $path );
		$current = &$data;
		$length  = count( $keys );

		// Navigate to the parent of the target key.
		for ( $i = 0; $i < $length - 1; $i++ ) {
			$key = $keys[ $i ];

			if ( ! isset( $current[ $key ] ) || ! is_array( $current[ $key ] ) ) {
				$current[ $key ] = [];
			}

			$current = &$current[ $key ];
		}

		// Set the final value.
		$final_key             = $keys[ $length - 1 ];
		$current[ $final_key ] = $value;

		return $data;
	}

	/**
	 * Build authentication headers based on integration configuration
	 *
	 * Consolidates authentication header building logic from connection-tester and workflow-processor.
	 *
	 * @param array $credentials User credentials.
	 * @param array $integration_config Full integration configuration.
	 * @param array $options Additional options for header building.
	 *                      - 'default_headers': Array of default headers to include.
	 *                      - 'endpoint_headers': Array of endpoint-specific headers.
	 * @return array Headers array.
	 * @since 1.13.0
	 */
	public static function build_auth_headers( $credentials, $integration_config, $options = [] ) {
		// Set default headers.
		$default_headers = $options['default_headers'] ?? [
			'User-Agent' => 'SureForms/' . SRFM_PRO_VER,
		];

		$headers = $default_headers;

		// Process integration-level headers from auth config (these may contain template variables).
		$config_headers = $integration_config['auth']['headers'] ?? [];
		foreach ( $config_headers as $key => $value ) {
			$headers[ $key ] = self::replace_placeholders( $value, $credentials );
		}

		// Process endpoint-specific headers (they may contain template variables).
		$endpoint_headers = $options['endpoint_headers'] ?? [];
		foreach ( $endpoint_headers as $key => $value ) {
			$headers[ $key ] = self::replace_placeholders( $value, $credentials );
		}

		// Add authentication headers based on integration type.
		$auth_config = $integration_config['auth'] ?? [];
		if ( ! empty( $auth_config ) ) {
			$auth_type = $auth_config['type'] ?? '';

			switch ( $auth_type ) {
				case 'api_key':
					if ( ! empty( $credentials['api_key'] ) ) {
						$header_name = $auth_config['header'] ?? 'Authorization';
						$prefix      = $auth_config['prefix'] ?? 'Bearer ';
						// Only set if not already set by config or endpoint headers.
						if ( ! isset( $headers[ $header_name ] ) ) {
							$headers[ $header_name ] = $prefix . $credentials['api_key'];
						}
					}
					break;

				case 'bearer_token':
				case 'bearer':
					if ( ! empty( $credentials['token'] ) && ! isset( $headers['Authorization'] ) ) {
						$headers['Authorization'] = 'Bearer ' . $credentials['token'];
					}
					break;

				case 'basic':
				case 'basic_auth':
					if ( ! empty( $credentials['username'] ) && ! empty( $credentials['password'] ) && ! isset( $headers['Authorization'] ) ) {
						// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Used for HTTP Basic Auth encoding, not obfuscation.
						$headers['Authorization'] = 'Basic ' . base64_encode(
							$credentials['username'] . ':' . $credentials['password']
						);
					}
					break;

				case 'custom':
					// Handle custom authentication headers.
					if ( isset( $auth_config['headers'] ) && is_array( $auth_config['headers'] ) ) {
						foreach ( $auth_config['headers'] as $header_name => $header_template ) {
							// Only set if not already set by config or endpoint headers.
							if ( ! isset( $headers[ $header_name ] ) ) {
								$headers[ $header_name ] = self::replace_placeholders(
									$header_template,
									$credentials
								);
							}
						}
					}
					break;
			}
		}

		return $headers;
	}

	/**
	 * Check if a field value should be considered empty
	 *
	 * @param mixed $value The field value to check.
	 * @return bool True if the value is considered empty.
	 * @since 1.13.0
	 */
	public static function is_empty_field_value( $value ) {
		// Consider null, empty string, empty array as empty.
		if ( is_null( $value ) || '' === $value || ( is_array( $value ) && empty( $value ) ) ) {
			return true;
		}

		// Consider string "0" as NOT empty (valid value).
		if ( '0' === $value || 0 === $value ) {
			return false;
		}

		// Consider whitespace-only strings as empty.
		if ( is_string( $value ) && '' === trim( $value ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Load and decode JSON file
	 *
	 * @param string $file_path Path to JSON file.
	 * @return array|null Decoded JSON data or null on failure.
	 * @since 1.13.0
	 */
	public static function load_json_file( $file_path ) {
		if ( ! file_exists( $file_path ) ) {
			return null;
		}

		$json_content = file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading JSON config file.
		$config       = json_decode( Helper::get_string_value( $json_content ), true );

		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $config ) ) {
			return null;
		}

		return $config;
	}

	/**
	 * Load all integration configurations from directory
	 *
	 * @param string $integrations_dir Base integrations directory path.
	 * @param string $filter_provider Optional provider filter ('WordPress', etc.).
	 * @param bool   $active_only For WordPress integrations, only return active plugins.
	 * @return array Array of loaded configurations.
	 * @since 1.13.0
	 */
	public static function load_all_configs( $integrations_dir, $filter_provider = '', $active_only = false ) {
		$configs = [];

		// Load root JSON files.
		$files = glob( $integrations_dir . '*.json' );
		if ( ! empty( $files ) ) {
			foreach ( $files as $file_path ) {
				$config = self::load_json_file( $file_path );
				if ( $config && self::should_include_config( $config, $filter_provider, $active_only ) ) {
					$configs[] = $config;
				}
			}
		}

		// Load subdirectory configs.
		if ( is_dir( $integrations_dir ) ) {
			$scan_result = scandir( $integrations_dir );
			if ( false !== $scan_result ) {
				$directories = array_diff( $scan_result, [ '.', '..' ] );
				foreach ( $directories as $directory ) {
					$config_file = $integrations_dir . $directory . '/config.json';
					if ( is_dir( $integrations_dir . $directory ) && file_exists( $config_file ) ) {
						$config = self::load_json_file( $config_file );
						if ( $config && self::should_include_config( $config, $filter_provider, $active_only ) ) {
							$configs[] = $config;
						}
					}
				}
			}
		}

		return $configs;
	}

	/**
	 * Execute WordPress plugin action
	 *
	 * @param array $integration_config Integration configuration.
	 * @param array $action_config Action configuration.
	 * @param array $processed_fields Processed field data.
	 * @param array $original_submission_data Original form submission data.
	 * @return array Execution result.
	 * @since 1.13.0
	 */
	public static function execute_wordpress_plugin_action( $integration_config, $action_config, $processed_fields, $original_submission_data = [] ) {
		// Create provider instance to execute the action.
		$provider = Provider_Factory::create( $integration_config['integration']['name'] ?? '', $integration_config );

		if ( ! $provider ) {
			return [
				'success' => false,
				'message' => __( 'Failed to create provider instance.', 'sureforms-pro' ),
			];
		}

		// Check if provider supports action execution.
		if ( ! method_exists( $provider, 'execute_action' ) ) {
			return [
				'success' => false,
				'message' => __( 'Provider does not support action execution.', 'sureforms-pro' ),
			];
		}

		// Prepare data for the action (merge processed fields with original data for context).
		$action_data = array_merge( $original_submission_data, $processed_fields );

		// Execute the action.
		$action_name = $action_config['name'] ?? '';
		return $provider->execute_action( $action_name, $action_data );
	}

	/**
	 * Verify OAuth callback request authenticity
	 *
	 * Validates that the request comes from our OAuth middleware
	 * by checking User-Agent, origin headers, and verification token.
	 *
	 * @param \WP_REST_Request $request The REST request.
	 * @return bool True if request is from our middleware, false otherwise.
	 * @since 2.1.0
	 */
	public static function verify_oauth_callback( $request ) {
		// Check User-Agent header (middleware sends specific User-Agent).
		$user_agent = $request->get_header( 'User-Agent' );
		if ( empty( $user_agent ) || strpos( $user_agent, 'SureForms Integrations Middleware' ) === false ) {
			return false;
		}

		// Get middleware URL from constant.
		$middleware_url = defined( 'SRFM_MIDDLEWARE_BASE_URL' ) ? SRFM_MIDDLEWARE_BASE_URL . 'integrations/' : '';
		if ( empty( $middleware_url ) ) {
			return false;
		}

		// Extract expected origin from middleware URL.
		$expected_origin = wp_parse_url( $middleware_url, PHP_URL_SCHEME ) . '://' . wp_parse_url( $middleware_url, PHP_URL_HOST ) . '/';
		$request_origin  = $request->get_header( 'Origin' );
		$request_referer = $request->get_header( 'Referer' );

		// Check if origin or referer matches our middleware.
		$origin_valid = false;
		if ( ! empty( $request_origin ) && $request_origin === $expected_origin ) {
			$origin_valid = true;
		} elseif ( ! empty( $request_referer ) && strpos( $request_referer, $expected_origin ) === 0 ) {
			$origin_valid = true;
		}

		if ( ! $origin_valid ) {
			return false;
		}

		// Additional validation: Check if request has valid JSON content-type.
		$content_type = $request->get_header( 'Content-Type' );
		if ( empty( $content_type ) || strpos( $content_type, 'application/json' ) === false ) {
			return false;
		}

		// Verify time-based token if present.
		$verification_token = Helper::get_string_value( $request->get_param( 'verification_token' ) );
		$timestamp          = absint( $request->get_param( 'timestamp' ) );
		$integration_type   = Helper::get_string_value( $request->get_param( 'integration_type' ) );

		if ( ! empty( $verification_token ) && ! empty( $timestamp ) && ! empty( $integration_type ) ) {
			// Check if timestamp is within acceptable range (5 minutes).
			$current_time = time();
			if ( abs( $current_time - $timestamp ) > 300 ) {
				return false;
			}

			// Verify the HMAC token.
			$expected_token = hash_hmac( 'sha256', $integration_type . '|' . $timestamp, 'srfm_oauth_secret' );
			if ( ! hash_equals( $expected_token, $verification_token ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Get stored credentials with OAuth validation and refresh
	 *
	 * Retrieves integration credentials and ensures OAuth tokens are valid and fresh.
	 * This consolidates the credential fetching logic used in field-manager.php and workflow-processor.php.
	 *
	 * @param string                                                        $integration_name Integration name/type.
	 * @param array                                                         $auth_config Integration auth configuration (for OAuth refresh).
	 * @param OAuth_Handler                                                 $oauth_handler OAuth handler instance (optional, will create if not provided).
	 * @param \SRFM_Pro\Inc\Pro\Native_Integrations\Services\Config_Manager $config_manager Configuration manager instance (required if OAuth handler not provided).
	 * @return array|\WP_Error Credentials array on success, WP_Error on failure.
	 * @since 2.1.0
	 */
	public static function get_stored_credentials( $integration_name, $auth_config = [], $oauth_handler = null, $config_manager = null ) {
		// Get integration data from database.
		$integration_data = Integrations::get_by_type( sanitize_key( $integration_name ) );

		if ( empty( $integration_data ) || empty( $integration_data['data'] ) ) {
			return new \WP_Error(
				'no_credentials',
				__( 'No credentials found for this integration.', 'sureforms-pro' )
			);
		}

		// Decrypt stored credentials.
		$credentials = Integrations::decrypt_sensitive_data( Helper::get_array_value( $integration_data['data'] ) );

		if ( empty( $credentials ) ) {
			return new \WP_Error(
				'invalid_credentials',
				__( 'Failed to decrypt integration credentials.', 'sureforms-pro' )
			);
		}

		// Check if these are OAuth credentials and refresh if needed.
		if ( OAuth_Handler::is_oauth_credentials( $credentials ) ) {
			// Create OAuth handler if not provided.
			if ( null === $oauth_handler ) {
				if ( null === $config_manager ) {
					return new \WP_Error(
						'missing_oauth_handler_dependencies',
						__( 'Configuration manager is required to create an OAuth handler.', 'sureforms-pro' )
					);
				}

				$oauth_handler = new OAuth_Handler( $config_manager );
			}

			$credentials = $oauth_handler->ensure_fresh_oauth_tokens( $integration_name, $credentials, $auth_config );

			// If OAuth refresh failed, return the error.
			if ( is_wp_error( $credentials ) ) {
				return $credentials;
			}
		}

		return $credentials;
	}

	/**
	 * Replace placeholders in auth_config URLs with credential values
	 *
	 * Handles placeholder replacement for OAuth URLs (authorization_url and token_url)
	 * with values from credentials like domain for region-specific APIs (e.g., Zoho CRM).
	 *
	 * @param array $auth_config Authentication configuration.
	 * @param array $credentials Credentials containing placeholder values (e.g., domain).
	 * @return array Auth config with placeholders replaced.
	 * @since 2.3.0
	 */
	public static function replace_auth_config_placeholders( $auth_config, $credentials ) {
		if ( empty( $credentials ) || ! is_array( $credentials ) ) {
			return $auth_config;
		}

		// Replace placeholders in authorization_url.
		if ( isset( $auth_config['authorization_url'] ) ) {
			$auth_config['authorization_url'] = self::replace_placeholders(
				$auth_config['authorization_url'],
				$credentials
			);
		}

		// Replace placeholders in token_url.
		if ( isset( $auth_config['token_url'] ) ) {
			$auth_config['token_url'] = self::replace_placeholders(
				$auth_config['token_url'],
				$credentials
			);
		}

		return $auth_config;
	}

	/**
	 * Check if the given array is an indexed array of strings (not associative).
	 *
	 * An indexed array of strings is an array where the keys are consecutive integers
	 * starting from 0, and all values are strings.
	 *
	 * @param array $arr The array to check.
	 * @return bool True if $arr is an indexed array of strings, false otherwise.
	 * @since 2.3.0
	 */
	public static function is_indexed_string_array( $arr ) {
		// Check indexed (non-associative).
		if ( array_keys( $arr ) !== range( 0, count( $arr ) - 1 ) ) {
			return false;
		}

		// Check all values are strings.
		foreach ( $arr as $value ) {
			if ( ! is_string( $value ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Transform an indexed array of strings into an indexed array of associative arrays.
	 *
	 * Example: ['a', 'b'] => [ [ 'a' => 'a' ], [ 'b' => 'b' ] ]
	 * If $input is not an indexed string array, return $input as-is.
	 *
	 * @param array $input The input array.
	 * @return array The transformed array or the original $input if not applicable.
	 * @since 2.3.0
	 */
	public static function transform_to_key_label_array( $input ) {
		if ( ! self::is_indexed_string_array( $input ) ) {
			return $input;
		}
		return array_map(
			static function( $value ) {
				return [
					'key'   => $value,
					'label' => $value,
				];
			},
			$input
		);
	}

	/**
	 * Map API response to column headers array
	 *
	 * Converts Google Sheets-style header response into a structured format.
	 * Expected response format: {"values": [["Name", "Email", "Phone"]]}
	 *
	 * @param array $data API response data.
	 * @param array $config Configuration with response_mapping.
	 * @return array Column headers array.
	 * @since 2.3.0
	 */
	public static function map_response_to_column_headers( $data, $config ) {
		$headers = [];
		$mapping = $config['response_mapping'] ?? [];

		// Get the data path (e.g., "values.0" for Google Sheets).
		$data_path = $mapping['data_path'] ?? '';
		if ( empty( $data_path ) ) {
			return $headers;
		}

		// Navigate to the headers array.
		$headers_array = self::get_nested_value( $data, $data_path );

		if ( ! is_array( $headers_array ) ) {
			return $headers;
		}

		// Check if headers_array flag is set (indicates flat array of header strings).
		$is_headers_array = $mapping['headers_array'] ?? false;

		if ( $is_headers_array ) {
			// Convert flat array ["Name", "Email", "Phone"] to structured format.
			foreach ( $headers_array as $index => $header_name ) {
				if ( ! empty( $header_name ) ) {
					$headers[] = [
						'key'   => sanitize_key( $header_name ),
						'label' => $header_name,
						'index' => $index,
					];
				}
			}
		}

		// Handle fallback_on_empty configuration.
		if ( empty( $headers ) && ! empty( $config['fallback_on_empty'] ) ) {
			$fallback = $config['fallback_on_empty'];

			if ( ! empty( $fallback['create_default_headers'] ) ) {
				$count  = $fallback['default_count'] ?? 10;
				$prefix = $fallback['default_prefix'] ?? 'Column ';

				for ( $i = 1; $i <= $count; $i++ ) {
					$headers[] = [
						'key'   => 'column_' . $i,
						'label' => $prefix . $i,
						'index' => $i - 1,
					];
				}
			}
		}

		return $headers;
	}

	/**
	 * Check if config should be included based on filters
	 *
	 * @param array  $config Configuration array.
	 * @param string $filter_provider Provider filter.
	 * @param bool   $active_only Whether to check if plugin is active.
	 * @return bool True if config should be included.
	 * @since 1.13.0
	 */
	private static function should_include_config( $config, $filter_provider, $active_only ) {
		// Provider filter.
		if ( ! empty( $filter_provider ) && ( $config['provider'] ?? '' ) !== $filter_provider ) {
			return false;
		}

		// Active plugin filter for WordPress integrations.
		if ( $active_only && 'wordpress' === ( $config['provider'] ?? '' ) ) { // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled -- Configuration uses lowercase.
			$plugin_detection = $config['plugin_detection'] ?? [];
			if ( ! is_array( $plugin_detection ) ) {
				return false;
			}

			// Check for class-based detection.
			$plugin_class = $plugin_detection['class'] ?? '';
			if ( ! empty( $plugin_class ) ) {
				return class_exists( Helper::get_string_value( $plugin_class ) );
			}

			// Check for constant-based detection.
			$plugin_constant = $plugin_detection['constant'] ?? '';
			if ( ! empty( $plugin_constant ) ) {
				return defined( Helper::get_string_value( $plugin_constant ) );
			}

			return false;
		}

		return true;
	}
}
