<?php
/**
 * Social Login (OAuth) Feature for Guard Dog.
 *
 * Provides social login via OAuth 2.0 Authorization Code flow.
 * Supports Google, Microsoft, and Facebook providers.
 *
 * @package GuardDog
 * @since 1.9.30
 */

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

class Guard_Dog_Social_Login {

    /**
     * Whether delete-user cleanup hooks have been registered.
     *
     * @var bool
     */
    private static $cleanup_hooks_initialized = false;

    /**
     * Transient prefix for OAuth authentication flag (used for 2FA bypass).
     *
     * @var string
     */
    const OAUTH_AUTH_TRANSIENT_PREFIX = 'guard_dog_oauth_authed_';

    /**
     * Transient prefix for OAuth state.
     *
     * @var string
     */
    const STATE_TRANSIENT_PREFIX = 'guard_dog_oauth_';

    /**
     * State transient TTL in seconds (10 minutes).
     *
     * @var int
     */
    const STATE_TTL = 600;

    /**
     * Rate limit for OAuth init requests per IP per window.
     *
     * @var int
     */
    const RATE_LIMIT = 20;

    /**
     * Rate limit window in seconds (15 minutes).
     *
     * @var int
     */
    const RATE_LIMIT_WINDOW = 900;

    /**
     * Activity log instance.
     *
     * @var Guard_Dog_Activity_Log|null
     */
    private $activity_log;

    /**
     * Registered provider instances.
     *
     * @var array<string, Guard_Dog_Social_Provider_Interface>
     */
    private $providers = [];

    /**
     * Constructor.
     *
     * @param Guard_Dog_Activity_Log|null $activity_log Activity log instance.
     */
    public function __construct( ?Guard_Dog_Activity_Log $activity_log = null ) {
        $this->activity_log = $activity_log;
        $this->register_default_providers();
        $this->init_cleanup_hooks();

        if ( $this->is_enabled() ) {
            $this->init_hooks();
        }
    }

    /**
     * Register hooks that should run even when social login is disabled.
     */
    private function init_cleanup_hooks(): void {
        if ( self::$cleanup_hooks_initialized ) {
            return;
        }

        add_action( 'delete_user', [ $this, 'cleanup_user_social_accounts' ], 10, 1 );
        add_action( 'wpmu_delete_user', [ $this, 'cleanup_user_social_accounts' ], 10, 1 );

        self::$cleanup_hooks_initialized = true;
    }

    /**
     * Register default provider implementations.
     */
    private function register_default_providers(): void {
        $this->providers['google']    = new Guard_Dog_Google_Provider();
        $this->providers['microsoft'] = new Guard_Dog_Microsoft_Provider();
        $this->providers['apple']     = new Guard_Dog_Apple_Provider();
    }

    /**
     * Initialize WordPress hooks.
     */
    private function init_hooks(): void {
        // Login form buttons
        add_action( 'login_form', [ $this, 'render_social_login_buttons' ], 15 );
        add_action( 'login_enqueue_scripts', [ $this, 'enqueue_login_assets' ] );

        // OAuth init via admin-ajax (nopriv for login, priv for account linking)
        add_action( 'wp_ajax_nopriv_guard_dog_oauth_init', [ $this, 'handle_oauth_init' ] );
        add_action( 'wp_ajax_guard_dog_oauth_init', [ $this, 'handle_oauth_init' ] );

        // OAuth callback via clean URL path (no query strings, required by Microsoft Azure AD)
        add_action( 'init', [ $this, 'maybe_handle_oauth_callback' ], 5 );

        // Account linking/unlinking (authenticated only)
        add_action( 'wp_ajax_guard_dog_oauth_link', [ $this, 'handle_oauth_link_init' ] );
        add_action( 'wp_ajax_guard_dog_oauth_unlink', [ $this, 'handle_oauth_unlink' ] );

        // User profile section
        add_action( 'show_user_profile', [ $this, 'render_profile_social_accounts' ] );
        add_action( 'edit_user_profile', [ $this, 'render_profile_social_accounts' ] );

        // Override Gravatar with social avatar when available
        add_filter( 'get_avatar_url', [ $this, 'filter_avatar_url' ], 10, 3 );
    }

    /**
     * The URL path for the OAuth callback endpoint.
     *
     * @var string
     */
    const CALLBACK_PATH = 'guard-dog/oauth/callback';

    /**
     * Intercept requests to the OAuth callback URL path.
     *
     * Uses a fixed URL path instead of admin-ajax or REST API to provide a clean
     * callback URL without query strings, as required by Microsoft Azure AD.
     * Works regardless of permalink settings.
     */
    public function maybe_handle_oauth_callback(): void {
        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        // 1. Context: Checking URL path to route OAuth callbacks
        // 2. Only used for string comparison, not output or database
        // 3. The actual OAuth parameters are sanitized in handle_oauth_callback()
        $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
        // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

        $path = trim( (string) wp_parse_url( $request_uri, PHP_URL_PATH ), '/' );

        if ( $path === $this->get_callback_request_path() ) {
            $this->handle_oauth_callback();
            exit;
        }
    }

    /**
     * Check if the social login feature is enabled.
     *
     * @return bool
     */
    public function is_enabled(): bool {
        return (bool) get_option( 'guard_dog_social_login_enabled', false );
    }

    /**
     * Get all registered providers.
     *
     * @return array<string, Guard_Dog_Social_Provider_Interface>
     */
    public function get_registered_providers(): array {
        return $this->providers;
    }

    /**
     * Get only enabled and configured providers.
     *
     * A provider is enabled when:
     * 1. Its individual toggle is on
     * 2. Client ID and Client Secret are both set
     *
     * @return array<string, Guard_Dog_Social_Provider_Interface>
     */
    public function get_enabled_providers(): array {
        $enabled = [];

        foreach ( $this->providers as $id => $provider ) {
            $provider_enabled = get_option( "guard_dog_social_{$id}_enabled", false );

            if ( $provider_enabled && $provider->is_configured() ) {
                $enabled[ $id ] = $provider;
            }
        }

        return $enabled;
    }

