<?php
/**
 * Base Supplier App Abstract Class
 *
 * @category   WordPress_Plugin
 * @package    Common
 * @subpackage Abstracts
 * @author     miniOrange <info@miniorange.com>
 * @license    MIT/Expat
 * @link       https://plugins.miniorange.com/woocommerce-api-product-sync-with-woocommerce-rest-apis
 */

namespace MOWCPS\Common\Abstracts;

use WP_Error;
use MOWCPS\Common\Interfaces\Supplier_App_Interface;
use MOWCPS\Common\Constants\Constants;
use MOWCPS\Common\Constants\Plugin_Filters;
use MOWCPS\Common\Utils\Configuration_Loader;
use MOWCPS\Common\Handlers\Rest_API\Supplier_Handler;
use MOWCPS\Common\Handlers\Rest_API\Connection_Handler;
use MOWCPS\Common\Handlers\Rest_API\Attribute_Mapping_Handler;
use MOWCPS\Common\Handlers\Rest_API\Authorization_Handler;
use MOWCPS\Common\Handlers\Rest_API\Setting_Handler;
use MOWCPS\Common\Services\Logging\Supplier_Logger;
use MOWCPS\Common\Models\Connection_Model;
use MOWCPS\Common\Models\Supplier_Model;
use MOWCPS\Common\Models\Attribute_Mapping_Model;
use MOWCPS\Common\Factories\Supplier_App_Factory;
use MOWCPS\Common\Utils\Model_Utils;
use MOWCPS\Common\Utils\Utils;

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

/**
 * Class Base_Supplier_App
 *
 * Base abstract class for all supplier app implementations
 */
abstract class Base_Supplier_App implements Supplier_App_Interface {
	/**
	 * Supplier name.
	 *
	 * @var string
	 */
	protected $supplier_name;

	/**
	 * Supplier app type
	 *
	 * @var string
	 */
	public $app_type;

	/**
	 * Connection name to UUID mapping for reference resolution
	 *
	 * @var array
	 */
	protected $connection_name_to_id_map = array();

	/**
	 * Connection ID to attribute mapping IDs mapping for reference resolution
	 *
	 * @var array
	 */
	private $connection_to_mapping_ids_map = array();

	/**
	 * Constructor
	 *
	 * @param string $app_type The supplier app type.
	 */
	public function __construct( $app_type ) {
		$this->app_type      = $app_type;
		$this->supplier_name = ucwords( str_replace( '-', ' ', $app_type ) );
	}

	/**
	 * Perform connection test
	 *
	 * @param array $supplier_data Supplier data.
	 * @return array Test result.
	 */
	abstract public function perform_connection_test( $supplier_data );

	/**
	 * Validate supplier-specific configuration
	 *
	 * @param array $supplier_data Supplier data to validate.
	 * @return bool|WP_Error True if valid, WP_Error if invalid.
	 */
	abstract public function validate_supplier_specific_configuration( $supplier_data );

	/**
	 * Test connection for supplier
	 *
	 * @param string $app_type App type.
	 * @param array  $details Connection data to test.
	 * @return array|WP_Error Test result or error.
	 */
	public function test_connection( $app_type, $details ) {
		// Validate input data.
		$validation_result = $this->validate_test_data( $app_type );
		if ( is_wp_error( $validation_result ) ) {
			return $validation_result;
		}

		// Perform the actual connection test.
		return $this->perform_connection_test( $details );
	}

