<?php
/**
 * OAuth2 Authentication Strategy
 *
 * @category   WordPress_Plugin
 * @package    Common
 * @subpackage Strategies\Authentication
 * @author     miniOrange <info@miniorange.com>
 * @license    MIT/Expat
 * @link       https://plugins.miniorange.com/woocommerce-api-product-sync-with-woocommerce-rest-apis
 */

namespace MOWCPS\Common\Strategies\Authentication;

use WP_Error;
use MOWCPS\Common\Abstracts\Auth_Base_Strategy;
use MOWCPS\Common\Constants\Constants;
use MOWCPS\Common\Services\API\API_Service;
use MOWCPS\Common\Services\Logging\Supplier_Logger;

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

/**
 * Class OAuth2_Auth_Strategy
 *
 * Handles OAuth2 authentication with support for multiple grant types
 */
class OAuth2_Auth_Strategy extends Auth_Base_Strategy {
	/**
	 * Supported grant types
	 *
	 * @var array
	 */
	private $supported_grant_types = array(
		'client_credentials',
		'password',
	);

	/**
	 * Generate authorization headers
	 *
	 * @param int   $supplier_id Supplier ID.
	 * @param array $settings Auth settings.
	 * @return array|WP_Error Headers array or error
	 */
	public function generate_headers( $settings, $supplier_id ) {
		// Validate settings first.
		$validation = $this->validate_settings( $settings );
		if ( is_wp_error( $validation ) ) {
			return $validation;
		}

		// Always fetch the token; caching is skipped internally for test calls (supplier_id = 0).
		$access_token = $this->get_oauth2_access_token( $supplier_id, $settings );
		if ( is_wp_error( $access_token ) ) {
			return $access_token;
		}

		$response_settings = $settings['response_settings'] ?? array();
		$request_settings  = $settings['request_settings'] ?? array();

		$token_type  = $response_settings['token_type'] ?? Constants::DEFAULT_TOKEN_TYPE;
		$header_name = $request_settings['authorization_header_name'] ?? Constants::DEFAULT_AUTH_HEADER_NAME;

		$header_value = ltrim( $token_type . ' ' . $access_token );

		return array(
			$header_name => $header_value,
		);
	}

	/**
	 * Validate auth settings
	 *
	 * @param array $settings Auth settings.
	 * @return bool|WP_Error True if valid, WP_Error if invalid
	 */
	public function validate_settings( $settings ) {
		$request_settings  = $settings['request_settings'] ?? array();
		$response_settings = $settings['response_settings'] ?? array();

		// Check required request settings.
		$grant_type       = $request_settings['grant_type'] ?? Constants::OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS;
		$access_token_url = $request_settings['access_token_url'] ?? '';
		$client_id        = $request_settings['client_id'] ?? '';
		$client_secret    = $request_settings['client_secret'] ?? '';

		if ( empty( $access_token_url ) ) {
			return new WP_Error(
				'invalid_oauth2_config',
				'Invalid OAuth2 configuration: missing required fields (access_token_url, client_id, client_secret)',
				array( 'status' => 400 )
			);
		}

		// Validate grant type.
		if ( ! in_array( $grant_type, $this->supported_grant_types, true ) ) {
			return new WP_Error(
				'invalid_oauth2_grant_type',
				'Invalid OAuth2 grant type: ' . $grant_type,
				array( 'status' => 400 )
			);
		}

		// Check required response settings.
		$token_key = $response_settings['token_key'] ?? Constants::DEFAULT_TOKEN_KEY;
		if ( empty( $token_key ) ) {
			return new WP_Error(
				'invalid_oauth2_response_config',
				'Invalid OAuth2 response configuration: missing token_key',
				array( 'status' => 400 )
			);
		}

		return true;
	}

	/**
	 * Get OAuth2 access token with caching
	 *
	 * @param int   $supplier_id Supplier ID.
	 * @param array $settings OAuth2 settings.
	 * @return string|WP_Error Access token or error
	 */
	private function get_oauth2_access_token( $supplier_id, $settings ) {
		// Check for cached token first (skip cache for connection tests).
		if ( $supplier_id > 0 ) {
			$cached_token = $this->get_cached_token( $supplier_id );
			if ( $cached_token ) {
				return $cached_token;
			}
		}

		// No cached token, make a fresh request.
		return $this->refresh_oauth2_token( $supplier_id, $settings );
	}

