<?php

namespace WeeConnectPay\Integrations;

use WeeConnectPay\AccessToken;
use WeeConnectPay\Environment;
use WeeConnectPay\Exceptions\WeeConnectPayException;
use WeeConnectPay\Exceptions\Codes\ExceptionCode;
use WeeConnectPay\WordPress\Plugin\includes\WeeConnectPayAPI;
use WeeConnectPay\WordPress\Plugin\includes\WeeConnectPayUtilities;

/**
 * Manages OAuth2 Client Credentials authentication flow.
 *
 * Handles token exchange, refresh, context fingerprint computation,
 * and context metadata collection for the new authentication system.
 *
 * @since 3.19.0
 */
class OAuthTokenManager {

	/**
	 * WordPress transient key for preventing concurrent token refreshes
	 */
	private const REFRESH_LOCK_TRANSIENT = 'weeconnectpay_token_refresh_lock';

	/**
	 * Lock duration in seconds
	 */
	private const REFRESH_LOCK_DURATION = 30;

	/**
	 * Exchange OAuth2 client credentials for an access token.
	 *
	 * Calls POST /oauth/token with client_id, client_secret, and context headers.
	 *
	 * @param string $clientId
	 * @param string $clientSecret
	 * @return object Token response from the backend
	 * @throws WeeConnectPayException
	 * @since 3.19.0
	 */
	public function exchangeCredentials( string $clientId, string $clientSecret ): object {
		$apiDomain = WeeConnectPayAPI::getApiDomain();
		$endpoint  = $apiDomain . '/oauth/token';

		$fingerprint = $this->computeContextFingerprint();
		$metadata    = $this->getContextMetadata();

		$response = wp_remote_post(
			$endpoint,
			array(
				'timeout' => 60,
				'headers' => array(
					'Content-Type'             => 'application/json',
					'Accept'                   => 'application/json',
					'X-WCP-Context-Fingerprint' => $fingerprint,
					'X-WCP-Context-Metadata'    => wp_json_encode( $metadata ),
				),
				'body' => wp_json_encode( array(
					'grant_type'    => 'client_credentials',
					'client_id'     => $clientId,
					'client_secret' => $clientSecret,
				) ),
			)
		);

		if ( is_wp_error( $response ) ) {
			$errorMessage = $response->get_error_message();
			LogService::error( 'OAuth token exchange failed (WP Error): ' . $errorMessage );
			throw new WeeConnectPayException(
				'Failed to connect to WeeConnectPay API: ' . $errorMessage,
				ExceptionCode::UNHANDLED_EXCEPTION
			);
		}

		$httpCode     = wp_remote_retrieve_response_code( $response );
		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );

		if ( $httpCode !== 200 ) {
			$errorMessage = isset( $responseBody->message ) ? $responseBody->message : 'Unknown error';

			// Check for specific fingerprint mismatch error
			if ( $httpCode === 403 && isset( $responseBody->error ) && $responseBody->error === 'context_mismatch' ) {
				LogService::error( 'OAuth token exchange rejected: environment mismatch. Please disconnect and reconnect.' );
				throw new WeeConnectPayException(
					'Environment mismatch detected. These credentials are bound to a different WordPress installation. Please create new credentials in the WeeConnectPay dashboard.',
					ExceptionCode::UNHANDLED_EXCEPTION
				);
			}

			LogService::error( 'OAuth token exchange failed (HTTP ' . $httpCode . '): ' . $errorMessage );
			throw new WeeConnectPayException(
				'Authentication failed: ' . $errorMessage,
				ExceptionCode::UNHANDLED_EXCEPTION
			);
		}

		if ( ! isset( $responseBody->access_token ) ) {
			LogService::error( 'OAuth token exchange response missing access_token' );
			throw new WeeConnectPayException(
				'Invalid response from authentication server.',
				ExceptionCode::UNHANDLED_EXCEPTION
			);
		}