	/**
	 * Create supplier with all related configurations
	 *
	 * @param array $data Supplier data from API request.
	 * @return array|WP_Error Created supplier with configurations or error.
	 */
	public function create_supplier( $data ) {
		$limit_check = $this->check_supplier_limit();
		if ( is_wp_error( $limit_check ) ) {
			return $limit_check;
		}

		/**
		 * Hook for altering supplier data before creating the supplier.
		 *
		 * @param array $data Supplier data.
		 * @return array Altered supplier data.
		 */
		if ( has_filter( Plugin_Filters::FILTER_ALTER_SUPPLIER_DATA ) ) {
			$returned_data = apply_filters( Plugin_Filters::FILTER_ALTER_SUPPLIER_DATA,
				array(
					'data' => $data,
				)
			);

			$this->app_type = $returned_data['app_type'];
		}

		$details = $data['details'] ?? array();
		// Load and process configuration with dynamic parameters.
		$processed_config = $this->load_and_process_configuration( $details, $data );
		if ( is_wp_error( $processed_config ) ) {
			return $processed_config;
		}

		// Create supplier using existing Supplier_Handler.
		$supplier_handler = new Supplier_Handler();

		// Merge configuration data with request data.
		$supplier_data = array_merge(
			$data,
			array(
				'sync_type'                           => $processed_config['sync_type'] ?? Constants::SYNC_TYPE_API,
				'unique_product_identifier'           => $processed_config['unique_product_identifier'] ?? null,
				'unique_product_variation_identifier' => $processed_config['unique_product_variation_identifier'] ?? null,
			)
		);

		$supplier_result = $supplier_handler->create( $supplier_data );

		if ( is_wp_error( $supplier_result ) ) {
			return $supplier_result;
		}

		// Get supplier UUID and ID for creating related entries.
		$supplier_uuid = $supplier_result['uuid'] ?? '';
		if ( empty( $supplier_uuid ) ) {
			return new WP_Error(
				Constants::ERROR_INVALID_SUPPLIER_CONFIG,
				'Failed to get supplier UUID after creation.',
				array( 'status' => Constants::HTTP_INTERNAL_SERVER_ERROR )
			);
		}

		// Get the numeric supplier ID from the database using the UUID.
		$supplier_model = Supplier_Model::get_by_uuid( $supplier_uuid );
		$supplier_id    = $supplier_model['id'] ?? '';
		if ( empty( $supplier_id ) ) {
			return new WP_Error(
				Constants::ERROR_INVALID_SUPPLIER_CONFIG,
				'Failed to get supplier ID after creation.',
				array( 'status' => Constants::HTTP_INTERNAL_SERVER_ERROR )
			);
		}

		$app_type = ucwords( str_replace( '-', '_', $data['app_type'] ) );

		// Register supplier specific hooks.
		$supported_app_types = Supplier_App_Factory::$supported_app_types;
		$namespace           = in_array( $this->app_type, $supported_app_types, true ) ? 'Common' : MOWCPS_PLAN_NAMESPACE;
		$hook_class          = 'MOWCPS\\' . $namespace . '\\Hooks\\' . $app_type;

		if ( class_exists( $hook_class ) ) {
			new $hook_class();
		}

		// Create entities in the correct order with reference resolution.
		$this->create_authorization( $supplier_uuid, $processed_config[ Constants::AUTHORIZATION ] ?? array() );
		$this->create_connections( $supplier_uuid, $processed_config[ Constants::CONNECTIONS ] );
		$this->create_attribute_mappings( $supplier_uuid, $processed_config[ Constants::ATTRIBUTE_MAPPING ] );
		$this->create_settings( $supplier_uuid, $processed_config[ Constants::OPERATIONS ], $supplier_id );

		$plan = false !== strpos( Constants::MOWCPS_PLAN_NAME, Constants::STANDARD ) ? Constants::PLAN_TYPE_STANDARD : Constants::PLAN_TYPE_PREMIUM;
		if ( isset( $processed_config[ Constants::ORDER_SYNC_CONFIG ] ) ) {
			if ( has_filter( Plugin_Filters::FILTER_CREATE_ORDER_CONFIG ) ) {
				do_action(
					Plugin_Filters::FILTER_CREATE_ORDER_CONFIG,
					$supplier_uuid,
					$processed_config[ Constants::ORDER_SYNC_CONFIG ],
					$this->connection_name_to_id_map,
					$supplier_id
				);
			}
		}

		// Handle order_sync from default template after connections are created.
		if ( isset( $processed_config[ Constants::ORDER_SYNC_CONFIG ] ) && has_filter( Plugin_Filters::FILTER_UPDATE_DEFAULT_ORDER_SYNC ) ) {
			// Process order_sync config again with supplier_id since it wasn't available during initial processing.
			$order_sync_configs = $processed_config[ Constants::ORDER_SYNC_CONFIG ];
			foreach ( $order_sync_configs as &$order_sync_config ) {
				if ( isset( $order_sync_config['configuration'] ) && is_array( $order_sync_config['configuration'] ) ) {
					$order_sync_config['configuration'] = Configuration_Loader::replace_dynamic_parameters(
						$order_sync_config['configuration'],
						array( 'supplier_id' => $supplier_uuid )
					);
				}
			}
			unset( $order_sync_config ); // Break reference.

			do_action(
				Plugin_Filters::FILTER_UPDATE_DEFAULT_ORDER_SYNC,
				$supplier_model,
				$order_sync_configs,
				$processed_config
			);
		}

		do_action( Plugin_Filters::ACTION_SUPPLIER_SETUP_COMPLETE, $supplier_model );

		$response = array(
			'success' => true,
			'data'    => $supplier_result,
		);

		return $response;
	}

