<?php
/**
 * Base OAuth2 provider: state, token exchange, and login.
 *
 * @package WP_PowerSuite\OAuth
 */

namespace WP_PowerSuite\OAuth;

use WP_PowerSuite\Core\Contracts\ModuleSettingsRepository;
use WP_PowerSuite\OAuth\Accounts\AccountLinkSettings;
use WP_PowerSuite\OAuth\Helpers\Logger;
use WP_PowerSuite\OAuth\Helpers\RateLimiter;
use WP_PowerSuite\OAuth\Helpers\UrlHelper;

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

abstract class AbstractProvider {

	protected StateManager $state;
	protected TokenHandler $tasks;
	protected UserResolver $users;

	public function __construct( protected ModuleSettingsRepository $settings ) {
		$this->state = new StateManager();
		$this->tasks = new TokenHandler();
		$this->users = new UserResolver();
	}

	/**
	 * Short callback slug (wpps_auth=...).
	 */
	abstract public function getProviderSlug(): string;

	/**
	 * Module id for options (e.g. google-sign-in).
	 */
	abstract public function getModuleId(): string;

	/**
	 * Public redirect URI to register in provider console.
	 */
	abstract public function getRedirectUri(): string;

	/**
	 * true = use home_url/?wpps_auth=slug (recommended). false = legacy admin-ajax.
	 */
	public function usePublicCallbackUrl(): bool {
		return true;
	}

	/**
	 * Legacy action for admin-ajax.php (if usePublicCallbackUrl is false in subclass).
	 */
	public function getLegacyAction(): string {
		return '';
	}

	/**
	 * @param int $link_user_id 0 = sign-in; if > 0, OAuth will link this provider to that user (Connect).
	 * @return string Authorization URL
	 */
	abstract public function getAuthorizationUrl( string $redirect_after = '', int $link_user_id = 0 ): string;

	/**
	 * Exchange `code` for profile array { id, email, ... }.
	 *
	 * @return array<string, mixed>|\WP_Error
	 */
	abstract public function exchangeCodeForProfile( string $code, string $redirect_uri_used ): array|\WP_Error;

	/**
	 * Options passed to UserResolver.
	 *
	 * @return array<string, mixed>
	 */
	abstract public function getUserResolverOptions(): array;

	/**
	 * Default/safe path after successful login.
	 */
	public function getDefaultPostLoginUrl(): string {
		return $this->getPostLoginRedirect( 'login' );
	}

	/**
	 * Configurable post-login or post–account-link URL (per module, then filter).
	 *
	 * @param string $context 'login' or 'link'.
	 */
	protected function getPostLoginRedirect( string $context = 'login' ): string {
		$m = $this->settings->all( [] );
		$u = (string) ( $m['redirect_after_login'] ?? '' );
		// “Connect” flow: default to WooCommerce “Connected accounts” (not wp-admin) when no custom URL is set.
		if ( $u === '' && 'link' === $context && class_exists( 'WooCommerce' ) && function_exists( 'wc_get_account_endpoint_url' ) && (int) wc_get_page_id( 'myaccount' ) > 0 ) {
			$e = (string) wc_get_account_endpoint_url( 'wpps-social' );
			if ( $e !== '' ) {
				$u = (string) add_query_arg( 'wpps_social_ok', 'linked', $e );
			}
		}
		if ( $u === '' ) {
			$u = home_url( '/' );
		}
		/**
		 * Post-login / post–connect redirect.
		 *
		 * @param string $url      Candidate URL.
		 * @param string $provider Provider slug.
		 * @param string $context  'login' or 'link'.
		 */
		return (string) apply_filters( 'wpps_social_post_login_redirect', $u, $this->getProviderSlug(), $context );
	}

	/**
	 * Redirect URI used in token exchange (must match the authorize request).
	 */
	public function getRedirectUriForCurrentRequest(): string {
		$legacy = $this->getLegacyAction();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( $legacy !== '' && isset( $_GET['action'] ) && (string) wp_unslash( (string) $_GET['action'] ) === $legacy ) {
			return \WP_PowerSuite\OAuth\Helpers\UrlHelper::callbackUrl( $this->getProviderSlug(), true, $legacy );
		}
		return $this->getRedirectUri();
	}