    /**
     * Get the OAuth callback URL.
     *
     * Uses a fixed URL path to provide a clean URL without query strings,
     * as required by providers like Microsoft Azure AD. Works regardless
     * of permalink or REST API settings.
     *
     * @return string
     */
    public function get_callback_url(): string {
        return home_url( '/' . self::CALLBACK_PATH );
    }

    /**
     * Get the normalized request path for the OAuth callback.
     *
     * Derives the expected request path from the generated callback URL so
     * installs under a path prefix still match correctly.
     *
     * @return string
     */
    protected function get_callback_request_path(): string {
        return trim( (string) wp_parse_url( $this->get_callback_url(), PHP_URL_PATH ), '/' );
    }

    /**
     * Find a WordPress user by linked social account.
     *
     * @param string $provider Provider ID.
     * @param string $provider_user_id Provider's user ID.
     * @return \WP_User|null
     */
    public function find_user_by_social_account( string $provider, string $provider_user_id ): ?\WP_User {
        global $wpdb;
        $table = Guard_Dog()->db->get_social_accounts_table_name();

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        // Direct query needed for custom table lookup; result is not cached as it must be real-time for auth
        $user_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT user_id FROM {$table} WHERE provider = %s AND provider_user_id = %s",
                $provider,
                $provider_user_id
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

        if ( ! $user_id ) {
            return null;
        }

        $user = get_userdata( (int) $user_id );
        if ( ! $user instanceof \WP_User ) {
            $this->remove_orphaned_social_account( $provider, $provider_user_id, (int) $user_id );
            return null;
        }

        return $user instanceof \WP_User ? $user : null;
    }

    /**
     * Find a WordPress user by email address.
     *
     * @param string $email Email address.
     * @return \WP_User|null
     */
    public function find_user_by_email( string $email ): ?\WP_User {
        $user = get_user_by( 'email', $email );
        return $user instanceof \WP_User ? $user : null;
    }

    /**
     * Link a social account to a WordPress user.
     *
     * @param int    $user_id WordPress user ID.
     * @param string $provider Provider ID.
     * @param string $provider_user_id Provider's user ID.
     * @param string $provider_email Email from the provider.
     * @return bool
     */
    public function link_social_account( int $user_id, string $provider, string $provider_user_id, string $provider_email = '' ): bool {
        global $wpdb;
        $table = Guard_Dog()->db->get_social_accounts_table_name();

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
        // Direct insert needed for custom table
        $result = $wpdb->insert( $table, [
            'user_id'          => $user_id,
            'provider'         => $provider,
            'provider_user_id' => $provider_user_id,
            'provider_email'   => $provider_email,
            'created_at'       => current_time( 'mysql' ),
        ] );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery

        if ( $result && $this->activity_log ) {
            $this->activity_log->add_log_entry( 'social_account_linked', [
                'user_id'    => $user_id,
                'ip_address' => Guard_Dog_Utils::get_client_ip(),
                'details'    => [
                    'provider'         => $provider,
                    'provider_user_id' => $provider_user_id,
                ],
            ] );
        }

        return (bool) $result;
    }

    /**
     * Unlink a social account from a WordPress user.
     *
     * @param int    $user_id WordPress user ID.
     * @param string $provider Provider ID.
     * @return bool
     */
    public function unlink_social_account( int $user_id, string $provider ): bool {
        global $wpdb;
        $table = Guard_Dog()->db->get_social_accounts_table_name();

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        // Direct delete needed for custom table
        $result = $wpdb->delete( $table, [
            'user_id'  => $user_id,
            'provider' => $provider,
        ] );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

        if ( $result && $this->activity_log ) {
            $this->activity_log->add_log_entry( 'social_account_unlinked', [
                'user_id'    => $user_id,
                'ip_address' => Guard_Dog_Utils::get_client_ip(),
                'details'    => [ 'provider' => $provider ],
            ] );
        }

        return (bool) $result;
    }