	/**
	 * Load and process configuration with dynamic parameters
	 *
	 * @param array $details Supplier details for dynamic parameter replacement.
	 * @param array $request_data Request data from API request body.
	 * @return array|WP_Error Processed configuration or error.
	 */
	protected function load_and_process_configuration( $details, $request_data = null ) {
		// If request indicates import-export flow, use request body as config source.
		if ( $request_data && isset( $request_data['operation_type'] ) && ! empty( $request_data['operation_type'] && Constants::OPERATION_TYPE_IMPORT_EXPORT === $request_data['operation_type'] ) ) {
			$config = is_array( $request_data ) ? $request_data : array();
		} else {
			// Default: Load configuration from JSON file.
			$config = Configuration_Loader::load_supplier_config( $this->app_type );
			if ( is_wp_error( $config ) ) {
				return $config;
			}
		}

		// Validate raw configuration first.
		$validation_result = Configuration_Loader::validate_configuration( $config );
		if ( is_wp_error( $validation_result ) ) {
			return $validation_result;
		}

		// Process configuration with dynamic parameters after validation.
		$processed_config = Configuration_Loader::process_configuration( $config, $details );

		return $processed_config;
	}

	/**
	 * Create authorization for the supplier
	 *
	 * @param int   $supplier_id Supplier ID.
	 * @param array $config Processed configuration.
	 */
	protected function create_authorization( $supplier_id, $config ) {
		$authorization = $config ?? array();
		if ( empty( $authorization ) ) {
			return;
		}

		$authorization_handler = new Authorization_Handler();
		$authorization_data    = array_merge( $authorization, array( 'supplier_id' => $supplier_id ) );

		$result = $authorization_handler->create( $supplier_id, $authorization_data );

		if ( is_wp_error( $result ) ) {
			Supplier_Logger::debug( $supplier_id, sprintf( 'Failed to create %s: %s', Constants::AUTHORIZATION, $result->get_error_message() ) );
		}
	}

	/**
	 * Create connections for the supplier
	 *
	 * @param int   $supplier_id Supplier ID.
	 * @param array $config Processed configuration.
	 */
	protected function create_connections( $supplier_id, $config ) {
		$connections = $config ?? array();
		if ( empty( $connections ) ) {
			return;
		}

		$connection_handler              = new Connection_Handler();
		$this->connection_name_to_id_map = array();

		foreach ( $connections as $connection_config ) {
			$connection_name = $connection_config['name'] ?? '';

			// Skip connections with empty connection name or priority 0.
			if ( empty( $connection_name ) || 0 === $connection_config['priority'] ) {
				continue;
			}

			$connection_data = array_merge( $connection_config, array( 'supplier_id' => $supplier_id ) );
			$result          = $connection_handler->create( $supplier_id, $connection_data );

			if ( is_wp_error( $result ) ) {
				Supplier_Logger::debug( $supplier_id, sprintf( 'Failed to create %s: %s', 'connection', $result->get_error_message() ) );
				continue;
			}

			// Store the connection name to ID mapping.
			$this->connection_name_to_id_map[ $connection_name ] = $result['uuid'] ?? null;
		}
	}

