<?php
/**
 * Canonical user meta for linked social identities (single JSON blob + legacy index keys).
 *
 * @package WP_PowerSuite\OAuth\Accounts
 */

namespace WP_PowerSuite\OAuth\Accounts;

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

final class SocialAccountStore {

	public const META_KEY = 'wpps_social_identities';

	/** @var array<string, string> provider slug => legacy usermeta key for external subject (query index) */
	private const LEGACY_INDEX_KEYS = [
		'google'   => 'wpps_google_id',
		'facebook' => 'facebook_sign_in_id',
		'github'   => 'wpps_github_id',
	];

	/**
	 * @return array<string, string>
	 */
	public static function getLegacyIndexKeys(): array {
		return self::LEGACY_INDEX_KEYS;
	}

	public static function legacyKeyForProvider( string $provider_slug ): string {
		$provider_slug = sanitize_key( $provider_slug );
		return self::LEGACY_INDEX_KEYS[ $provider_slug ] ?? 'wpps_' . $provider_slug . '_id';
	}

	/**
	 * @return array<string, array{id: string, email?: string, name?: string, connected_at?: int}>
	 */
	public static function getIdentities( int $user_id ): array {
		$user_id = (int) $user_id;
		if ( $user_id <= 0 ) {
			return [];
		}
		self::migrateLegacyIfNeeded( $user_id );
		$raw = get_user_meta( $user_id, self::META_KEY, true );
		if ( ! is_string( $raw ) || $raw === '' ) {
			return [];
		}
		$dec = json_decode( $raw, true );
		if ( ! is_array( $dec ) ) {
			return [];
		}
		$out = [];
		foreach ( $dec as $k => $row ) {
			$ks = sanitize_key( (string) $k );
			if ( $ks === '' || ! is_array( $row ) ) {
				continue;
			}
			$id = isset( $row['id'] ) ? (string) $row['id'] : '';
			if ( $id === '' ) {
				continue;
			}
			$out[ $ks ] = [
				'id'            => $id,
				'email'         => isset( $row['email'] ) ? (string) $row['email'] : '',
				'name'          => isset( $row['name'] ) ? (string) $row['name'] : '',
				'connected_at'  => isset( $row['connected_at'] ) ? (int) $row['connected_at'] : 0,
			];
		}
		return apply_filters( 'wpps_social_identities', $out, $user_id );
	}

	/**
	 * Find user id by provider + external subject (uses legacy index meta for O(1) lookup).
	 */
	public static function findUserIdBySubject( string $provider_slug, string $subject ): ?int {
		$provider_slug = sanitize_key( $provider_slug );
		$subject       = trim( $subject );
		if ( $provider_slug === '' || $subject === '' ) {
			return null;
		}
		$key = self::legacyKeyForProvider( $provider_slug );
		$users = get_users(
			[
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- O(1) lookup by provider subject index.
				'meta_key'   => $key,
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- exact subject match, limited to one user.
				'meta_value' => $subject,
				'number'     => 1,
				'fields'     => 'ID',
			]
		);
		if ( ! $users ) {
			return null;
		}
		return (int) $users[0];
	}

	/**
	 * Whether this provider+subject is linked to a different user.
	 */
	public static function isSubjectUsedByOther( string $provider_slug, string $subject, int $except_user_id ): ?int {
		$subject = trim( $subject );
		if ( $subject === '' ) {
			return null;
		}
		$uid = self::findUserIdBySubject( $provider_slug, $subject );
		if ( $uid === null ) {
			return null;
		}
		if ( (int) $uid === (int) $except_user_id ) {
			return null;
		}
		return (int) $uid;
	}