		return $responseBody;
	}

	/**
	 * Check if the current access token needs refresh and re-exchange if so.
	 *
	 * Uses a WordPress transient lock to prevent concurrent refreshes.
	 *
	 * @return AccessToken|null New token if refreshed, null if no refresh needed
	 * @since 3.19.0
	 */
	public function refreshTokenIfNeeded(): ?AccessToken {
		$settings = new IntegrationSettings();

		// Only applicable for client_credentials auth
		if ( $settings->getAuthMethodOrDefault() !== 'client_credentials' ) {
			return null;
		}

		try {
			$currentToken = $settings->getAccessToken();
		} catch ( \Throwable $e ) {
			return null;
		}

		$expiresAt = $currentToken->getExpiresAt();
		if ( ! $expiresAt ) {
			return null;
		}

		// Check if token expires within the next 24 hours
		$expiresTimestamp = strtotime( $expiresAt );
		if ( $expiresTimestamp === false ) {
			return null;
		}

		$bufferSeconds = 24 * 60 * 60; // 24 hours
		if ( time() < ( $expiresTimestamp - $bufferSeconds ) ) {
			return null; // Token is still valid
		}

		// Acquire lock to prevent concurrent refreshes (per-environment)
		$lockKey = self::REFRESH_LOCK_TRANSIENT . '_' . Environment::getCurrentKey();
		if ( get_transient( $lockKey ) ) {
			return null; // Another process is refreshing
		}
		set_transient( $lockKey, true, self::REFRESH_LOCK_DURATION );

		try {
			$clientId     = $settings->getOauthClientId();
			$clientSecret = $settings->getOauthClientSecret();

			$tokenResponse = $this->exchangeCredentials( $clientId, $clientSecret );

			$expiresAt = isset( $tokenResponse->expires_in )
				? date( 'Y-m-d H:i:s', time() + $tokenResponse->expires_in )
				: null;

			$newToken = new AccessToken(
				$tokenResponse->access_token,
				isset( $tokenResponse->scopes ) ? (array) $tokenResponse->scopes : array(),
				'client_credentials',
				$expiresAt
			);

			$settings->setAccessToken( $newToken );

			LogService::info( 'OAuth access token refreshed successfully.' );

			return $newToken;
		} catch ( \Throwable $e ) {
			LogService::error( 'OAuth token refresh failed: ' . $e->getMessage() );
			return null;
		} finally {
			delete_transient( $lockKey );
		}
	}

	/**
	 * Compute the context fingerprint for this WordPress installation.
	 *
	 * SHA-256 hash of "woocommerce:{domain}:{db_prefix}"
	 *
	 * @return string 64-character hex SHA-256 hash
	 * @since 3.19.0
	 */
	public function computeContextFingerprint(): string {
		global $wpdb;

		$domain   = wp_parse_url( site_url(), PHP_URL_HOST );
		$dbPrefix = $wpdb->prefix;

		return hash( 'sha256', 'woocommerce:' . $domain . ':' . $dbPrefix );
	}

	/**
	 * Get structured context metadata for this WordPress installation.
	 *
	 * @return array Structured metadata about the platform
	 * @since 3.19.0
	 */
	public function getContextMetadata(): array {
		global $wp_version;

		return array(
			'platform'         => 'woocommerce',
			'platform_version' => defined( 'WC_VERSION' ) ? WC_VERSION : '0',
			'backend'          => 'wordpress',
			'backend_version'  => $wp_version ?: '0',
			'integration_version' => defined( 'WEECONNECT_VERSION' ) ? WEECONNECT_VERSION : '0',
		);
	}

	/**
	 * Fetch caller identity from the backend using the current access token.
	 *
	 * @param string $accessToken
	 * @return object Response containing merchant, employee, app, and pakms info
	 * @throws WeeConnectPayException
	 * @since 3.19.0
	 */
	public function fetchCallerIdentity( string $accessToken ): object {
		$apiDomain = WeeConnectPayAPI::getApiDomain();
		$endpoint  = $apiDomain . '/v1/get-caller-identity';

		$fingerprint = $this->computeContextFingerprint();
		$metadata    = $this->getContextMetadata();

		$response = wp_remote_get(
			$endpoint,
			array(
				'timeout' => 60,
				'headers' => array(
					'Authorization'             => 'Bearer ' . $accessToken,
					'Content-Type'              => 'application/json',
					'Accept'                    => 'application/json',
					'X-WCP-Context-Fingerprint' => $fingerprint,
					'X-WCP-Context-Metadata'    => wp_json_encode( $metadata ),
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			$errorMessage = $response->get_error_message();
			LogService::error( 'Caller identity fetch failed (WP Error): ' . $errorMessage );
			throw new WeeConnectPayException(
				'Failed to fetch account information: ' . $errorMessage,
				ExceptionCode::UNHANDLED_EXCEPTION
			);
		}

		$httpCode     = wp_remote_retrieve_response_code( $response );
		$responseBody = json_decode( wp_remote_retrieve_body( $response ) );

		if ( $httpCode !== 200 ) {
			$errorMessage = isset( $responseBody->message ) ? $responseBody->message : 'Unknown error';
			LogService::error( 'Caller identity fetch failed (HTTP ' . $httpCode . '): ' . $errorMessage );
			throw new WeeConnectPayException(
				'Failed to fetch account information: ' . $errorMessage,
				ExceptionCode::UNHANDLED_EXCEPTION
			);
		}

		return $responseBody;
	}
}