	/**
	 * Create attribute mappings for the supplier
	 *
	 * @param int   $supplier_id Supplier ID.
	 * @param array $config Processed configuration.
	 */
	protected function create_attribute_mappings( $supplier_id, $config ) {
		$mappings = $config ?? array();
		if ( empty( $mappings ) ) {
			return;
		}

		$mapping_handler                     = new Attribute_Mapping_Handler();
		$this->connection_to_mapping_ids_map = array();

		foreach ( $mappings as $mapping_config ) {
			$connection_name = $mapping_config['connection'] ?? '';
			if ( empty( $connection_name ) ) {
				continue;
			}

			// Resolve connection name to ID.
			$connection_id = $this->connection_name_to_id_map[ $connection_name ] ?? null;
			if ( empty( $connection_id ) ) {
				Supplier_Logger::debug( $supplier_id, sprintf( 'Failed to create %s: %s', 'attribute_mapping', "Connection '$connection_name' not found" ) );
				continue;
			}

			// Process fetch_from_other_api configuration if present.
			if ( isset( $mapping_config['advanced_configuration']['fetch_from_other_api'] ) ) {
				$fetch_config = &$mapping_config['advanced_configuration']['fetch_from_other_api'];
				if ( isset( $fetch_config['connection'] ) ) {
					$fetch_connection_name = $fetch_config['connection'];
					$fetch_connection_id   = $this->connection_name_to_id_map[ $fetch_connection_name ] ?? null;
					if ( $fetch_connection_id ) {
						$fetch_config['connection'] = $fetch_connection_id;
					} else {
						Supplier_Logger::debug( $supplier_id, sprintf( 'Failed to resolve fetch_from_other_api connection: %s', $fetch_connection_name ) );
					}
				}
			}

			$mapping_data = array_merge(
				$mapping_config,
				array(
					'supplier_id'   => $supplier_id,
					'connection_id' => $connection_id,
				)
			);

			$result = $mapping_handler->create( $supplier_id, $mapping_data );
			if ( is_wp_error( $result ) ) {
				Supplier_Logger::debug( $supplier_id, sprintf( 'Failed to create %s: %s', 'attribute_mapping', $result->get_error_message() ) );
				continue;
			}

			// Store the attribute mapping UUID grouped by connection UUID.
			$mapping_uuid = $result['uuid'] ?? null;
			if ( ! empty( $mapping_uuid ) ) {
				if ( ! isset( $this->connection_to_mapping_ids_map[ $connection_id ] ) ) {
					$this->connection_to_mapping_ids_map[ $connection_id ] = array();
				}
				$this->connection_to_mapping_ids_map[ $connection_id ][] = $mapping_uuid;
			}
		}
	}