	/**
	 * Refresh OAuth2 token
	 *
	 * @param int   $supplier_id Supplier ID.
	 * @param array $settings OAuth2 settings.
	 * @return string|WP_Error Access token or error
	 */
	private function refresh_oauth2_token( $supplier_id, $settings ) {
		$request_settings  = $settings['request_settings'] ?? array();
		$response_settings = $settings['response_settings'] ?? array();

		// Map UI field names to expected field names.
		$access_token_url        = $request_settings['access_token_url'];
		$client_id               = $request_settings['client_id'];
		$client_secret           = $request_settings['client_secret'];
		$grant_type              = $request_settings['grant_type'];
		$scope                   = $request_settings['scope'] ?? '';
		$send_client_credentials = $request_settings['send_client_credentials'] ?? Constants::DEFAULT_SEND_CLIENT_CREDENTIALS;

		// Prepare request data based on grant type.
		$request_data = $this->prepare_oauth2_request_data( $grant_type, $request_settings );

		// Add client credentials based on send_client_credentials setting.
		$send_in_body   = in_array( 'body', $send_client_credentials, true );
		$send_in_header = in_array( 'header', $send_client_credentials, true );

		// Default to header if neither is specified.
		if ( ! $send_in_body && ! $send_in_header ) {
			$send_in_header = true;
		}

		$auth_headers = array();

		// Add client credentials to request body if specified.
		if ( $send_in_body ) {
			$request_data['client_id']     = $client_id;
			$request_data['client_secret'] = $client_secret;
		}

		// Add client credentials to Authorization header if specified.
		if ( $send_in_header ) {
			$auth_headers[ Constants::DEFAULT_AUTH_HEADER_NAME ] = 'Basic ' . base64_encode( $client_id . ':' . $client_secret ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- The code is not used for obfuscation.
		}

		if ( ! empty( $scope ) ) {
			$request_data['scope'] = $scope;
		}

		$api_request = array(
			'method'  => Constants::HTTP_REQUEST_METHOD_POST,
			'headers' => $auth_headers,
			'body'    => $request_data,
		);

		// Make token request using API service.
		$response = API_Service::make_api_call( $access_token_url, $api_request );

		if ( is_wp_error( $response ) ) {
			Supplier_Logger::debug( $supplier_id, 'OAuth2 authentication failed' );
			Supplier_Logger::debug( $supplier_id, 'Error: ' . $response->get_error_message() );
			Supplier_Logger::debug( $supplier_id, 'Error code: ' . $response->get_error_code() );
			Supplier_Logger::debug( $supplier_id, 'Check your OAuth2 credentials and token endpoint configuration' );
			return $response;
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( ! $data || empty( $data[ $response_settings['token_key'] ] ) ) {
			$error = new WP_Error(
				'oauth2_token_error',
				'Failed to get OAuth2 access token',
				array( 'status' => 500 )
			);
			Supplier_Logger::debug( $supplier_id, 'OAuth2 token response invalid or missing required token' );
			Supplier_Logger::debug( $supplier_id, 'Response body: ' . $body );
			Supplier_Logger::debug( $supplier_id, 'Expected token key: ' . $response_settings['token_key'] );
			Supplier_Logger::debug( $supplier_id, 'Verify your OAuth2 response configuration and token key mapping' );
			return $error;
		}

		$token      = $data[ $response_settings['token_key'] ];
		$expires_in = $data[ $response_settings['expires_in_key'] ] ?? $response_settings['expires_in'] ?? Constants::DEFAULT_EXPIRES_IN;

		if ( Constants::TEST_SUPPLIER !== $supplier_id ) {
			// Cache the token.
			$this->set_cached_token( $supplier_id, $token, $expires_in );
		}

		return $token;
	}

	/**
	 * Prepare OAuth2 request data based on grant type
	 *
	 * @param string $grant_type Grant type.
	 * @param array  $request_settings Request settings.
	 * @return array Request data.
	 */
	private function prepare_oauth2_request_data( $grant_type, $request_settings ) {
		$request_data = array(
			'grant_type' => $grant_type,
		);

		switch ( $grant_type ) {
			case Constants::OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS:
				// No additional data needed for client credentials.
				break;

			case Constants::OAUTH2_GRANT_TYPE_PASSWORD:
				// Add username and password for password grant.
				if ( ! empty( $request_settings['username'] ) ) {
					$request_data['username'] = $request_settings['username'];
				}
				if ( ! empty( $request_settings['password'] ) ) {
					$request_data['password'] = $request_settings['password'];
				}
				break;
		}

		return $request_data;
	}
}
