<?php
/**
 * OAuth flow handlers for Mail Doctor.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use MailDoctor\Helpers\Settings;
use WP_Error;

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

/**
 * Handles OAuth initiation, callbacks, and token refresh.
 */
class OAuth {


	/**
	 * Action slug for notices.
	 */
	const NOTICE_KEY = 'mail_doctor_oauth_notice';

	/**
	 * Initialize admin-post handlers.
	 *
	 * @return void
	 */
	public static function init(): void {
		add_action( 'admin_post_mail_doctor_oauth_start', array( __CLASS__, 'start' ) );
		add_action( 'admin_post_mail_doctor_oauth_callback', array( __CLASS__, 'callback' ) );
	}

	/**
	 * Begin OAuth authorization flow.
	 *
	 * @return void
	 */
	public static function start(): void {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You are not allowed to connect OAuth providers.', 'mail-doctor' ) );
		}

			check_admin_referer( 'mail_doctor_oauth_start' );

			$profile_id = isset( $_GET['profile_id'] ) ? absint( wp_unslash( $_GET['profile_id'] ) ) : 0;
			$provider   = isset( $_GET['provider'] ) ? sanitize_key( wp_unslash( $_GET['provider'] ) ) : '';

		if ( $profile_id <= 0 || '' === $provider ) {
			wp_die( esc_html__( 'Invalid OAuth initiation request.', 'mail-doctor' ) );
		}

		$details = Profiles::oauth_details( $profile_id );

		if ( empty( $details ) ) {
			wp_die( esc_html__( 'Profile is not configured for OAuth.', 'mail-doctor' ) );
		}

		if ( 'oauth2' !== ( Profiles::find( $profile_id )['auth'] ?? '' ) ) {
			wp_die( esc_html__( 'Profile is using password authentication.', 'mail-doctor' ) );
		}

		if ( '' === $details['client_id'] || '' === $details['client_secret'] ) {
			wp_die( esc_html__( 'Client ID and Client Secret must be configured before connecting.', 'mail-doctor' ) );
		}

		$state = wp_generate_password( 32, false, false );

		set_transient(
			'mail_doctor_oauth_state_' . $state,
			array(
				'profile_id' => $profile_id,
				'provider'   => $provider,
				'user_id'    => get_current_user_id(),
			),
			10 * MINUTE_IN_SECONDS
		);

		$auth_url = self::authorize_url( $provider, $details, $state );

		if ( is_wp_error( $auth_url ) ) {
			wp_die( esc_html( $auth_url->get_error_message() ) );
		}