    /**
     * Remove all social-account rows for a deleted WordPress user.
     *
     * @param int $user_id Deleted user ID.
     */
    public function cleanup_user_social_accounts( int $user_id ): void {
        if ( $user_id <= 0 ) {
            return;
        }

        global $wpdb;
        $table = Guard_Dog()->db->get_social_accounts_table_name();

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
        $wpdb->delete(
            $table,
            [ 'user_id' => $user_id ],
            [ '%d' ]
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
    }

    /**
     * Remove an orphaned social-account record that points to a missing user.
     *
     * @param string $provider Provider ID.
     * @param string $provider_user_id Provider user ID.
     * @param int    $user_id Missing WordPress user ID.
     */
    private function remove_orphaned_social_account( string $provider, string $provider_user_id, int $user_id ): void {
        global $wpdb;
        $table = Guard_Dog()->db->get_social_accounts_table_name();

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
        $wpdb->delete(
            $table,
            [
                'provider'         => $provider,
                'provider_user_id' => $provider_user_id,
            ],
            [ '%s', '%s' ]
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery

        Guard_Dog()->debug->log(
            'Removed orphaned social account record.',
            'WARNING',
            'social_login',
            [
                'provider'         => $provider,
                'provider_user_id' => $provider_user_id,
                'user_id'          => $user_id,
            ]
        );
    }

    /**
     * Get all linked social providers for a user.
     *
     * @param int $user_id WordPress user ID.
     * @return array
     */
    public function get_user_linked_providers( int $user_id ): array {
        global $wpdb;
        $table = Guard_Dog()->db->get_social_accounts_table_name();

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        // Direct query needed for custom table; no cache as user may link/unlink in same request
        $results = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT provider, provider_email, created_at, last_login_at FROM {$table} WHERE user_id = %d",
                $user_id
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

        return $results ?: [];
    }

    /**
     * Generate an OAuth state token and store metadata in a transient.
     *
     * @param string $provider Provider ID.
     * @param string $redirect_to URL to redirect to after login.
     * @return string The state token.
     */
    public function generate_oauth_state( string $provider, string $redirect_to = '', string $error_redirect_to = '' ): string {
        $state = wp_generate_password( 32, false );

        set_transient( self::STATE_TRANSIENT_PREFIX . $state, [
            'provider'          => $provider,
            'redirect_to'       => $redirect_to,
            'error_redirect_to' => $error_redirect_to,
            'timestamp'         => time(),
            'ip'                => Guard_Dog_Utils::get_client_ip(),
        ], self::STATE_TTL );

        return $state;
    }

    /**
     * Validate an OAuth state token and return stored metadata.
     * Deletes the transient (single-use).
     *
     * @param string $state The state token to validate.
     * @return array|null Stored data or null if invalid.
     */
    public function validate_oauth_state( string $state ): ?array {
        $data = get_transient( self::STATE_TRANSIENT_PREFIX . $state );

        if ( ! is_array( $data ) ) {
            return null;
        }

        // Delete transient (single-use)
        delete_transient( self::STATE_TRANSIENT_PREFIX . $state );

        return $data;
    }

    /**
     * Handle OAuth init: generate state, redirect to provider.
     */
    public function handle_oauth_init(): void {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        // 1. Context: OAuth initiation for unauthenticated login
        // 2. Security: CSRF protection via OAuth state parameter (not nonces)
        // 3. Sanitization: Provider validated against registered list
        // 4. Justification: Nonces require authenticated sessions; this is pre-auth
        // 5. Rate limiting applied below to prevent abuse
        $provider_id = isset( $_GET['provider'] ) ? sanitize_text_field( wp_unslash( $_GET['provider'] ) ) : '';
        $redirect_to = isset( $_GET['redirect_to'] ) ? sanitize_url( wp_unslash( $_GET['redirect_to'] ) ) : admin_url();
        $error_redirect_to = isset( $_GET['error_redirect_to'] ) ? sanitize_url( wp_unslash( $_GET['error_redirect_to'] ) ) : '';
        // phpcs:enable WordPress.Security.NonceVerification.Recommended

        $redirect_to       = $this->sanitize_redirect_target( $redirect_to, admin_url() );
        $error_redirect_to = $this->sanitize_redirect_target( $error_redirect_to, $redirect_to );

        // Rate limiting
        $ip = Guard_Dog_Utils::get_client_ip();
        $rate_key = 'guard_dog_oauth_rate_' . md5( $ip );
        $attempts = (int) get_transient( $rate_key );

        if ( $attempts >= self::RATE_LIMIT ) {
            wp_die(
                esc_html__( 'Too many authentication requests. Please try again later.', 'guard-dog' ),
                esc_html__( 'Rate Limited', 'guard-dog' ),
                [ 'response' => 429 ]
            );
        }

        set_transient( $rate_key, $attempts + 1, self::RATE_LIMIT_WINDOW );

        // Validate provider
        $enabled_providers = $this->get_enabled_providers();
        if ( ! isset( $enabled_providers[ $provider_id ] ) ) {
            wp_die(
                esc_html__( 'Invalid or disabled social login provider.', 'guard-dog' ),
                esc_html__( 'Error', 'guard-dog' ),
                [ 'response' => 400 ]
            );
        }

        $provider = $enabled_providers[ $provider_id ];
        $state = $this->generate_oauth_state( $provider_id, $redirect_to, $error_redirect_to );

        try {
            $league_provider = $provider->create_league_provider( [
                'client_id'     => get_option( "guard_dog_social_{$provider_id}_client_id", '' ),
                'client_secret' => get_option( "guard_dog_social_{$provider_id}_client_secret", '' ),
                'redirect_uri'  => $this->get_callback_url(),
            ] );

            $auth_url = $league_provider->getAuthorizationUrl( [
                'scope' => $provider->get_scopes(),
                'state' => $state,
            ] );
        } catch ( \Exception $e ) {
            Guard_Dog()->debug->log(
                'Social login provider init error: ' . $e->getMessage(),
                'ERROR',
                'social_login',
                [ 'provider' => $provider_id ]
            );
            $this->log_social_login_failure(
                'provider_init_error',
                $provider_id,
                [ 'error' => $e->getMessage() ]
            );
            $this->redirect_with_error( 'provider_config', $redirect_to, $error_redirect_to );
            return;
        }

        wp_redirect( $auth_url );
        exit;
    }

    /**
     * Handle OAuth callback: validate state, exchange code, log user in.
     *
     * Called via REST API endpoint. Reads code, state, and error from query params.
     *
     * @param \WP_REST_Request|string $request REST request object (unused, params read from $_GET).
     */
    public function handle_oauth_callback( $request = null ): void {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
        // 1. Context: OAuth callback from external provider
        // 2. Security: CSRF protection via state parameter validation
        // 3. Sanitization: All inputs sanitized before use
        // 4. Justification: External redirect cannot include WordPress nonces
        // 5. State parameter is single-use and expires in 10 minutes
        // 6. POST fallback: Apple uses response_mode=form_post, sending code/state via POST body
        $code              = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
        $state             = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
        $error             = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : '';
        $error_description = isset( $_GET['error_description'] ) ? sanitize_text_field( wp_unslash( $_GET['error_description'] ) ) : '';

        // Apple Sign In uses response_mode=form_post, sending params via POST
        if ( empty( $code ) && isset( $_POST['code'] ) ) {
            $code = sanitize_text_field( wp_unslash( $_POST['code'] ) );
        }
        if ( empty( $state ) && isset( $_POST['state'] ) ) {
            $state = sanitize_text_field( wp_unslash( $_POST['state'] ) );
        }
        if ( empty( $error ) && isset( $_POST['error'] ) ) {
            $error = sanitize_text_field( wp_unslash( $_POST['error'] ) );
        }
        if ( empty( $error_description ) && isset( $_POST['error_description'] ) ) {
            $error_description = sanitize_text_field( wp_unslash( $_POST['error_description'] ) );
        }
        // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing

        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $request_uri    = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
        $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
        // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $request_path = trim( (string) wp_parse_url( $request_uri, PHP_URL_PATH ), '/' );

        // Handle provider errors (user denied access)
        if ( ! empty( $error ) ) {
            $redirect_to       = '';
            $error_redirect_to = '';
            $provider_id       = null;

            if ( ! empty( $state ) ) {
                $state_data = $this->validate_oauth_state( $state );
                if ( is_array( $state_data ) ) {
                    $provider_id       = $state_data['provider'] ?? null;
                    $redirect_to       = $state_data['redirect_to'] ?? '';
                    $error_redirect_to = $state_data['error_redirect_to'] ?? '';
                }
            }

            Guard_Dog()->debug->log(
                'Social login provider returned an OAuth error.',
                'ERROR',
                'social_login',
                [
                    'error'             => $error,
                    'error_description' => $error_description,
                    'state_present'     => ! empty( $state ),
                    'request_method'    => $request_method,
                    'request_path'      => $request_path,
                ]
            );
            $this->log_social_login_failure(
                'provider_returned_error',
                $provider_id,
                [
                    'error'             => $error,
                    'error_description' => $error_description,
                    'request_method'    => $request_method,
                    'request_path'      => $request_path,
                ]
            );

            $redirect_error = in_array( $error, [ 'access_denied', 'user_cancelled_authorize' ], true )
                ? 'provider_denied'
                : 'oauth_error';

            $this->redirect_with_error( $redirect_error, $redirect_to, $error_redirect_to );
            return;
        }

        if ( empty( $code ) || empty( $state ) ) {
            Guard_Dog()->debug->log(
                'Social login callback missing required OAuth parameters.',
                'ERROR',
                'social_login',
                [
                    'code_present'   => ! empty( $code ),
                    'state_present'  => ! empty( $state ),
                    'request_method' => $request_method,
                    'request_path'   => $request_path,
                ]
            );
            $this->log_social_login_failure(
                'missing_params',
                null,
                [
                    'code_present'   => ! empty( $code ),
                    'state_present'  => ! empty( $state ),
                    'request_method' => $request_method,
                    'request_path'   => $request_path,
                ]
            );
            $this->redirect_with_error( 'missing_params' );
            return;
        }

        // Validate state (CSRF protection)
        $state_data = $this->validate_oauth_state( $state );
        if ( ! $state_data ) {
            Guard_Dog()->debug->log(
                'Social login callback failed state validation.',
                'ERROR',
                'social_login',
                [
                    'state'          => $state,
                    'request_method' => $request_method,
                    'request_path'   => $request_path,
                ]
            );
            $this->log_social_login_failure(
                'invalid_state',
                null,
                [
                    'request_method' => $request_method,
                    'request_path'   => $request_path,
                ]
            );
            $this->redirect_with_error( 'invalid_state' );
            return;
        }

        $provider_id       = $state_data['provider'];
        $redirect_to       = $state_data['redirect_to'] ?: admin_url();
        $error_redirect_to = $state_data['error_redirect_to'] ?? '';
        $linking_user_id = $this->get_linking_user_id_from_state( $state_data );

        // Get provider instance
        if ( ! isset( $this->providers[ $provider_id ] ) ) {
            Guard_Dog()->debug->log(
                'Social login callback referenced an unknown provider.',
                'ERROR',
                'social_login',
                [
                    'provider'     => $provider_id,
                    'request_path' => $request_path,
                ]
            );
            $this->log_social_login_failure(
                'invalid_provider',
                $provider_id,
                [ 'request_path' => $request_path ]
            );
            $this->redirect_with_error( 'invalid_provider', $redirect_to, $error_redirect_to );
            return;
        }

        $provider = $this->providers[ $provider_id ];

        try {
            // Create league provider and exchange code for token
            $league_provider = $provider->create_league_provider( [
                'client_id'     => get_option( "guard_dog_social_{$provider_id}_client_id", '' ),
                'client_secret' => get_option( "guard_dog_social_{$provider_id}_client_secret", '' ),
                'redirect_uri'  => $this->get_callback_url(),
            ] );

            $token = $league_provider->getAccessToken( $this->create_authorization_code_grant(), [
                'code' => $code,
            ] );

            // Get user info from provider
            $resource_owner = $league_provider->getResourceOwner( $token );
            $user_data = $provider->normalize_resource_owner( $resource_owner );

            // Apple sends user name/email via POST body on first authorization only.
            // This data is NOT available on subsequent logins, so we must capture it here.
            if ( 'apple' === $provider_id ) {
                $user_data = $this->merge_apple_first_login_data( $user_data );
            }

        } catch ( \Exception $e ) {
            Guard_Dog()->debug->log(
                'Social login OAuth error: ' . $e->getMessage(),
                'ERROR',
                'social_login',
                [
                    'provider'     => $provider_id,
                    'request_path' => $request_path,
                ]
            );

            if ( $this->activity_log ) {
                $this->log_social_login_failure(
                    'oauth_error',
                    $provider_id,
                    [ 'error' => $e->getMessage() ]
                );
            }

            $this->redirect_with_error( 'oauth_error', $redirect_to, $error_redirect_to );
            return;
        }

        // Check if this is an account-linking request.
        // Apple uses response_mode=form_post, and many browsers do not send
        // WordPress auth cookies on that cross-site POST callback. The
        // state token is the reliable source of truth for link intent.
        if ( $linking_user_id > 0 ) {
            $this->complete_account_linking( $linking_user_id, $provider_id, $user_data, $redirect_to );
            return;
        }

        // Try to find existing user
        $user = $this->find_user_by_social_account( $provider_id, $user_data['provider_user_id'] );

        // If not linked, optionally match by verified email and persist the link.
        if ( ! $user && ! empty( $user_data['email'] ) ) {
            $auto_link = get_option( 'guard_dog_social_auto_link_email', false );
            if ( $auto_link && $this->is_provider_email_verified( $user_data ) ) {
                $matched_user = $this->find_user_by_email( $user_data['email'] );
                if ( $matched_user && $this->link_social_account(
                    $matched_user->ID,
                    $provider_id,
                    $user_data['provider_user_id'],
                    $user_data['email']
                ) ) {
                    $user = $matched_user;
                }
            }
        }

        // Optionally auto-create user
        if ( ! $user ) {
            $auto_create = get_option( 'guard_dog_social_auto_create_users', false );
            if ( $auto_create && ! empty( $user_data['email'] ) && $this->is_provider_email_verified( $user_data ) ) {
                $user = $this->create_user_from_social_data( $user_data );
                if ( $user ) {
                    $this->link_social_account(
                        $user->ID,
                        $provider_id,
                        $user_data['provider_user_id'],
                        $user_data['email']
                    );
                }
            }
        }

        if ( ! $user ) {
            $this->redirect_with_error( 'no_matching_user', $redirect_to, $error_redirect_to );
            return;
        }

        // Complete login
        $this->complete_social_login( $user, $provider_id, $user_data, $redirect_to );
    }

    /**
     * Merge Apple's first-login POST data into normalized user data.
     *
     * Apple only sends the user's name and email via a POST 'user' JSON parameter
     * on the very first authorization. Subsequent logins only return the user ID
     * from the id_token JWT. This method captures that first-login data.
     *
     * @since 1.9.30
     *
     * @param array $user_data Normalized user data from the resource owner.
     * @return array User data with Apple POST data merged in.
     */
    private function merge_apple_first_login_data( array $user_data ): array {
        // phpcs:disable WordPress.Security.NonceVerification.Missing
        // 1. Context: Apple OAuth callback sends user data via POST body
        // 2. Security: CSRF protection handled by OAuth state parameter validation
        // 3. Sanitization: JSON decoded and individual fields sanitized below
        // 4. Justification: Apple's response_mode=form_post sends user info as POST param
        if ( empty( $_POST['user'] ) || ! is_string( $_POST['user'] ) ) {
            return $user_data;
        }

        // Decode raw JSON first - sanitize_text_field would corrupt the JSON structure.
        // Individual fields are sanitized after decoding below.
        $raw_user = wp_unslash( $_POST['user'] );
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        $apple_user = json_decode( $raw_user, true );

        if ( ! is_array( $apple_user ) ) {
            return $user_data;
        }

        if ( empty( $user_data['email'] ) && ! empty( $apple_user['email'] ) ) {
            $user_data['email'] = sanitize_email( $apple_user['email'] );
        }

        if ( ! empty( $apple_user['name'] ) && is_array( $apple_user['name'] ) ) {
            $first = sanitize_text_field( $apple_user['name']['firstName'] ?? '' );
            $last  = sanitize_text_field( $apple_user['name']['lastName'] ?? '' );

            if ( empty( $user_data['first_name'] ) && ! empty( $first ) ) {
                $user_data['first_name'] = $first;
            }
            if ( empty( $user_data['last_name'] ) && ! empty( $last ) ) {
                $user_data['last_name'] = $last;
            }
            if ( empty( $user_data['name'] ) && ( ! empty( $first ) || ! empty( $last ) ) ) {
                $user_data['name'] = trim( $first . ' ' . $last );
            }
        }

        return $user_data;
    }

    /**
     * Complete the social login process: set auth cookie, session, redirect.
     *
     * @param \WP_User $user WordPress user.
     * @param string   $provider_id Provider ID.
     * @param array    $user_data Normalized user data from provider.
     * @param string   $redirect_to URL to redirect to.
     */
    private function complete_social_login( \WP_User $user, string $provider_id, array $user_data, string $redirect_to ): void {
        // Set OAuth auth flag for 2FA bypass (like passkeys).
        // 5-minute TTL to account for slow servers and caching layers.
        set_transient( self::OAUTH_AUTH_TRANSIENT_PREFIX . $user->ID, true, 300 );

        // Update last login timestamp
        $this->update_last_login( $provider_id, $user_data['provider_user_id'] );

        // Store social avatar URL (refreshed each login)
        if ( ! empty( $user_data['avatar_url'] ) ) {
            update_user_meta( $user->ID, 'guard_dog_social_avatar_url', esc_url_raw( $user_data['avatar_url'] ) );
        }

        // Set WordPress auth
        wp_set_current_user( $user->ID );
        wp_set_auth_cookie( $user->ID, false );
        do_action( 'wp_login', $user->user_login, $user );

        // Log activity
        if ( $this->activity_log ) {
            $this->activity_log->add_log_entry( 'social_login_success', [
                'user_id'    => $user->ID,
                'username'   => $user->user_login,
                'ip_address' => Guard_Dog_Utils::get_client_ip(),
                'details'    => [
                    'provider'       => $provider_id,
                    'provider_email' => $user_data['email'],
                ],
            ] );
        }

        wp_safe_redirect( $redirect_to );
        exit;
    }

    /**
     * Complete account linking for an already-logged-in user.
     *
     * @param string $provider_id Provider ID.
     * @param array  $user_data Normalized user data.
     * @param string $redirect_to URL to redirect to.
     */
    private function complete_account_linking( int $user_id, string $provider_id, array $user_data, string $redirect_to ): void {
        if ( $user_id <= 0 || ! get_userdata( $user_id ) ) {
            $this->log_social_login_failure(
                'link_failed',
                $provider_id,
                [
                    'context' => 'link',
                    'error'   => 'missing_linking_user',
                ]
            );
            $this->redirect_with_error( 'link_failed', $redirect_to );
            return;
        }

        if ( empty( $user_data['provider_user_id'] ) ) {
            $this->log_social_login_failure(
                'link_failed',
                $provider_id,
                [
                    'context' => 'link',
                    'error'   => 'missing_provider_user_id',
                ]
            );
            $this->redirect_with_error( 'link_failed', $redirect_to );
            return;
        }

        // Check if this social account is already linked to another user
        $existing = $this->find_user_by_social_account( $provider_id, $user_data['provider_user_id'] );
        if ( $existing && $existing->ID !== $user_id ) {
            $this->log_social_login_failure(
                'already_linked_other',
                $provider_id,
                [
                    'context'          => 'link',
                    'existing_user_id' => $existing->ID,
                    'linking_user_id'  => $user_id,
                ]
            );
            $this->redirect_with_error( 'already_linked_other', $redirect_to );
            return;
        }

        if ( $existing && $existing->ID === $user_id ) {
            wp_safe_redirect( add_query_arg( 'social_linked', $provider_id, $redirect_to ) );
            exit;
        }

        $linked = $this->link_social_account(
            $user_id,
            $provider_id,
            $user_data['provider_user_id'],
            $user_data['email']
        );

        if ( ! $linked ) {
            $existing_after = $this->find_user_by_social_account( $provider_id, $user_data['provider_user_id'] );
            if ( $existing_after && $existing_after->ID !== $user_id ) {
                $this->log_social_login_failure(
                    'already_linked_other',
                    $provider_id,
                    [
                        'context'          => 'link',
                        'existing_user_id' => $existing_after->ID,
                        'linking_user_id'  => $user_id,
                    ]
                );
                $this->redirect_with_error( 'already_linked_other', $redirect_to );
                return;
            }

            $this->log_social_login_failure(
                'link_failed',
                $provider_id,
                [
                    'context'          => 'link',
                    'linking_user_id'  => $user_id,
                    'provider_user_id' => $user_data['provider_user_id'],
                ]
            );
            $this->redirect_with_error( 'link_failed', $redirect_to );
            return;
        }

        // Store social avatar URL
        if ( ! empty( $user_data['avatar_url'] ) ) {
            update_user_meta( $user_id, 'guard_dog_social_avatar_url', esc_url_raw( $user_data['avatar_url'] ) );
        }

        wp_safe_redirect( add_query_arg( 'social_linked', $provider_id, $redirect_to ) );
        exit;
    }

    /**
     * Create a WordPress user from social provider data.
     *
     * @param array $user_data Normalized user data.
     * @return \WP_User|null
     */
    private function create_user_from_social_data( array $user_data ): ?\WP_User {
        $default_role = get_option( 'guard_dog_social_new_user_role', get_option( 'default_role', 'subscriber' ) );

        $username = sanitize_user( $user_data['email'], true );
        if ( username_exists( $username ) ) {
            $username = sanitize_user( $user_data['email'] . '_' . wp_generate_password( 4, false ), true );
        }

        $user_id = wp_insert_user( [
            'user_login'   => $username,
            'user_email'   => $user_data['email'],
            'user_pass'    => wp_generate_password( 32, true, true ),
            'first_name'   => $user_data['first_name'],
            'last_name'    => $user_data['last_name'],
            'display_name' => $user_data['name'] ?: $user_data['email'],
            'role'         => $default_role,
        ] );

        if ( is_wp_error( $user_id ) ) {
            Guard_Dog()->debug->log( 'Social login user creation failed: ' . $user_id->get_error_message(), 'ERROR' );
            return null;
        }

        // Store social avatar URL
        if ( ! empty( $user_data['avatar_url'] ) ) {
            update_user_meta( $user_id, 'guard_dog_social_avatar_url', esc_url_raw( $user_data['avatar_url'] ) );
        }

        if ( $this->activity_log ) {
            $this->activity_log->add_log_entry( 'social_user_created', [
                'user_id'    => $user_id,
                'username'   => $username,
                'ip_address' => Guard_Dog_Utils::get_client_ip(),
                'details'    => [
                    'email' => $user_data['email'],
                    'name'  => $user_data['name'],
                ],
            ] );
        }

        return get_userdata( $user_id );
    }

    /**
     * Check whether the identity provider explicitly verified the email address.
     *
     * @param array $user_data Normalized provider user data.
     * @return bool True when the provider included a truthy email_verified claim.
     */
    private function is_provider_email_verified( array $user_data ): bool {
        if ( ! array_key_exists( 'email_verified', $user_data ) ) {
            return false;
        }

        return filter_var( $user_data['email_verified'], FILTER_VALIDATE_BOOLEAN );
    }

    /**
     * Update last_login_at for a social account.
     *
     * @param string $provider Provider ID.
     * @param string $provider_user_id Provider's user ID.
     */
    private function update_last_login( string $provider, string $provider_user_id ): void {
        global $wpdb;
        $table = Guard_Dog()->db->get_social_accounts_table_name();

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        // Direct update needed for custom table
        $wpdb->update(
            $table,
            [ 'last_login_at' => current_time( 'mysql' ) ],
            [
                'provider'         => $provider,
                'provider_user_id' => $provider_user_id,
            ]
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    }

    /**
     * Filter the avatar URL to use the social profile picture when available.
     *
     * @param string $url         Current avatar URL (Gravatar).
     * @param mixed  $id_or_email User ID, email, or WP_User/WP_Comment object.
     * @param array  $args        Avatar arguments.
     * @return string Filtered avatar URL.
     */
    public function filter_avatar_url( string $url, $id_or_email, array $args ): string {
        $user_id = 0;

        if ( is_numeric( $id_or_email ) ) {
            $user_id = (int) $id_or_email;
        } elseif ( is_string( $id_or_email ) ) {
            $user = get_user_by( 'email', $id_or_email );
            if ( $user ) {
                $user_id = $user->ID;
            }
        } elseif ( $id_or_email instanceof \WP_User ) {
            $user_id = $id_or_email->ID;
        } elseif ( $id_or_email instanceof \WP_Comment && $id_or_email->user_id ) {
            $user_id = (int) $id_or_email->user_id;
        }

        if ( ! $user_id ) {
            return $url;
        }

        $social_avatar = get_user_meta( $user_id, 'guard_dog_social_avatar_url', true );

        if ( ! empty( $social_avatar ) ) {
            return $social_avatar;
        }

        return $url;
    }

    /**
     * Redirect to login page with an error code.
     *
     * @param string $error_code Error identifier.
     * @param string $redirect_to Optional override redirect URL.
     */
    private function redirect_with_error( string $error_code, string $redirect_to = '', string $error_redirect_to = '' ): void {
        $login_url = $error_redirect_to ?: $redirect_to ?: wp_login_url();
        wp_safe_redirect( add_query_arg( 'social_login_error', $error_code, $login_url ) );
        exit;
    }

    /**
     * Get a human-readable social login error message.
     *
     * @param string $error_code Error identifier.
     * @return string
     */
    private function get_social_login_error_message( string $error_code ): string {
        $error_messages = [
            'provider_denied'      => __( 'You denied access. Please try again.', 'guard-dog' ),
            'missing_params'       => __( 'Authentication failed. Missing parameters.', 'guard-dog' ),
            'invalid_state'        => __( 'Authentication expired. Please try again.', 'guard-dog' ),
            'invalid_provider'     => __( 'Invalid login provider.', 'guard-dog' ),
            'provider_config'      => __( 'Authentication provider configuration error. Re-save the provider settings and try again.', 'guard-dog' ),
            'oauth_error'          => __( 'Authentication error. Please try again.', 'guard-dog' ),
            'no_matching_user'     => __( 'No account found for this email. Contact your administrator.', 'guard-dog' ),
            'already_linked_other' => __( 'This social account is already linked to another user.', 'guard-dog' ),
            'link_failed'          => __( 'We could not link this social account. Please try again.', 'guard-dog' ),
        ];

        return $error_messages[ $error_code ] ?? __( 'Social login failed. Please try again.', 'guard-dog' );
    }

    /**
     * Determine the target user for an account-linking callback.
     *
     * Apple callbacks arrive via cross-site POST, which can omit WordPress auth
     * cookies in modern browsers. The link state is therefore the authoritative
     * source for the intended user account.
     *
     * @param array $state_data Validated OAuth state payload.
     * @return int
     */
    private function get_linking_user_id_from_state( array $state_data ): int {
        if ( isset( $state_data['action'] ) && 'link' === $state_data['action'] ) {
            return (int) ( $state_data['user_id'] ?? 0 );
        }

        if ( is_user_logged_in() ) {
            return get_current_user_id();
        }

        return 0;
    }

    /**
     * Create the League authorization-code grant instance explicitly.
     *
     * Scoped production builds can break league/oauth2-client's internal
     * string-to-class grant resolution, because GrantFactory constructs the
     * class name from a raw string. Instantiating the grant directly avoids
     * that namespace mismatch.
     *
     * @return object
     */
    private function create_authorization_code_grant() {
        return new \GuardDog\Vendor\League\OAuth2\Client\Grant\AuthorizationCode();
    }

    /**
     * Record a social login failure in the activity log.
     *
     * This path remains available even when Guard Dog debug logging is disabled,
     * which makes production OAuth failures easier to inspect.
     *
     * @param string      $reason Failure reason identifier.
     * @param string|null $provider_id Optional provider ID.
     * @param array       $details Additional context for the failure.
     */
    private function log_social_login_failure( string $reason, ?string $provider_id = null, array $details = [] ): void {
        if ( ! $this->activity_log ) {
            $this->log_social_login_error_fallback( $reason, $provider_id, $details );
            return;
        }

        $payload = array_merge(
            [ 'reason' => $reason ],
            $details
        );

        if ( ! empty( $provider_id ) ) {
            $payload['provider'] = $provider_id;
        }

        $this->activity_log->add_log_entry( 'social_login_failed', [
            'ip_address' => Guard_Dog_Utils::get_client_ip(),
            'details'    => $payload,
        ] );

        $this->log_social_login_error_fallback( $reason, $provider_id, $details );
    }

    /**
     * Write a minimal fallback log line for production environments.
     *
     * Guard Dog debug logging depends on WP_DEBUG/WP_DEBUG_LOG, which is often
     * disabled in production. This fallback keeps critical social login failures
     * visible in the PHP error log without exposing details to the browser.
     *
     * @param string      $reason Failure reason identifier.
     * @param string|null $provider_id Optional provider ID.
     * @param array       $details Additional context for the failure.
     */
    private function log_social_login_error_fallback( string $reason, ?string $provider_id = null, array $details = [] ): void {
        if ( ! function_exists( 'error_log' ) ) {
            return;
        }

        $payload = array_merge(
            [ 'reason' => $reason ],
            $details
        );

        if ( ! empty( $provider_id ) ) {
            $payload['provider'] = $provider_id;
        }

        $json_payload = wp_json_encode( $payload );
        if ( false === $json_payload ) {
            $json_payload = '{"reason":"encoding_failed"}';
        }

        error_log( '[Guard Dog][social_login] ' . $json_payload );
    }

    /**
     * Handle OAuth unlink request (AJAX, authenticated).
     */
    public function handle_oauth_unlink(): void {
        check_ajax_referer( 'guard_dog_social_nonce', '_wpnonce' );

        $provider = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '';
        $user_id = get_current_user_id();

        if ( empty( $provider ) || ! $user_id ) {
            wp_send_json_error( [ 'message' => __( 'Invalid request.', 'guard-dog' ) ] );
        }

        $result = $this->unlink_social_account( $user_id, $provider );

        if ( $result ) {
            // Remove social avatar if no other social accounts remain
            $remaining = $this->get_user_linked_providers( $user_id );
            if ( empty( $remaining ) ) {
                delete_user_meta( $user_id, 'guard_dog_social_avatar_url' );
            }
            wp_send_json_success( [ 'message' => __( 'Account unlinked successfully.', 'guard-dog' ) ] );
        } else {
            wp_send_json_error( [ 'message' => __( 'Failed to unlink account.', 'guard-dog' ) ] );
        }
    }

    /**
     * Handle OAuth link init request (AJAX, authenticated).
     */
    public function handle_oauth_link_init(): void {
        check_ajax_referer( 'guard_dog_social_nonce', '_wpnonce' );

        $provider_id = isset( $_GET['provider'] ) ? sanitize_text_field( wp_unslash( $_GET['provider'] ) ) : '';
        $redirect_to = isset( $_GET['redirect_to'] ) ? sanitize_url( wp_unslash( $_GET['redirect_to'] ) ) : admin_url( 'profile.php' );

        if ( ! isset( $this->providers[ $provider_id ] ) ) {
            wp_die( esc_html__( 'Invalid provider.', 'guard-dog' ), '', [ 'response' => 400 ] );
        }

        $provider = $this->providers[ $provider_id ];
        $state = $this->generate_oauth_state( $provider_id, $redirect_to, $redirect_to );

        // Store linking flag in state data
        $state_data = get_transient( self::STATE_TRANSIENT_PREFIX . $state );
        $state_data['action'] = 'link';
        $state_data['user_id'] = get_current_user_id();
        set_transient( self::STATE_TRANSIENT_PREFIX . $state, $state_data, self::STATE_TTL );

        try {
            $league_provider = $provider->create_league_provider( [
                'client_id'     => get_option( "guard_dog_social_{$provider_id}_client_id", '' ),
                'client_secret' => get_option( "guard_dog_social_{$provider_id}_client_secret", '' ),
                'redirect_uri'  => $this->get_callback_url(),
            ] );

            $auth_url = $league_provider->getAuthorizationUrl( [
                'scope' => $provider->get_scopes(),
                'state' => $state,
            ] );
        } catch ( \Exception $e ) {
            Guard_Dog()->debug->log(
                'Social account linking provider init error: ' . $e->getMessage(),
                'ERROR',
                'social_login',
                [ 'provider' => $provider_id ]
            );
            $this->log_social_login_failure(
                'provider_init_error',
                $provider_id,
                [
                    'context' => 'link',
                    'error'   => $e->getMessage(),
                ]
            );
            $this->redirect_with_error( 'provider_config', $redirect_to );
            return;
        }

        wp_redirect( $auth_url );
        exit;
    }

    /**
     * Render social login buttons on the login form.
     */
    public function render_social_login_buttons(): void {
        $markup = $this->get_login_buttons_markup();
        if ( '' === $markup ) {
            return;
        }

        // Social login markup includes SVG brand icons from trusted provider classes.
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo $markup;
    }

    /**
     * Enqueue CSS for login page social buttons.
     */
    public function enqueue_login_assets(): void {
        $this->enqueue_public_login_assets();
    }

    /**
     * Enqueue assets for public login surfaces.
     */
    public function enqueue_public_login_assets(): void {
        if ( empty( $this->get_enabled_providers() ) ) {
            return;
        }

        wp_enqueue_style(
            'guard-dog-social-login',
            GUARD_DOG_URL . 'assets/css/guard-dog-social-login.css',
            [],
            GUARD_DOG_VERSION
        );
    }

    /**
     * Render reusable social login buttons markup for public login surfaces.
     *
     * @param string $redirect_to Where to send the user after a successful login.
     * @param string $error_redirect_to Where to send the user after an OAuth error.
     * @return string
     */
    public function get_login_buttons_markup( string $redirect_to = '', string $error_redirect_to = '' ): string {
        $providers = $this->get_enabled_providers();
        if ( empty( $providers ) ) {
            return '';
        }

        $callback_base      = admin_url( 'admin-ajax.php' );
        $redirect_to        = $this->sanitize_redirect_target( $redirect_to, '' );
        $error_redirect_to  = $this->sanitize_redirect_target( $error_redirect_to, $redirect_to );

        ob_start();
        include GUARD_DOG_PATH . 'includes/views/partials/social-login-buttons.php';
        return (string) ob_get_clean();
    }

    /**
     * Validate a redirect target for social auth flows.
     *
     * @param string $redirect_to Raw redirect target.
     * @param string $fallback Fallback redirect target.
     * @return string
     */
    private function sanitize_redirect_target( string $redirect_to, string $fallback ): string {
        $validated = wp_validate_redirect( $redirect_to, $fallback );

        return ( is_string( $validated ) && '' !== $validated ) ? $validated : $fallback;
    }

    /**
     * Render social accounts section on user profile page.
     *
     * @param \WP_User $user The user being edited.
     */
    public function render_profile_social_accounts( \WP_User $user ): void {
        if ( get_current_user_id() !== $user->ID && ! current_user_can( 'edit_users' ) ) {
            return;
        }

        $linked = $this->get_user_linked_providers( $user->ID );
        $linked_map = [];
        foreach ( $linked as $account ) {
            $linked_map[ $account->provider ] = $account;
        }

        $nonce = wp_create_nonce( 'guard_dog_social_nonce' );
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        $social_error = isset( $_GET['social_login_error'] ) ? sanitize_text_field( wp_unslash( $_GET['social_login_error'] ) ) : '';
        $social_linked = isset( $_GET['social_linked'] ) ? sanitize_text_field( wp_unslash( $_GET['social_linked'] ) ) : '';
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        ?>
        <h2><?php esc_html_e( 'Social Login', 'guard-dog' ); ?></h2>
        <p class="description">
            <?php esc_html_e( 'Link your social accounts for faster sign-in.', 'guard-dog' ); ?>
        </p>
        <?php if ( $social_linked && isset( $this->providers[ $social_linked ] ) ) : ?>
            <div class="notice notice-success inline"><p><?php echo esc_html( sprintf( __( '%s account linked successfully.', 'guard-dog' ), $this->providers[ $social_linked ]->get_name() ) ); ?></p></div>
        <?php endif; ?>
        <?php if ( $social_error ) : ?>
            <div class="notice notice-error inline"><p><?php echo esc_html( $this->get_social_login_error_message( $social_error ) ); ?></p></div>
        <?php endif; ?>
        <table class="form-table" role="presentation">
            <?php foreach ( $this->providers as $id => $provider ) : ?>
                <tr>
                    <th scope="row"><?php echo esc_html( $provider->get_name() ); ?></th>
                    <td>
                        <?php if ( isset( $linked_map[ $id ] ) ) : ?>
                            <span class="description">
                                <?php echo esc_html( $linked_map[ $id ]->provider_email ); ?>
                                (<?php
                                    echo esc_html(
                                        sprintf(
                                            /* translators: %s: date linked */
                                            __( 'linked %s', 'guard-dog' ),
                                            date_i18n( get_option( 'date_format' ), strtotime( $linked_map[ $id ]->created_at ) )
                                        )
                                    );
                                ?>)
                            </span>
                            <button type="button" class="button button-link-delete guard-dog-social-unlink"
                                data-provider="<?php echo esc_attr( $id ); ?>"
                                data-nonce="<?php echo esc_attr( $nonce ); ?>">
                                <?php esc_html_e( 'Unlink', 'guard-dog' ); ?>
                            </button>
                        <?php else : ?>
                            <a href="<?php echo esc_url( admin_url( 'admin-ajax.php?action=guard_dog_oauth_link&provider=' . $id . '&_wpnonce=' . $nonce . '&redirect_to=' . rawurlencode( admin_url( 'profile.php' ) ) ) ); ?>" class="button">
                                <?php
                                echo esc_html(
                                    sprintf(
                                        /* translators: %s: provider name */
                                        __( 'Link %s Account', 'guard-dog' ),
                                        $provider->get_name()
                                    )
                                );
                                ?>
                            </a>
                        <?php endif; ?>
                    </td>
                </tr>
            <?php endforeach; ?>
        </table>
        <?php
    }
}