	/**
	 * @param array{id: string, email?: string, display_name?: string, first_name?: string, last_name?: string, avatar_url?: string} $profile
	 * @param array{meta_key: string, avatar_meta_key?: string, store_avatar?: bool, provider: string} $opts
	 * @return \WP_Error|bool true on success
	 */
	public static function addOrUpdate( int $user_id, array $profile, array $opts ): \WP_Error|bool {
		$user_id  = (int) $user_id;
		$provider = sanitize_key( (string) ( $opts['provider'] ?? '' ) );
		$ext_id   = (string) ( $profile['id'] ?? '' );
		if ( $user_id <= 0 || $provider === '' || $ext_id === '' ) {
			return new \WP_Error( 'wpps_social', __( 'Invalid link request.', 'wp-powersuite' ) );
		}

		$other = self::isSubjectUsedByOther( $provider, $ext_id, $user_id );
		if ( $other !== null ) {
			return new \WP_Error( 'wpps_social_taken', __( 'This account is already linked to another user.', 'wp-powersuite' ) );
		}

		$identities                    = self::getIdentities( $user_id );
		$email                         = (string) ( $profile['email'] ?? '' );
		$email                         = is_email( $email ) ? $email : '';
		$display                       = (string) ( $profile['display_name'] ?? '' );
		$identities[ $provider ]       = [
			'id'            => $ext_id,
			'email'         => $email,
			'name'          => $display,
			'connected_at'  => (int) time(),
		];
		$ok                            = self::saveIdentities( $user_id, $identities );
		if ( ! $ok ) {
			return new \WP_Error( 'wpps_social', __( 'Could not save account link.', 'wp-powersuite' ) );
		}

		$meta_key   = (string) ( $opts['meta_key'] ?? self::legacyKeyForProvider( $provider ) );
		$store_av   = ! empty( $opts['store_avatar'] );
		$av_mkey    = (string) ( $opts['avatar_meta_key'] ?? 'wpps_oauth_avatar' );
		update_user_meta( $user_id, $meta_key, $ext_id );
		if ( $store_av && ! empty( $profile['avatar_url'] ) ) {
			$g_av = ( 'google' === $provider ) ? 'wpps_google_avatar' : ( ( 'github' === $provider ) ? 'wpps_github_avatar' : $av_mkey );
			update_user_meta( $user_id, $g_av, esc_url_raw( (string) $profile['avatar_url'] ) );
		}
		/**
		 * Fires after a social identity is stored for a user.
		 *
		 * @param int    $user_id  WordPress user ID.
		 * @param string $provider Provider slug.
		 * @param array  $profile  Sanitized profile subset.
		 */
		do_action( 'wpps_social_account_linked', $user_id, $provider, $identities[ $provider ] );
		return true;
	}

	/**
	 * @param array<string, array<string, mixed>> $identities
	 */
	public static function saveIdentities( int $user_id, array $identities ): bool {
		$user_id = (int) $user_id;
		if ( $user_id <= 0 ) {
			return false;
		}
		$clean = [];
		foreach ( $identities as $k => $row ) {
			$k = sanitize_key( (string) $k );
			if ( $k === '' || ! is_array( $row ) ) {
				continue;
			}
			$clean[ $k ] = [
				'id'            => (string) ( $row['id'] ?? '' ),
				'email'         => (string) ( $row['email'] ?? '' ),
				'name'          => (string) ( $row['name'] ?? '' ),
				'connected_at'  => (int) ( $row['connected_at'] ?? 0 ),
			];
		}
		return (bool) update_user_meta( $user_id, self::META_KEY, wp_json_encode( $clean ) );
	}

	/**
	 * @param string $meta_key Legacy index key to delete.
	 * @return bool
	 */
	public static function removeProvider( int $user_id, string $provider_slug, string $meta_key ): bool {
		$user_id       = (int) $user_id;
		$provider_slug = sanitize_key( $provider_slug );
		if ( $user_id <= 0 || $provider_slug === '' ) {
			return false;
		}
		$ids = self::getIdentities( $user_id );
		if ( ! isset( $ids[ $provider_slug ] ) ) {
			return false;
		}
		unset( $ids[ $provider_slug ] );
		self::saveIdentities( $user_id, $ids );
		delete_user_meta( $user_id, $meta_key );
		if ( 'google' === $provider_slug ) {
			delete_user_meta( $user_id, 'wpps_google_avatar' );
		} elseif ( 'github' === $provider_slug ) {
			delete_user_meta( $user_id, 'wpps_github_avatar' );
		} elseif ( 'facebook' === $provider_slug ) {
			// No dedicated avatar key in legacy theme; optional cleanup.
		}
		/**
		 * Fires after a user disconnects a provider.
		 *
		 * @param int    $user_id  WordPress user ID.
		 * @param string $provider Provider slug.
		 */
		do_action( 'wpps_social_account_unlinked', $user_id, $provider_slug );
		return true;
	}

	/**
	 * One-time import from legacy per-provider keys into META_KEY.
	 */
	private static function migrateLegacyIfNeeded( int $user_id ): void {
		$existing = get_user_meta( $user_id, self::META_KEY, true );
		if ( is_string( $existing ) && $existing !== '' ) {
			return;
		}
		$identities = [];
		foreach ( self::LEGACY_INDEX_KEYS as $prov => $mkey ) {
			$v = get_user_meta( $user_id, $mkey, true );
			$v = is_string( $v ) || is_numeric( $v ) ? (string) $v : '';
			if ( $v === '' ) {
				continue;
			}
			$identities[ $prov ] = [
				'id'            => $v,
				'email'         => '',
				'name'          => '',
				'connected_at'  => 0,
			];
		}
		if ( $identities === [] ) {
			return;
		}
		self::saveIdentities( $user_id, $identities );
	}
}