		wp_safe_redirect( $auth_url );
		exit;
	}

	/**
	 * Handle OAuth callback.
	 *
	 * @return void
	 */
	public static function callback(): void {
		// Verify user has capability to manage OAuth.
		if ( ! current_user_can( 'manage_woocommerce' ) && ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to manage OAuth providers.', 'mail-doctor' ) );
		}

		// The 'state' parameter acts as a nonce here, verified against the transient below.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$error = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : '';
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$code     = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
		$redirect = self::settings_url();

		if ( '' === $state ) {
			self::store_notice( __( 'Missing OAuth state. Please try again.', 'mail-doctor' ), 'error' );
			wp_safe_redirect( $redirect );
			exit;
		}

		$payload = get_transient( 'mail_doctor_oauth_state_' . $state );
		delete_transient( 'mail_doctor_oauth_state_' . $state );

		if ( ! is_array( $payload ) || empty( $payload['profile_id'] ) ) {
			self::store_notice( __( 'The OAuth session has expired. Please start again.', 'mail-doctor' ), 'error' );
			wp_safe_redirect( $redirect );
			exit;
		}

		// Verify current user matches the user who initiated OAuth flow.
		$initiating_user_id = isset( $payload['user_id'] ) ? (int) $payload['user_id'] : 0;
		if ( $initiating_user_id > 0 && get_current_user_id() !== $initiating_user_id ) {
			self::store_notice( __( 'OAuth session user mismatch. Please try again.', 'mail-doctor' ), 'error' );
			wp_safe_redirect( $redirect );
			exit;
		}

		if ( $error ) {
			/* translators: %s: error message */
			self::store_notice( sprintf( __( 'OAuth authorization failed: %s', 'mail-doctor' ), $error ), 'error' );
			wp_safe_redirect( $redirect );
			exit;
		}

		if ( '' === $code ) {
			self::store_notice( __( 'OAuth authorization code missing.', 'mail-doctor' ), 'error' );
			wp_safe_redirect( $redirect );
			exit;
		}

		$profile_id = (int) $payload['profile_id'];
		$provider   = sanitize_key( $payload['provider'] ?? '' );

		$details = Profiles::oauth_details( $profile_id );

		if ( empty( $details ) ) {
			self::store_notice( __( 'Profile configuration not found.', 'mail-doctor' ), 'error' );
			wp_safe_redirect( $redirect );
			exit;
		}

		$token = self::exchange_code( $provider, $details, $code );

		if ( is_wp_error( $token ) ) {
			self::store_notice( $token->get_error_message(), 'error' );
			wp_safe_redirect( $redirect );
			exit;
		}

		Profiles::update_oauth_tokens(
			$profile_id,
			array(
				'access_token'  => $token['access_token'],
				'refresh_token' => $token['refresh_token'],
				'expires_at'    => $token['expires_at'],
			)
		);

		self::store_notice(
			sprintf(
				/* translators: %s: profile name */
				__( 'OAuth connection successful for profile "%s".', 'mail-doctor' ),
				Profiles::find( $profile_id )['name'] ?? '#' . $profile_id
			),
			'updated'
		);

		wp_safe_redirect( $redirect );
		exit;
	}

	/**
	 * Ensure access token is valid and refresh if necessary.
	 *
	 * @param array<string, mixed> $credentials Profile credential payload.
	 * @return array<string, mixed>
	 */
	public static function ensure_credentials( array $credentials ): array {
		if ( empty( $credentials['secret'] ) || 'oauth2' !== ( $credentials['auth'] ?? '' ) ) {
			return $credentials;
		}

		$secret     = $credentials['secret'];
		$expires    = isset( $secret['expires_at'] ) ? (int) $secret['expires_at'] : 0;
		$provider   = sanitize_key( $secret['provider'] ?? '' );
		$profile_id = isset( $credentials['id'] ) ? (int) $credentials['id'] : 0;

		if ( $expires > time() + 60 ) {
			return $credentials;
		}

		if ( empty( $secret['refresh_token'] ) ) {
			return $credentials;
		}

		$details = Profiles::oauth_details( $profile_id );

		if ( empty( $details ) ) {
			return $credentials;
		}

		$refresh = self::refresh_token( $provider ?: ( $details['provider'] ?? '' ), $details );

		if ( is_wp_error( $refresh ) ) {
			return $credentials;
		}

		Profiles::update_oauth_tokens(
			$profile_id,
			array(
				'access_token'  => $refresh['access_token'],
				'refresh_token' => $refresh['refresh_token'],
				'expires_at'    => $refresh['expires_at'],
			)
		);

		$credentials['secret']['access_token']  = $refresh['access_token'];
		$credentials['secret']['refresh_token'] = $refresh['refresh_token'];
		$credentials['secret']['expires_at']    = $refresh['expires_at'];

		return $credentials;
	}

	/**
	 * Obtain authorization URL for provider.
	 *
	 * @param string               $provider Provider key.
	 * @param array<string, mixed> $details  Profile OAuth details.
	 * @param string               $state    State token.
	 * @return string|WP_Error
	 */
	protected static function authorize_url( string $provider, array $details, string $state ) {
		$redirect_uri = $details['redirect_uri'] ?? Profiles::oauth_redirect_uri();

		if ( 'gmail' === $provider || 'google' === $provider ) {
			$scope = $details['scope'] ?: 'https://mail.google.com/';

			$params = array(
				'client_id'              => $details['client_id'],
				'redirect_uri'           => $redirect_uri,
				'response_type'          => 'code',
				'scope'                  => $scope,
				'access_type'            => 'offline',
				'include_granted_scopes' => 'true',
				'prompt'                 => 'consent',
				'state'                  => $state,
			);

			return add_query_arg( $params, 'https://accounts.google.com/o/oauth2/v2/auth' );
		}

		if ( in_array( $provider, array( 'outlook', 'office365', 'microsoft' ), true ) ) {
			$scope  = $details['scope'] ?: 'https://outlook.office365.com/SMTP.Send offline_access';
			$tenant = $details['tenant'] ?: 'common';
			$params = array(
				'client_id'     => $details['client_id'],
				'redirect_uri'  => $redirect_uri,
				'response_type' => 'code',
				'scope'         => $scope,
				'response_mode' => 'query',
				'state'         => $state,
			);

			return add_query_arg( $params, 'https://login.microsoftonline.com/' . rawurlencode( $tenant ) . '/oauth2/v2.0/authorize' );
		}

		return new WP_Error( 'mail_doctor_unknown_provider', __( 'Unsupported OAuth provider.', 'mail-doctor' ) );
	}

	/**
	 * Exchange authorization code for tokens.
	 *
	 * @param string               $provider Provider key.
	 * @param array<string, mixed> $details  OAuth details.
	 * @param string               $code     Authorization code.
	 * @return array<string, mixed>|WP_Error
	 */
	protected static function exchange_code( string $provider, array $details, string $code ) {
		$redirect_uri = $details['redirect_uri'] ?? Profiles::oauth_redirect_uri();

		if ( '' === $details['client_secret'] ) {
			return new WP_Error( 'mail_doctor_missing_secret', __( 'Client secret is not configured for this profile.', 'mail-doctor' ) );
		}

		if ( in_array( $provider, array( 'gmail', 'google' ), true ) ) {
			$response = wp_remote_post(
				'https://oauth2.googleapis.com/token',
				array(
					'timeout' => 20,
					'body'    => array(
						'client_id'     => $details['client_id'],
						'client_secret' => $details['client_secret'],
						'code'          => $code,
						'redirect_uri'  => $redirect_uri,
						'grant_type'    => 'authorization_code',
					),
					'headers' => array(
						'Accept' => 'application/json',
					),
				)
			);

			return self::parse_token_response( $response, true );
		}

		if ( in_array( $provider, array( 'outlook', 'office365', 'microsoft' ), true ) ) {
			$tenant         = $details['tenant'] ?: 'common';
			$token_endpoint = 'https://login.microsoftonline.com/' . rawurlencode( $tenant ) . '/oauth2/v2.0/token';
			$response       = wp_remote_post(
				$token_endpoint,
				array(
					'timeout' => 20,
					'body'    => array(
						'client_id'     => $details['client_id'],
						'client_secret' => $details['client_secret'],
						'code'          => $code,
						'redirect_uri'  => $redirect_uri,
						'grant_type'    => 'authorization_code',
					),
					'headers' => array(
						'Accept' => 'application/json',
					),
				)
			);

			return self::parse_token_response( $response, true );
		}

		return new WP_Error( 'mail_doctor_unknown_provider', __( 'Unsupported OAuth provider.', 'mail-doctor' ) );
	}

	/**
	 * Refresh OAuth access token.
	 *
	 * @param string               $provider Provider key.
	 * @param array<string, mixed> $details  OAuth details with refresh token.
	 * @return array<string, mixed>|WP_Error
	 */
	protected static function refresh_token( string $provider, array $details ) {
		if ( empty( $details['refresh_token'] ) ) {
			return new WP_Error( 'mail_doctor_missing_refresh_token', __( 'Refresh token is missing. Reconnect the provider.', 'mail-doctor' ) );
		}

		if ( '' === $details['client_secret'] ) {
			return new WP_Error( 'mail_doctor_missing_secret', __( 'Client secret is not configured.', 'mail-doctor' ) );
		}

		if ( in_array( $provider, array( 'gmail', 'google' ), true ) ) {
			$response = wp_remote_post(
				'https://oauth2.googleapis.com/token',
				array(
					'timeout' => 20,
					'body'    => array(
						'client_id'     => $details['client_id'],
						'client_secret' => $details['client_secret'],
						'refresh_token' => $details['refresh_token'],
						'grant_type'    => 'refresh_token',
					),
					'headers' => array(
						'Accept' => 'application/json',
					),
				)
			);

			return self::parse_token_response( $response, false, $details['refresh_token'] );
		}

		if ( in_array( $provider, array( 'outlook', 'office365', 'microsoft' ), true ) ) {
			$tenant   = $details['tenant'] ?: 'common';
			$response = wp_remote_post(
				'https://login.microsoftonline.com/' . rawurlencode( $tenant ) . '/oauth2/v2.0/token',
				array(
					'timeout' => 20,
					'body'    => array(
						'client_id'     => $details['client_id'],
						'client_secret' => $details['client_secret'],
						'refresh_token' => $details['refresh_token'],
						'grant_type'    => 'refresh_token',
						'scope'         => $details['scope'] ?: 'https://outlook.office365.com/SMTP.Send offline_access',
					),
					'headers' => array(
						'Accept' => 'application/json',
					),
				)
			);

			return self::parse_token_response( $response, false, $details['refresh_token'] );
		}

		return new WP_Error( 'mail_doctor_unknown_provider', __( 'Unsupported OAuth provider.', 'mail-doctor' ) );
	}

	/**
	 * Parse OAuth token response.
	 *
	 * @param array|WP_Error $response HTTP response.
	 * @param bool           $expect_refresh Whether refresh token expected.
	 * @param string         $fallback_refresh Optional fallback refresh token.
	 * @return array<string, mixed>|WP_Error
	 */
	protected static function parse_token_response( $response, bool $expect_refresh = false, string $fallback_refresh = '' ) {
		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'mail_doctor_oauth_http', $response->get_error_message() );
		}

		$code = wp_remote_retrieve_response_code( $response );

		if ( $code < 200 || $code >= 300 ) {
			$body = wp_remote_retrieve_body( $response );
			return new WP_Error(
				'mail_doctor_oauth_http',
				sprintf(
					/* translators: %s: HTTP status code */
					__( 'OAuth provider returned an error (HTTP %s).', 'mail-doctor' ),
					$code
				) . ' ' . $body
			);
		}

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

		if ( ! is_array( $data ) || empty( $data['access_token'] ) ) {
			return new WP_Error( 'mail_doctor_oauth_invalid_response', __( 'Unable to parse OAuth token response.', 'mail-doctor' ) );
		}

		$refresh_token = $data['refresh_token'] ?? ( $expect_refresh ? '' : $fallback_refresh );

		if ( $expect_refresh && '' === $refresh_token ) {
			return new WP_Error( 'mail_doctor_oauth_missing_refresh', __( 'OAuth provider did not return a refresh token.', 'mail-doctor' ) );
		}

		$expires_in = isset( $data['expires_in'] ) ? (int) $data['expires_in'] : 3600;

		return array(
			'access_token'  => $data['access_token'],
			'refresh_token' => $refresh_token ?: $fallback_refresh,
			'expires_at'    => time() + max( 60, $expires_in ),
		);
	}

	/**
	 * Store transient notice for later display.
	 *
	 * @param string $message Notice text.
	 * @param string $type    'updated' or 'error'.
	 * @return void
	 */
	protected static function store_notice( string $message, string $type = 'updated' ): void {
		set_transient(
			self::NOTICE_KEY,
			array(
				'type'    => $type,
				'message' => $message,
			),
			30
		);
	}

	/**
	 * Build plugin settings URL.
	 *
	 * @return string
	 */
	protected static function settings_url(): string {
		return Admin::page_url( 'settings' );
	}

	/**
	 * Expose pending notice for Admin controller.
	 *
	 * @return array<string, string>|null
	 */
	public static function consume_notice(): ?array {
		$notice = get_transient( self::NOTICE_KEY );

		if ( false !== $notice ) {
			delete_transient( self::NOTICE_KEY );
		}

		return is_array( $notice ) ? $notice : null;
	}
}