	/**
	 * Handle query params (Google/GitHub: code, state, error).
	 */
	public function handleQueryCallback(): void {
		$error = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['error'] ) ) : '';
		if ( $error !== '' ) {
			Logger::log( 'Provider OAuth error', [ 'p' => $this->getProviderSlug(), 'error' => $error ] );
			$this->redirectWithError( __( 'Sign-in was cancelled or failed.', 'wp-powersuite' ) );
		}
		$code  = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['code'] ) ) : '';
		$state = isset( $_GET['state'] ) ? (string) wp_unslash( (string) $_GET['state'] ) : '';
		if ( $code === '' || $state === '' ) {
			$this->redirectWithError( __( 'Invalid OAuth response.', 'wp-powersuite' ) );
		}
		$key = $this->getStateStorageKey();
		$ver = $this->state->verifyAndConsume( $key, $state );
		if ( $ver === false ) {
			Logger::log( 'State mismatch', [ 'p' => $this->getProviderSlug() ] );
			$this->redirectWithError( __( 'Security check failed. Please try again.', 'wp-powersuite' ) );
		}
		$link_uid  = (int) ( is_array( $ver ) ? ( $ver['link_user_id'] ?? 0 ) : 0 );
		$after_raw = '';
		if ( is_array( $ver ) && isset( $ver['redirect'] ) && is_string( $ver['redirect'] ) && $ver['redirect'] !== '' ) {
			$after_raw = $ver['redirect'];
		} elseif ( is_array( $ver ) && isset( $ver['verified'] ) ) {
			$after_raw = $this->state->getRedirectFromState( (string) $ver['verified'] );
		}
		$profile   = $this->exchangeCodeForProfile( $code, $this->getRedirectUriForCurrentRequest() );
		if ( is_wp_error( $profile ) ) {
			Logger::log( 'Token/profile error', [ 'e' => $profile->get_error_message(), 'code' => $profile->get_error_code() ] );
			$this->redirectWithError( $this->publicErrorMessage( $profile ), $link_uid > 0 );
		}
		if ( $link_uid > 0 ) {
			if ( ! is_user_logged_in() || (int) get_current_user_id() !== $link_uid ) {
				$this->redirectWithError( __( 'Your session does not match this connection request. Please try again from your account page.', 'wp-powersuite' ), true );
			}
			if ( ! AccountLinkSettings::isLinkingEnabled() ) {
				$this->redirectWithError( __( 'Account linking is disabled.', 'wp-powersuite' ), true );
			}
			$linked = $this->users->linkToUser( $link_uid, $profile, $this->getUserResolverOptions() );
			if ( is_wp_error( $linked ) ) {
				$this->redirectWithError( $this->publicErrorMessage( $linked ), true );
			}
			$def = $this->getPostLoginRedirect( 'link' );
			$red = ( $after_raw !== '' ) ? UrlHelper::safeLoginRedirect( $after_raw, $def ) : $def;
			$this->completeLoginOrRedirect( $linked, $red );
			return;
		}
		$user = $this->users->findOrCreate( $profile, $this->getUserResolverOptions() );
		if ( is_wp_error( $user ) ) {
			$this->redirectWithError( $this->publicErrorMessage( $user ) );
		}
		$def = $this->getPostLoginRedirect( 'login' );
		$red = ( $after_raw !== '' ) ? UrlHelper::safeLoginRedirect( $after_raw, $def ) : $def;
		$this->logUserIn( $user, $red, $after_raw );
	}

	/**
	 * Reuse session when already the same user; otherwise set auth (connect flow may refresh cookie).
	 */
	protected function completeLoginOrRedirect( \WP_User $user, string $url ): void {
		if ( (int) get_current_user_id() === (int) $user->ID ) {
			wp_safe_redirect( $url );
			exit;
		}
		$this->logUserIn( $user, $url );
	}

	public function getStateStorageKey(): string {
		return sanitize_key( $this->getProviderSlug() );
	}

	protected function logUserIn( \WP_User $user, string $url, string $requested_redirect_to = '' ): void {
		$allowed = $this->isUserAllowedToLogin( $user );
		if ( is_wp_error( $allowed ) ) {
			$this->redirectWithError( $this->publicErrorMessage( $allowed ) );
		}
		wp_clear_auth_cookie();
		wp_set_current_user( (int) $user->ID );
		wp_set_auth_cookie( (int) $user->ID, false, is_ssl() );
		/**
		 * Fires when a user logs in (parity with core).
		 */
		do_action( 'wp_login', $user->user_login, $user );
		do_action( 'wp_powersuite_oauth_login_completed', $user );

		if ( class_exists( \RedirectAfterLogin\Module::class ) ) {
			$url = \RedirectAfterLogin\Module::applyPostLoginRedirectFilters( $url, $requested_redirect_to, $user );
		} else {
			$url = (string) wp_validate_redirect( $url, home_url( '/' ) );
		}

		wp_safe_redirect( $url );
		exit;
	}

	abstract protected function errorQueryArg(): string;

	protected function redirectWithError( string $message, bool $return_to_account = false ): void {
		$message = $this->sanitizePublicErrorMessage( $message );
		if ( $return_to_account && class_exists( 'WooCommerce' ) && function_exists( 'wc_get_account_endpoint_url' ) ) {
			$url = wc_get_account_endpoint_url( 'wpps-social' );
			if ( is_string( $url ) && $url !== '' && ! is_wp_error( $url ) ) {
				$url = add_query_arg( 'wpps_social_error', rawurlencode( $message ), $url );
				wp_safe_redirect( $url );
				exit;
			}
		}
		$login = wp_login_url();
		$arg   = $this->errorQueryArg();
		wp_safe_redirect( add_query_arg( $arg, rawurlencode( $message ), $login ) );
		exit;
	}

	/**
	 * If this request is our OAuth callback.
	 */
	public function isThisCallbackRequest(): bool {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET[ UrlHelper::AUTH_QUERY_PARAM ] ) && sanitize_key( (string) wp_unslash( (string) $_GET[ UrlHelper::AUTH_QUERY_PARAM ] ) ) === $this->getProviderSlug() ) {
			return $this->callbackGate();
		}
		$act = $this->getLegacyAction();
		if ( $act !== '' ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( isset( $_GET['action'] ) && (string) wp_unslash( (string) $_GET['action'] ) === $act ) {
				return $this->callbackGate();
			}
		}
		return false;
	}

	private function callbackGate(): bool {
		if ( ! RateLimiter::allow( $this->getProviderSlug() ) ) {
			$this->redirectWithError( __( 'Too many sign-in attempts. Please wait a few minutes.', 'wp-powersuite' ) );
		}
		return true;
	}

	/**
	 * @return true|\WP_Error
	 */
	protected function isUserAllowedToLogin( \WP_User $user ): bool|\WP_Error {
		if ( function_exists( 'wp_is_user_spammy' ) && wp_is_user_spammy( $user ) ) {
			return new \WP_Error( 'blocked', __( 'This account cannot sign in.', 'wp-powersuite' ) );
		}
		if ( isset( $user->user_status ) && (string) $user->user_status !== '0' ) {
			return new \WP_Error( 'blocked', __( 'This account cannot sign in.', 'wp-powersuite' ) );
		}
		return true;
	}

	protected function publicErrorMessage( \WP_Error $error ): string {
		$msg  = (string) $error->get_error_message();
		$known = $this->knownPublicErrorMessages();
		if ( $msg !== '' && in_array( $msg, $known, true ) ) {
			return $msg;
		}

		$map = [
			'profile'                => __( 'Invalid profile from provider.', 'wp-powersuite' ),
			'config'                 => __( 'Provider not configured.', 'wp-powersuite' ),
			'email_exists'           => __( 'An account with this email already exists. Sign in with your password, then you can connect this provider from your account page.', 'wp-powersuite' ),
			'email_unverified'       => __( 'The provider did not verify this email address. Sign in with your password first, then connect this provider from your account page.', 'wp-powersuite' ),
			'taken'                  => __( 'This sign-in is already linked to a different user.', 'wp-powersuite' ),
			'wpps_social_taken'      => __( 'This account is already linked to another user.', 'wp-powersuite' ),
			'no_account'             => __( 'No matching account. Registration may be disabled for this sign-in method.', 'wp-powersuite' ),
			'email_required'         => __( 'A verified email is required to create an account.', 'wp-powersuite' ),
			'registration_disabled'  => __( 'New user registration is disabled on this site.', 'wp-powersuite' ),
			'create_failed'          => __( 'Could not create user.', 'wp-powersuite' ),
			'token'                  => __( 'Could not complete sign-in with the provider. Please try again.', 'wp-powersuite' ),
			'no_email'               => __( 'The provider did not return an email address.', 'wp-powersuite' ),
			'http_error'             => __( 'Could not complete sign-in with the provider. Please try again.', 'wp-powersuite' ),
			'blocked'                => __( 'This account cannot sign in.', 'wp-powersuite' ),
			'wpps_social'            => __( 'Account linking is disabled or invalid.', 'wp-powersuite' ),
			'user'                   => __( 'User not found.', 'wp-powersuite' ),
			'email_mismatch'         => __( 'The email from the provider does not match your account email.', 'wp-powersuite' ),
		];
		$code = (string) $error->get_error_code();
		if ( isset( $map[ $code ] ) ) {
			return $map[ $code ];
		}
		return __( 'Sign-in failed. Please try again or use your password.', 'wp-powersuite' );
	}

	protected function sanitizePublicErrorMessage( string $message ): string {
		$generic = __( 'Sign-in failed. Please try again or use your password.', 'wp-powersuite' );
		$known   = $this->knownPublicErrorMessages();
		return in_array( $message, $known, true ) ? $message : $generic;
	}

	/**
	 * @return list<string>
	 */
	private function knownPublicErrorMessages(): array {
		$generic = __( 'Sign-in failed. Please try again or use your password.', 'wp-powersuite' );
		return [
			__( 'Sign-in was cancelled or failed.', 'wp-powersuite' ),
			__( 'Invalid OAuth response.', 'wp-powersuite' ),
			__( 'Security check failed. Please try again.', 'wp-powersuite' ),
			__( 'Your session does not match this connection request. Please try again from your account page.', 'wp-powersuite' ),
			__( 'Account linking is disabled.', 'wp-powersuite' ),
			__( 'Too many sign-in attempts. Please wait a few minutes.', 'wp-powersuite' ),
			__( 'This account cannot sign in.', 'wp-powersuite' ),
			$generic,
			__( 'Invalid profile from provider.', 'wp-powersuite' ),
			__( 'Provider not configured.', 'wp-powersuite' ),
			__( 'An account with this email already exists. Sign in with your password, then you can connect this provider from your account page.', 'wp-powersuite' ),
			__( 'The provider did not verify this email address. Sign in with your password first, then connect this provider from your account page.', 'wp-powersuite' ),
			__( 'This sign-in is already linked to a different user.', 'wp-powersuite' ),
			__( 'This account is already linked to another user.', 'wp-powersuite' ),
			__( 'No matching account. Registration may be disabled for this sign-in method.', 'wp-powersuite' ),
			__( 'A verified email is required to create an account.', 'wp-powersuite' ),
			__( 'New user registration is disabled on this site.', 'wp-powersuite' ),
			__( 'Could not create user.', 'wp-powersuite' ),
			__( 'Could not complete sign-in with the provider. Please try again.', 'wp-powersuite' ),
			__( 'The provider did not return an email address.', 'wp-powersuite' ),
			__( 'Account linking is disabled or invalid.', 'wp-powersuite' ),
			__( 'User not found.', 'wp-powersuite' ),
			__( 'The email from the provider does not match your account email.', 'wp-powersuite' ),
			__( 'GitHub has no public verified email. Add a verified email to your GitHub profile, then try again.', 'wp-powersuite' ),
			__( 'Google did not verify this email address. Use a verified Google account.', 'wp-powersuite' ),
			__( 'Facebook did not return an email. Add the email permission in your Facebook app.', 'wp-powersuite' ),
			__( 'Could not read Google profile or email.', 'wp-powersuite' ),
			__( 'No matching account. Add a verified email to your GitHub profile or sign in with your password first.', 'wp-powersuite' ),
			__( 'Could not read Facebook profile.', 'wp-powersuite' ),
			__( 'Invalid GitHub profile response.', 'wp-powersuite' ),
			__( 'Failed to get access token from GitHub.', 'wp-powersuite' ),
			__( 'Failed to get access token from Google.', 'wp-powersuite' ),
			__( 'Could not get access token from Facebook.', 'wp-powersuite' ),
		];
	}
}