	/**
	 * Create settings for the supplier
	 *
	 * @param string $supplier_uuid Supplier UUID.
	 * @param array  $config Processed configuration.
	 * @param int    $supplier_id Numeric supplier ID.
	 */
	protected function create_settings( $supplier_uuid, $config, $supplier_id = null ) {
		$settings = $config ?? array();

		if ( empty( $settings ) ) {
			return;
		}

		// Replace supplier_id placeholder in settings configuration if supplier_id is provided.
		if ( ! empty( $supplier_id ) ) {
			$settings = Utils::replace_supplier_id_placeholder( $settings, $supplier_id );
		}

		foreach ( $settings as $setting_config ) {
			$operation = $setting_config['operation'] ?? '';

			if ( empty( $operation ) ) {
				Supplier_Logger::debug( $supplier_uuid, sprintf( 'Failed to create %s: %s', 'setting', 'Setting operation is required' ) );
				continue;
			}

			// Resolve connection IDs if the setting references connections.
			$connection_names = $setting_config['connections'] ?? array();
			$connection_ids   = array();

			foreach ( $connection_names as $connection_name ) {
				$connection_id = $this->connection_name_to_id_map[ $connection_name ] ?? null;
				if ( ! empty( $connection_id ) ) {
					$connection_ids[] = $connection_id;
				} else {
					Supplier_Logger::debug( $supplier_uuid, sprintf( 'Failed to create %s: %s', 'setting', "Connection '$connection_name' not found for operation '$operation'" ) );
				}
			}

			// Resolve attribute mapping IDs for the referenced connections.
			$attribute_mapping_ids = array();
			foreach ( $connection_ids as $connection_id ) {
				$mapping_ids           = $this->connection_to_mapping_ids_map[ $connection_id ] ?? array();
				$attribute_mapping_ids = array_merge( $attribute_mapping_ids, $mapping_ids );
			}

			// Prepare setting data in the format expected by the API.
			$setting_data = array(
				'operation'      => $operation,
				'operation_type' => $setting_config['operation_type'],
				'is_active'      => $setting_config['is_active'],
			);

			// For schedule operations, resolve connection IDs from names.
			if ( 'schedule' === $operation ) {
				// Get connection IDs from connection names.
				$connection_names        = $setting_config['connections'] ?? array();
				$schedule_connection_ids = array();
				foreach ( $connection_names as $connection_name ) {
					$connection_id = $this->connection_name_to_id_map[ $connection_name ] ?? null;
					if ( $connection_id ) {
						$schedule_connection_ids[] = $connection_id;
					}
				}

				// Get attribute mapping IDs for these connections.
				$schedule_mapping_ids      = array();
				$mapping_to_connection_map = array();

				foreach ( $schedule_connection_ids as $connection_id ) {

					// Get connection details.
					$connection = Connection_Model::get_by_uuid( $connection_id );
					if ( ! $connection ) {
						continue;
					}

					// Get the integer connection ID from the connection record.
					$connection_integer_id = $connection['id'];

					$mapping_ids = array();

					// If this is an update connection, always use the base connection's mappings.
					if ( 0 === strpos( $connection['name'], 'Updated ' ) ) {
						$base_name = str_replace( 'Updated ', '', $connection['name'] );
						foreach ( $this->connection_name_to_id_map as $name => $id ) {
							if ( $name === $base_name ) {
								$mapping_ids = $this->connection_to_mapping_ids_map[ $id ] ?? array();
								break;
							}
						}

						// Map each attribute mapping integer ID to the updated connection's integer ID.
						// This ensures the updated endpoint is used instead of base connection.
						foreach ( $mapping_ids as $mapping_uuid ) {
							// Fetch the integer ID for this attribute mapping.
							$mapping_record = Attribute_Mapping_Model::get_by_uuid( $mapping_uuid );
							if ( $mapping_record && isset( $mapping_record['id'] ) ) {
								$mapping_integer_id                               = $mapping_record['id'];
								$mapping_to_connection_map[ $mapping_integer_id ] = $connection_integer_id;
							}
						}
					} else {
						// For non-update connections, use their own mappings.
						$mapping_ids = $this->connection_to_mapping_ids_map[ $connection_id ] ?? array();

						// Map each attribute mapping integer ID to its own connection's integer ID.
						foreach ( $mapping_ids as $mapping_uuid ) {
							// Fetch the integer ID for this attribute mapping.
							$mapping_record = Attribute_Mapping_Model::get_by_uuid( $mapping_uuid );
							if ( $mapping_record && isset( $mapping_record['id'] ) ) {
								$mapping_integer_id                               = $mapping_record['id'];
								$mapping_to_connection_map[ $mapping_integer_id ] = $connection_integer_id;
							}
						}
					}

					$schedule_mapping_ids = array_merge( $schedule_mapping_ids, $mapping_ids );
				}

				// Add schedule data.
				$operation_data_key = $operation . '_data';
				if ( isset( $setting_config[ $operation_data_key ] ) ) {
					$setting_data[ $operation_data_key ] = $setting_config[ $operation_data_key ];
				}

				// Add attribute mapping IDs for resolving connections in the model.
				$setting_data['attribute_mapping_ids'] = $schedule_mapping_ids;

				// Add mapping to connection override map for scheduled syncs.
				// This ensures "Updated" connections use the correct connection ID.
				$setting_data['mapping_to_connection_map'] = $mapping_to_connection_map;
			} else {
				// For non-schedule operations, pass connection IDs directly.
				$setting_data['connection_ids']        = $connection_ids;
				$setting_data['attribute_mapping_ids'] = $attribute_mapping_ids;

				// Add operation-specific data.
				$operation_data_key = $operation . '_data';
				if ( isset( $setting_config[ $operation_data_key ] ) ) {
					$setting_data[ $operation_data_key ] = $setting_config[ $operation_data_key ];
				}
			}

			// Add configuration if present.
			if ( isset( $setting_config['configuration'] ) ) {
				$setting_data['configuration'] = $setting_config['configuration'];
			}

			// Create the setting using the Setting_Handler.
			$setting_handler = new Setting_Handler();
			$result          = $setting_handler->create( $supplier_uuid, $operation, $setting_data );

			if ( is_wp_error( $result ) ) {
				Supplier_Logger::debug( $supplier_uuid, sprintf( 'Failed to create %s: %s', 'setting', $result->get_error_message() ) );
				continue;
			}
		}
	}

