<?php
/**
 * Map social profile → WordPress user (link, find, or create).
 *
 * @package WP_PowerSuite\OAuth
 */

namespace WP_PowerSuite\OAuth;

use WP_PowerSuite\OAuth\Accounts\AccountLinkSettings;
use WP_PowerSuite\OAuth\Accounts\SocialAccountStore;

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

final class UserResolver {

	/**
	 * @param array{id: string, email: string, display_name?: string, first_name?: string, last_name?: string, avatar_url?: string} $profile
	 * @param array{
	 *   user_meta_key: string,
	 *   auto_register: bool,
	 *   bypass_registration: bool,
	 *   default_role: string,
	 *   store_avatar: bool,
	 *   avatar_meta_key?: string,
	 *   provider: string,
	 *   email_on_login?: string,
	 *   skip_email_lookup?: bool
	 * } $opts
	 */
	public function findOrCreate( array $profile, array $opts ): \WP_User|\WP_Error {
		$meta_key  = (string) ( $opts['user_meta_key'] ?? '' );
		$provider  = sanitize_key( (string) ( $opts['provider'] ?? '' ) );
		$ext_id    = (string) ( $profile['id'] ?? '' );
		$email     = (string) ( $profile['email'] ?? '' );
		$email     = is_email( $email ) ? $email : '';
		$autoreg   = (bool) ( $opts['auto_register'] ?? false );
		$bypass    = (bool) ( $opts['bypass_registration'] ?? false );
		$def_role  = in_array( $opts['default_role'] ?? 'subscriber', [ 'subscriber', 'contributor', 'author', 'editor' ], true )
			? (string) ( $opts['default_role'] ?? 'subscriber' )
			: 'subscriber';
		$email_on  = in_array( ( $opts['email_on_login'] ?? 'auto' ), [ 'auto', 'manual' ], true )
			? (string) ( $opts['email_on_login'] ?? 'auto' )
			: 'auto';

		if ( $ext_id === '' && $email === '' ) {
			return new \WP_Error( 'profile', __( 'Invalid profile: missing id and email.', 'wp-powersuite' ) );
		}
		if ( $provider === '' ) {
			return new \WP_Error( 'config', __( 'Provider not configured.', 'wp-powersuite' ) );
		}

		$is_noreply   = ( $email !== '' && str_contains( $email, '@users.noreply.github.com' ) );
		$skip_email   = ! empty( $opts['skip_email_lookup'] ) || $is_noreply;
		if ( $ext_id !== '' ) {
			$by_sub = SocialAccountStore::findUserIdBySubject( $provider, $ext_id );
			if ( $by_sub !== null ) {
				$user = get_user_by( 'id', (int) $by_sub );
				if ( $user ) {
					$this->syncIdentity( (int) $user->ID, $profile, $opts );
					return $user;
				}
			}
			if ( $meta_key !== '' ) {
				$by_legacy = get_users(
					[
						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- legacy provider id lookup, one user.
						'meta_key'   => $meta_key,
						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- exact external id, limited to one user.
						'meta_value' => $ext_id,
						'number'     => 1,
					]
				);
				if ( $by_legacy && isset( $by_legacy[0] ) && $by_legacy[0] instanceof \WP_User ) {
					$this->syncIdentity( (int) $by_legacy[0]->ID, $profile, $opts );
					return $by_legacy[0];
				}
			}
		}

		if ( $email !== '' && ! $skip_email ) {
			$by = get_user_by( 'email', $email );
			if ( $by ) {
				if ( $email_on === 'manual' ) {
					return new \WP_Error(
						'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' )
					);
				}
				if ( ! self::isEmailVerifiedForAutoLink( $provider, $profile, $email ) ) {
					return new \WP_Error(
						'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' )
					);
				}
				if ( $ext_id !== '' ) {
					$conflict = SocialAccountStore::isSubjectUsedByOther( $provider, $ext_id, (int) $by->ID );
					if ( $conflict !== null ) {
						return new \WP_Error( 'taken', __( 'This sign-in is already linked to a different user.', 'wp-powersuite' ) );
					}
				}
				$st = SocialAccountStore::addOrUpdate( (int) $by->ID, $profile, $this->toStoreOpts( $opts, $profile ) );
				if ( is_wp_error( $st ) ) {
					return $st;
				}
				return $by;
			}
		}

		if ( ! $autoreg ) {
			if ( $is_noreply ) {
				return new \WP_Error(
					'no_account',
					__( 'No matching account. Add a verified email to your GitHub profile or sign in with your password first.', 'wp-powersuite' )
				);
			}
			return new \WP_Error(
				'no_account',
				__( 'No matching account. Registration may be disabled for this sign-in method.', 'wp-powersuite' )
			);
		}

		if ( $is_noreply ) {
			return new \WP_Error(
				'email_required',
				__( 'GitHub has no public verified email. Add a verified email to your GitHub profile, then try again.', 'wp-powersuite' )
			);
		}

		$wp_can_reg = (bool) get_option( 'users_can_register' );
		if ( is_multisite() ) {
			$network_reg = (string) get_site_option( 'registration', 'none' );
			if ( $network_reg === 'none' ) {
				$wp_can_reg = false;
			}
		}
		if ( ! $wp_can_reg && ! $bypass ) {
			return new \WP_Error( 'registration_disabled', __( 'New user registration is disabled on this site.', 'wp-powersuite' ) );
		}

		if ( $email === '' || ! is_email( $email ) ) {
			return new \WP_Error( 'email_required', __( 'A verified email is required to create an account.', 'wp-powersuite' ) );
		}

		$username = $this->makeUsername( $profile );
		$pass     = wp_generate_password( 32, true, true );
		$uid      = wp_create_user( $username, $pass, $email );
		if ( is_wp_error( $uid ) ) {
			return $uid;
		}
		$user = get_user_by( 'id', $uid );
		if ( ! $user ) {
			return new \WP_Error( 'create_failed', __( 'Could not create user.', 'wp-powersuite' ) );
		}

		wp_update_user(
			[
				'ID'           => $uid,
				'display_name' => (string) ( $profile['display_name'] ?? $email ),
				'first_name'   => (string) ( $profile['first_name'] ?? '' ),
				'last_name'    => (string) ( $profile['last_name'] ?? '' ),
			]
		);

		$st = SocialAccountStore::addOrUpdate( (int) $uid, $profile, $this->toStoreOpts( $opts, $profile ) );
		if ( is_wp_error( $st ) ) {
			return $st;
		}
		$user->set_role( $def_role );

		return $user;
	}

	/**
	 * Connect flow: attach provider to a logged-in user (after OAuth, session verified in AbstractProvider).
	 *
	 * @param array<string, mixed> $opts Same shape as getUserResolverOptions.
	 */
	public function linkToUser( int $user_id, array $profile, array $opts ): \WP_User|\WP_Error {
		$user_id = (int) $user_id;
		if ( $user_id <= 0 || ! AccountLinkSettings::isLinkingEnabled() ) {
			return new \WP_Error( 'wpps_social', __( 'Account linking is disabled or invalid.', 'wp-powersuite' ) );
		}
		$provider  = sanitize_key( (string) ( $opts['provider'] ?? '' ) );
		$ext_id    = (string) ( $profile['id'] ?? '' );
		$email     = (string) ( $profile['email'] ?? '' );
		$email     = is_email( $email ) ? $email : '';
		if ( $provider === '' || $ext_id === '' ) {
			return new \WP_Error( 'profile', __( 'Invalid profile from provider.', 'wp-powersuite' ) );
		}
		$user = get_user_by( 'id', $user_id );
		if ( ! $user || ! ( $user instanceof \WP_User ) ) {
			return new \WP_Error( 'user', __( 'User not found.', 'wp-powersuite' ) );
		}
		/**
		 * Fires before linking a social provider in the “Connect” flow.
		 *
		 * @param int   $user_id
		 * @param array $profile
		 * @param string $provider
		 */
		do_action( 'wpps_social_before_link', $user_id, $profile, $provider );
		$taken = SocialAccountStore::isSubjectUsedByOther( $provider, $ext_id, $user_id );
		if ( $taken !== null ) {
			return new \WP_Error( 'taken', __( 'This sign-in is already linked to a different user.', 'wp-powersuite' ) );
		}
		if ( AccountLinkSettings::requireEmailMatchForConnect() && $email === '' ) {
			return new \WP_Error( 'email_required', __( 'A verified email is required to connect this provider to your account.', 'wp-powersuite' ) );
		}
		// GitHub often exposes only a noreply address; it cannot match the WP user email, but the OAuth id is still authoritative.
		$is_github_noreply = ( 'github' === $provider && $email !== '' && str_contains( strtolower( $email ), '@users.noreply.github.com' ) );
		if ( AccountLinkSettings::requireEmailMatchForConnect() && $email !== '' && ! $is_github_noreply ) {
			$wp_email = (string) $user->user_email;
			if ( strtolower( $email ) !== strtolower( $wp_email ) ) {
				/**
				 * Whether to allow linking when provider email != WP user email.
				 *
				 * @param bool  $deny
				 * @param int   $user_id
				 * @param string $provider
				 * @param array  $profile
				 */
				$deny = (bool) apply_filters( 'wpps_social_block_email_mismatch_on_connect', true, $user_id, $provider, $profile );
				if ( $deny ) {
					return new \WP_Error( 'email_mismatch', __( 'The email from the provider does not match your account email.', 'wp-powersuite' ) );
				}
			}
		}
		$st = SocialAccountStore::addOrUpdate( $user_id, $profile, $this->toStoreOpts( $opts, $profile ) );
		if ( is_wp_error( $st ) ) {
			return $st;
		}
		$user = get_user_by( 'id', $user_id );
		return ( $user && $user instanceof \WP_User ) ? $user : new \WP_Error( 'user', __( 'Could not load user.', 'wp-powersuite' ) );
	}

	/**
	 * @param array<string, mixed> $opts
	 */
	private function toStoreOpts( array $opts, array $profile ): array {
		return [
			'provider'        => (string) ( $opts['provider'] ?? '' ),
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- options key name, not a query.
			'meta_key'        => (string) ( $opts['user_meta_key'] ?? '' ),
			'avatar_meta_key' => (string) ( $opts['avatar_meta_key'] ?? 'wpps_oauth_avatar' ),
			'store_avatar'    => (bool) ( $opts['store_avatar'] ?? false ),
		];
	}

	/**
	 * @param array<string, mixed> $opts
	 */
	private function syncIdentity( int $user_id, array $profile, array $opts ): void {
		$st = SocialAccountStore::addOrUpdate( $user_id, $profile, $this->toStoreOpts( $opts, $profile ) );
		if ( is_wp_error( $st ) ) {
			return;
		}
		$av_mkey = (string) ( $opts['avatar_meta_key'] ?? 'wpps_oauth_avatar' );
		$this->maybeStoreAvatar( $user_id, $profile, (bool) ( $opts['store_avatar'] ?? false ), $av_mkey, (string) ( $opts['provider'] ?? '' ) );
	}

	/**
	 * @param array<string, mixed> $profile
	 */
	private function maybeStoreAvatar( int $user_id, array $profile, bool $storeav, string $av_mkey, string $provider ): void {
		if ( ! $storeav || empty( $profile['avatar_url'] ) ) {
			return;
		}
		$url  = esc_url_raw( (string) $profile['avatar_url'] );
		$g_av = ( 'google' === $provider ) ? 'wpps_google_avatar' : ( ( 'github' === $provider ) ? 'wpps_github_avatar' : $av_mkey );
		update_user_meta( $user_id, $g_av, $url );
	}

	/**
	 * @param array{display_name?: string, email?: string, user_login?: string, login?: string} $profile
	 */
	public function makeUsername( array $profile ): string {
		$from_login = (string) ( $profile['user_login'] ?? $profile['login'] ?? '' );
		$from_email = (string) ( $profile['email'] ?? '' );
		$base       = $from_login;
		if ( $base === '' && $from_email !== '' ) {
			$base = explode( '@', $from_email, 2 )[0] ?? 'user';
		}
		$base     = (string) sanitize_user( (string) $base, true );
		$base     = (string) preg_replace( '/[^a-z0-9_-]+/i', '', $base );
		$base     = strtolower( (string) $base );
		$base     = ( $base !== '' ) ? $base : 'user';
		$username = $base;
		$i        = 0;
		while ( username_exists( $username ) || $this->isUsernameIllegal( $username ) ) {
			$username = $base . ( ++$i );
		}
		return $username;
	}

	private function isUsernameIllegal( string $username ): bool {
		$normalized = sanitize_user( trim( $username ), true );
		if ( $normalized === '' ) {
			return false;
		}

		/** @var array<int, string> $illegal */
		$illegal = (array) apply_filters( 'illegal_user_logins', [] );
		if ( $illegal === [] ) {
			return false;
		}

		$needle = strtolower( $normalized );

		return in_array( $needle, array_map( 'strtolower', array_filter( $illegal, 'is_string' ) ), true );
	}

	/**
	 * Whether auto-link by email may proceed for this provider/profile pair.
	 *
	 * Google validates email_verified before UserResolver runs; trust its email when present.
	 *
	 * @param array<string, mixed> $profile
	 */
	private static function isEmailVerifiedForAutoLink( string $provider, array $profile, string $email ): bool {
		if ( ! empty( $profile['email_verified'] ) ) {
			return true;
		}
		if ( 'google' === $provider && is_email( $email ) ) {
			return true;
		}
		if ( 'facebook' === $provider && is_email( $email ) ) {
			return true;
		}
		return false;
	}
}
