<?php

declare( strict_types=1 );

namespace AlphabetAvatars;

use WP_PowerSuite\Core\AbstractModule;
use WP_User;

class Module extends AbstractModule {

	private const MODULE_ID = 'alphabet-avatars';
	private const MODULE_VERSION = '1.1.0';

	private const PALETTE = [
		'#1abc9c', '#2ecc71', '#16a085', '#27ae60', '#00b894',
		'#3498db', '#2980b9', '#0984e3', '#74b9ff',
		'#9b59b6', '#8e44ad', '#6c5ce7',
		'#f1c40f', '#e67e22', '#e74c3c', '#f39c12', '#d35400', '#c0392b', '#e17055', '#fdcb6e',
		'#34495e', '#2c3e50', '#95a5a6', '#7f8c8d',
		'#fd79a8',
	];

	private const DEFAULT_COLOR = '#3498db';

	private const META_LOCAL_AVATAR = 'powersuite_avatar';

	private ?array $settings_cache = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Alphabet Avatars', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Letter-based profile images with customizable colors—great when you want a polished look without relying on Gravatar.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return 'both';
	}

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		// Priority 10: initials fallback. Local User Avatar uses get_avatar_data; Google Sign-in uses get_avatar @ 20.
		add_filter( 'get_avatar', [ $this, 'filterAvatar' ], 10, 6 );
		add_filter( 'avatar_defaults', [ $this, 'addDefaultAvatar' ] );
		add_action( 'admin_head', [ $this, 'adminBarStyles' ] );
		add_action( 'wp_head', [ $this, 'adminBarStyles' ] );
	}


	/**
	 * Add Alphabet Avatar as a default avatar option in Settings → Discussion.
	 *
	 * @param array<string, string> $avatar_defaults Default avatars.
	 * @return array<string, string> Modified defaults.
	 */
	public function addDefaultAvatar( array $avatar_defaults ): array {
		$avatar_defaults['alphabet'] = __( 'Alphabet Avatar (Initials)', 'wp-powersuite' );
		return $avatar_defaults;
	}

	/**
	 * Output CSS to remove white background from alphabet avatars in admin bar.
	 */
	public function adminBarStyles(): void {
		echo '<style>
			#wpadminbar #wp-admin-bar-my-account.with-avatar > .ab-empty-item img,
			#wpadminbar #wp-admin-bar-my-account.with-avatar > a img {
				background: none !important;
				border: none !important;
			}
		</style>';
	}

	public function filterAvatar( string $avatar, $id_or_email, int $size, string $default, string $alt, array $args ): string {
		$user = $this->resolveUser( $id_or_email );
		if ( ! $user instanceof WP_User ) {
			return $avatar;
		}

		if ( $this->shouldDeferToExistingAvatar( (int) $user->ID, $args, $avatar ) ) {
			return $avatar;
		}

		$settings = $this->getSettings();
		return $this->buildAvatar( $user, $size, $alt, $args, $settings );
	}

	/**
	 * Build avatar img HTML. Used by filter.
	 *
	 * @param WP_User $user User object.
	 * @param int $size Avatar size.
	 * @param string $alt Alt text.
	 * @param array $args Additional arguments.
	 * @param array|null $settings Optional settings override.
	 * @return string Avatar HTML.
	 */
	public function buildAvatar( WP_User $user, int $size, string $alt, array $args, ?array $settings = null ): string {
		$settings = $settings ?? $this->getSettings();
		$initials = $this->getInitials( $user, $settings['letter_source'] ?? 'first_last' );
		$color    = $this->getColor( $user, $settings );
		$shape    = $settings['shape'] ?? 'circle';
		$size_set = $settings['size'] ?? 'regular';
		$svg      = $this->buildSvg( $initials, $color, $shape, $size_set, $size );
		// Use base64 data URIs: more reliable than rawurlencode, avoids esc_url issues
		$data_uri = 'data:image/svg+xml;base64,' . base64_encode( $svg );

		$css_size = (int) $size;
		$width    = $args['width'] ?? $css_size;
		$height   = $args['height'] ?? $css_size;
		$classes  = [ 'avatar', 'avatar-' . $css_size, 'photo', 'avatar-alphabet' ];
		if ( ! empty( $args['class'] ) ) {
			$classes[] = is_array( $args['class'] ) ? implode( ' ', $args['class'] ) : $args['class'];
		}
		$class_attr = implode( ' ', array_map( 'esc_attr', $classes ) );
		$loading    = isset( $args['loading'] ) ? ' loading="' . esc_attr( $args['loading'] ) . '"' : '';
		$decoding   = isset( $args['decoding'] ) ? ' decoding="' . esc_attr( $args['decoding'] ) . '"' : ' decoding="async"';
		$extra      = isset( $args['extra_attr'] ) ? ' ' . trim( (string) $args['extra_attr'] ) : '';

		return sprintf(
			'<img alt="%s" src="%s" class="%s" width="%d" height="%d"%s%s%s />',
			esc_attr( $alt ),
			esc_attr( $data_uri ),
			$class_attr,
			(int) $width,
			(int) $height,
			$loading,
			$decoding,
			$extra
		);
	}

	/**
	 * Keep uploads and other non-Gravatar avatars set via get_avatar_data.
	 *
	 * @param array<string, mixed> $args get_avatar() args (includes url, found_avatar from get_avatar_data).
	 */
	private function shouldDeferToExistingAvatar( int $user_id, array $args, string $avatar ): bool {
		if ( $this->userHasLocalAvatar( $user_id ) ) {
			return true;
		}

		$url = isset( $args['url'] ) ? (string) $args['url'] : '';
		if ( ! empty( $args['found_avatar'] ) && $url !== '' && ! $this->isGravatarUrl( $url ) ) {
			return true;
		}

		return str_contains( $avatar, 'avatar-local' );
	}

	private function userHasLocalAvatar( int $user_id ): bool {
		$aid = (int) get_user_meta( $user_id, self::META_LOCAL_AVATAR, true );
		if ( $aid < 1 ) {
			return false;
		}

		return wp_attachment_is_image( $aid );
	}

	private function isGravatarUrl( string $url ): bool {
		return (bool) preg_match( '#(?:secure\.gravatar\.com/avatar|gravatar\.com/avatar|www\.gravatar\.com/avatar)#i', $url );
	}

	private function resolveUser( $id_or_email ): ?WP_User {
		if ( $id_or_email instanceof WP_User ) {
			return $id_or_email;
		}
		if ( is_numeric( $id_or_email ) ) {
			$u = get_user_by( 'id', (int) $id_or_email );
			return $u instanceof WP_User ? $u : null;
		}
		if ( is_string( $id_or_email ) && strpos( $id_or_email, '@' ) !== false ) {
			$u = get_user_by( 'email', $id_or_email );
			return $u instanceof WP_User ? $u : null;
		}
		if ( is_string( $id_or_email ) && $id_or_email !== '' ) {
			$u = get_user_by( 'login', $id_or_email );
			return $u instanceof WP_User ? $u : null;
		}
		// Comment object (WP_Comment or similar) with user_id for registered commenters.
		if ( is_object( $id_or_email ) && isset( $id_or_email->user_id ) && $id_or_email->user_id > 0 ) {
			$u = get_user_by( 'id', (int) $id_or_email->user_id );
			return $u instanceof WP_User ? $u : null;
		}
		return null;
	}

	private function getInitials( WP_User $user, string $source ): string {
		$raw = '';
		switch ( $source ) {
			case 'username':
				$raw = $user->user_login;
				break;
			case 'display_name':
				$raw = $user->display_name;
				break;
			case 'full_name':
				$raw = $user->display_name;
				$raw = preg_replace( '/[^a-zA-Z0-9\s]/', '', $raw );
				$words = preg_split( '/\s+/', trim( $raw ), -1, PREG_SPLIT_NO_EMPTY );
				$initials = '';
				foreach ( $words as $word ) {
					if ( $word !== '' ) {
						$initials .= mb_substr( $word, 0, 1, 'UTF-8' );
						if ( mb_strlen( $initials, 'UTF-8' ) >= 2 ) {
							break;
						}
					}
				}
				return $initials !== '' ? mb_strtoupper( mb_substr( $initials, 0, 2, 'UTF-8' ), 'UTF-8' ) : $this->fallbackInitials( $user );
			default:
				// first_last: use user meta (WP_User does not include first_name/last_name by default).
				$first = trim( (string) get_user_meta( $user->ID, 'first_name', true ) );
				$last  = trim( (string) get_user_meta( $user->ID, 'last_name', true ) );
				if ( $first !== '' && $last !== '' ) {
					$raw = $first . ' ' . $last;
				} elseif ( $first !== '' || $last !== '' ) {
					$raw = $first . $last;
				} else {
					$raw = ( $user->display_name ?? '' ) !== '' ? $user->display_name : $user->user_login;
				}
				break;
		}
		$raw = preg_replace( '/[^a-zA-Z0-9\s]/', '', $raw );
		$raw = preg_replace( '/\s+/', ' ', trim( $raw ) );
		if ( $raw === '' ) {
			return '?';
		}
		$parts = explode( ' ', $raw, 2 );
		if ( $source === 'username' || $source === 'display_name' ) {
			$s = mb_substr( $parts[0], 0, 2, 'UTF-8' );
			return mb_strtoupper( $s, 'UTF-8' );
		}
		$l1 = mb_substr( $parts[0], 0, 1, 'UTF-8' );
		$l2 = isset( $parts[1] ) && $parts[1] !== '' ? mb_substr( $parts[1], 0, 1, 'UTF-8' ) : mb_substr( $parts[0], 1, 1, 'UTF-8' );
		if ( $l2 === '' ) {
			$l2 = $l1;
		}
		return mb_strtoupper( $l1 . $l2, 'UTF-8' );
	}

	private function fallbackInitials( WP_User $user ): string {
		$raw = ( $user->display_name ?? '' ) !== '' ? $user->display_name : $user->user_login;
		$raw = preg_replace( '/[^a-zA-Z0-9\s]/', '', $raw );
		$s   = mb_substr( trim( $raw ), 0, 2, 'UTF-8' );
		return $s !== '' ? mb_strtoupper( $s, 'UTF-8' ) : '?';
	}

	private function getColor( WP_User $user, array $settings ): string {
		if ( ( $settings['bg_mode'] ?? 'random' ) === 'custom' ) {
			$c = $settings['custom_color'] ?? self::DEFAULT_COLOR;
			return $this->sanitizeHex( $c ) ?: self::DEFAULT_COLOR;
		}
		$idx = absint( $user->ID ) % count( self::PALETTE );
		return self::PALETTE[ $idx ];
	}

	/**
	 * Sanitize hex color value.
	 *
	 * @param string $hex Hex color string.
	 * @return string|null Sanitized hex color or null if invalid.
	 */
	private function sanitizeHex( string $hex ): ?string {
		$hex = ltrim( $hex, '#' );
		// Security: Only allow valid hex color format
		if ( preg_match( '/^[a-fA-F0-9]{6}$/', $hex ) || preg_match( '/^[a-fA-F0-9]{3}$/', $hex ) ) {
			return '#' . strtolower( $hex );
		}
		return null;
	}

	/**
	 * Build SVG avatar markup.
	 * Uses dynamic size for proper scaling.
	 *
	 * @param string $initials User initials.
	 * @param string $color Background color.
	 * @param string $shape Shape (circle, rounded, or square).
	 * @param string $size_set Size setting (small or regular).
	 * @param int    $pixel_size Requested pixel size (e.g. 64).
	 * @return string SVG markup.
	 */
	private function buildSvg( string $initials, string $color, string $shape, string $size_set, int $pixel_size = 100 ): string {
		$font_size_ratio = $size_set === 'small' ? 0.45 : 0.5;
		$font_size       = (int) round( $pixel_size * $font_size_ratio );
		$radius          = $pixel_size / 2;
		$border_radius   = (int) round( $pixel_size * 0.2 );
		// Security: Sanitize color to prevent XSS
		$bg = $this->sanitizeHex( $color ) ?: self::DEFAULT_COLOR;
		// Security: Sanitize initials to prevent XSS
		$text = mb_substr( $initials, 0, 2, 'UTF-8' );
		$text = preg_replace( '/[^A-Za-z0-9?]/', '?', $text ) ?: '?';

		if ( $shape === 'circle' ) {
			$bg_el = sprintf( '<circle cx="%d" cy="%d" r="%d" fill="%s"/>', $radius, $radius, $radius, esc_attr( $bg ) );
		} elseif ( $shape === 'rounded' ) {
			$bg_el = sprintf( '<rect width="%d" height="%d" rx="%d" ry="%d" fill="%s"/>', $pixel_size, $pixel_size, $border_radius, $border_radius, esc_attr( $bg ) );
		} else {
			$bg_el = sprintf( '<rect width="%d" height="%d" fill="%s"/>', $pixel_size, $pixel_size, esc_attr( $bg ) );
		}

		$svg = sprintf(
			'<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">%s<text x="50%%" y="50%%" font-family="-apple-system,BlinkMacSystemFont,\'Segoe UI\',Arial,sans-serif" font-size="%d" font-weight="600" fill="#ffffff" text-anchor="middle" dominant-baseline="central">%s</text></svg>',
			$pixel_size,
			$pixel_size,
			$pixel_size,
			$pixel_size,
			$bg_el,
			$font_size,
			esc_html( $text )
		);

		return $svg;
	}

	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}
		$raw = $this->moduleSettings()->all( [] );
		if ( ! is_array( $raw ) ) {
			$raw = [];
		}
		$defaults = [
			'letter_source' => 'first_last',
			'bg_mode'       => 'random',
			'custom_color'  => self::DEFAULT_COLOR,
			'shape'         => 'circle',
			'size'          => 'regular',
		];
		$this->settings_cache = array_merge( $defaults, $raw );
		return $this->settings_cache;
	}

}