	/**
	 * Validate test data
	 *
	 * @param string $app_type App type.
	 * @return bool|WP_Error True if valid, WP_Error if invalid.
	 */
	protected function validate_test_data( $app_type ) {
		if ( empty( $app_type ) ) {
			return new WP_Error(
				Constants::ERROR_MISSING_REQUIRED_FIELD,
				sprintf( Constants::SUPPLIER_MISSING_APP_TYPE, $app_type ),
				array( 'status' => Constants::HTTP_BAD_REQUEST )
			);
		}

		return true;
	}

	/**
	 * Get common test result structure
	 *
	 * @param bool   $success     Whether the test was successful.
	 * @param string $message     Test message.
	 * @param int    $status_code Status code.
	 * @param array  $data        Test data.
	 * @return array Test result array.
	 */
	protected function get_test_result( $success, $message, $status_code = Constants::HTTP_BAD_REQUEST, $data = array() ) {
		$status_code = ! empty( $status_code ) ? $status_code : Constants::HTTP_INTERNAL_SERVER_ERROR;
		return array(
			'success'     => $success,
			'message'     => $message,
			'data'        => $data,
			'status_code' => $status_code,
		);
	}

	/**
	 * Find the best connection for testing based on priority
	 *
	 * @param array $connections Array of connections from configuration.
	 * @return array|null Connection configuration or null if none found.
	 */
	protected function find_test_connection( $connections ) {
		if ( empty( $connections ) ) {
			return null;
		}

		foreach ( $connections as $connection ) {
			$priority = (int) ( $connection['priority'] );
			if ( 1 === $priority ) {
				return $connection;
			}
		}
		return null;
	}

	/**
	 * Check if supplier limit has been reached
	 *
	 * @return true|WP_Error True if limit not reached, WP_Error if limit exceeded.
	 */
	private function check_supplier_limit() {
		$existing_suppliers = Supplier_Model::get_all( array( 'columns' => array( 'id' ) ) );
		$current_count      = is_array( $existing_suppliers ) ? count( $existing_suppliers ) : 0;

		$plan = false !== strpos( Constants::MOWCPS_PLAN_NAME, Constants::STANDARD ) ? Constants::PLAN_TYPE_STANDARD : Constants::PLAN_TYPE_PREMIUM;

		if ( Constants::PLAN_TYPE_STANDARD === $plan ) {
			if ( $current_count >= 1 ) {
				return new WP_Error(
					'supplier_limit_reached',
					'You have reached the maximum limit of 1 supplier allowed by your license. Please upgrade your license to add a new one.',
					array( 'status' => Constants::HTTP_FORBIDDEN )
				);
			}
			return true;
		}

		$no_of_suppliers = apply_filters( Plugin_Filters::FILTER_GET_NO_OF_SP, -1 );
		if ( -1 === $no_of_suppliers || 0 === $no_of_suppliers ) {
			if ( has_filter( Plugin_Filters::FILTER_REFRESH_LICENSE ) ) {
				do_action( Plugin_Filters::FILTER_REFRESH_LICENSE );
				$no_of_suppliers = apply_filters( Plugin_Filters::FILTER_GET_NO_OF_SP, -1 );
			}
			if ( -1 === $no_of_suppliers || 0 === $no_of_suppliers ) {
				return true;
			}
		}

		if ( $current_count >= $no_of_suppliers ) {
			return new WP_Error(
				'supplier_limit_reached',
				sprintf(
					'You have reached the maximum limit of %d supplier(s) allowed by your license. Please upgrade your license to add a new one.',
					$no_of_suppliers
				),
				array( 'status' => Constants::HTTP_FORBIDDEN )
			);
		}

		return true;
	}
}
