<?php

declare( strict_types = 1 );

namespace Automattic\WooCommerce\Internal\PushNotifications\Entities;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenInvalidDataException;
use Automattic\WooCommerce\Internal\PushNotifications\Validators\PushTokenValidator;

/**
 * Object representation of a push token.
 *
 * @since 10.4.0
 */
class PushToken {
	/**
	 * WordPress post type for storing push tokens.
	 */
	const POST_TYPE = 'wc_push_token';

	/**
	 * The locale to use for tokens when the locale is not set, e.g. for tokens
	 * created before `device_locale` was added.
	 */
	const DEFAULT_DEVICE_LOCALE = 'en_US';

	/**
	 * Platform identifier for Apple devices.
	 */
	const PLATFORM_APPLE = 'apple';

	/**
	 * Platform identifier for Android devices.
	 */
	const PLATFORM_ANDROID = 'android';

	/**
	 * Platform identifier for web browsers.
	 */
	const PLATFORM_BROWSER = 'browser';

	/**
	 * Origin identifier for WooCommerce Android app.
	 */
	const ORIGIN_WOOCOMMERCE_ANDROID = 'com.woocommerce.android';

	/**
	 * Origin identifier for WooCommerce Android app development builds.
	 */
	const ORIGIN_WOOCOMMERCE_ANDROID_DEV = 'com.woocommerce.android:dev';

	/**
	 * Origin identifier for WooCommerce iOS app.
	 */
	const ORIGIN_WOOCOMMERCE_IOS = 'com.automattic.woocommerce';

	/**
	 * Origin identifier for WooCommerce iOS app development builds.
	 */
	const ORIGIN_WOOCOMMERCE_IOS_DEV = 'com.automattic.woocommerce:dev';

	/**
	 * Origin identifier for browsers.
	 */
	const ORIGIN_BROWSER = 'browser';

	/**
	 * List of valid platforms.
	 */
	const PLATFORMS = array(
		self::PLATFORM_APPLE,
		self::PLATFORM_ANDROID,
		self::PLATFORM_BROWSER,
	);

	/**
	 * List of valid origins.
	 */
	const ORIGINS = array(
		self::ORIGIN_BROWSER,
		self::ORIGIN_WOOCOMMERCE_ANDROID,
		self::ORIGIN_WOOCOMMERCE_ANDROID_DEV,
		self::ORIGIN_WOOCOMMERCE_IOS,
		self::ORIGIN_WOOCOMMERCE_IOS_DEV,
	);

	/**
	 * The ID of the token post.
	 *
	 * @var int|null
	 */
	private ?int $id = null;

	/**
	 * The ID of the user who owns the token.
	 *
	 * @var int|null
	 */
	private ?int $user_id = null;

	/**
	 * The token representing a device we can send a push notification to.
	 *
	 * @var string|null
	 */
	private ?string $token = null;

	/**
	 * The UUID of the device that generated the token.
	 *
	 * @var string|null
	 */
	private ?string $device_uuid = null;

	/**
	 * The platform the token was generated by.
	 *
	 * @var string|null
	 */
	private ?string $platform = null;

	/**
	 * The origin the token belongs to.
	 *
	 * @var string|null
	 */
	private ?string $origin = null;

	/**
	 * The locale of the device the token belongs to.
	 *
	 * @var string|null
	 */
	private ?string $device_locale = null;

	/**
	 * An array of metadata for the token.
	 *
	 * @var array|null
	 */
	private ?array $metadata = null;

	/**
	 * Creates a new PushToken instance with the given data.
	 *
	 * @param array $data Optional array with keys: id, user_id, token, device_uuid, platform, origin.
	 * @throws PushTokenInvalidDataException If any of the provided values fail validation.
	 *
	 * @since 10.6.0
	 */
	public function __construct( array $data = array() ) {
		if ( array_key_exists( 'id', $data ) ) {
			$this->set_id( (int) $data['id'] );
		}

		if ( array_key_exists( 'user_id', $data ) ) {
			$this->set_user_id( (int) $data['user_id'] );
		}

		if ( array_key_exists( 'token', $data ) ) {
			$this->set_token( (string) $data['token'] );
		}

		if ( array_key_exists( 'device_uuid', $data ) ) {
			$this->set_device_uuid( (string) $data['device_uuid'] );
		}

		if ( array_key_exists( 'platform', $data ) ) {
			$this->set_platform( (string) $data['platform'] );
		}

		if ( array_key_exists( 'origin', $data ) ) {
			$this->set_origin( (string) $data['origin'] );
		}

		if ( array_key_exists( 'device_locale', $data ) ) {
			$this->set_device_locale( (string) $data['device_locale'] );
		}

		if ( array_key_exists( 'metadata', $data ) ) {
			$this->set_metadata( (array) $data['metadata'] );
		}
	}

	/**
	 * Validates and sets the ID.
	 *
	 * @param int $id The ID of the token post.
	 * @throws PushTokenInvalidDataException If ID is not valid.
	 * @return void
	 *
	 * @since 10.4.0
	 */
	public function set_id( int $id ): void {
		$result = PushTokenValidator::validate( compact( 'id' ), array( 'id' ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new PushTokenInvalidDataException( $result->get_error_message() );
		}

		$this->id = $id;
	}

	/**
	 * Validates and sets the user ID.
	 *
	 * @param int $user_id The ID of the user who owns the token.
	 * @throws PushTokenInvalidDataException If user ID is not valid.
	 * @return void
	 *
	 * @since 10.4.0
	 */
	public function set_user_id( int $user_id ): void {
		$result = PushTokenValidator::validate( compact( 'user_id' ), array( 'user_id' ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new PushTokenInvalidDataException( $result->get_error_message() );
		}

		$this->user_id = $user_id;
	}

	/**
	 * Validates and sets the token.
	 *
	 * @param string $token The token representing a device we can send a push notification to.
	 * @throws PushTokenInvalidDataException If token is not valid.
	 * @return void
	 *
	 * @since 10.4.0
	 */
	public function set_token( string $token ): void {
		$result = PushTokenValidator::validate( compact( 'token' ), array( 'token' ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new PushTokenInvalidDataException( $result->get_error_message() );
		}

		$this->token = trim( $token );
	}

	/**
	 * Validates and sets the device UUID, normalize empty (non-null) values to null.
	 *
	 * @param string|null $device_uuid The UUID of the device that generated the token.
	 * @throws PushTokenInvalidDataException If device UUID is not valid.
	 * @return void
	 *
	 * @since 10.4.0
	 */
	public function set_device_uuid( ?string $device_uuid ): void {
		$result = PushTokenValidator::validate( compact( 'device_uuid' ), array( 'device_uuid' ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new PushTokenInvalidDataException( $result->get_error_message() );
		}

		if ( null !== $device_uuid ) {
			$device_uuid = trim( $device_uuid );
		}

		$this->device_uuid = $device_uuid ? $device_uuid : null;
	}

	/**
	 * Validates and sets the device locale.
	 *
	 * @param string $device_locale The locale of the device the token belongs to.
	 * @throws PushTokenInvalidDataException If device locale is not valid.
	 * @return void
	 *
	 * @since 10.6.0
	 */
	public function set_device_locale( string $device_locale ): void {
		$result = PushTokenValidator::validate( compact( 'device_locale' ), array( 'device_locale' ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new PushTokenInvalidDataException( $result->get_error_message() );
		}

		$this->device_locale = trim( $device_locale );
	}

	/**
	 * Validates and sets the platform.
	 *
	 * @param string $platform The platform the token was generated by.
	 * @throws PushTokenInvalidDataException If platform is not valid.
	 * @return void
	 *
	 * @since 10.4.0
	 */
	public function set_platform( string $platform ): void {
		$result = PushTokenValidator::validate( compact( 'platform' ), array( 'platform' ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new PushTokenInvalidDataException( $result->get_error_message() );
		}

		$this->platform = trim( $platform );
	}

	/**
	 * Validates and sets the origin.
	 *
	 * @param string $origin The origin of the token, e.g. the app it came from.
	 * @throws PushTokenInvalidDataException If origin is not valid.
	 * @return void
	 *
	 * @since 10.4.0
	 */
	public function set_origin( string $origin ): void {
		$result = PushTokenValidator::validate( compact( 'origin' ), array( 'origin' ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new PushTokenInvalidDataException( $result->get_error_message() );
		}

		$this->origin = trim( $origin );
	}

	/**
	 * Validates and sets the metadata.
	 *
	 * @param array $metadata An array of metadata for the token, e.g. the app version, device OS etc.
	 * @throws PushTokenInvalidDataException If metadata is not valid.
	 * @return void
	 *
	 * @since 10.6.0
	 */
	public function set_metadata( array $metadata ): void {
		$result = PushTokenValidator::validate( compact( 'metadata' ), array( 'metadata' ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new PushTokenInvalidDataException( $result->get_error_message() );
		}

		if ( ! empty( $metadata ) ) {
			$keys   = array_map( 'sanitize_key', array_keys( $metadata ) );
			$values = array_map( 'sanitize_text_field', array_values( $metadata ) );

			/**
			 * Typehint for PHPStan, as it can't infer the $keys and $values are
			 * the same length therefore array_combine won't return false.
			 *
			 * @var array<string, string> $metadata
			 */
			$metadata = array_combine( $keys, $values );
		}

		$this->metadata = $metadata;
	}

	/**
	 * Gets the ID.
	 *
	 * @return int|null
	 *
	 * @since 10.4.0
	 */
	public function get_id(): ?int {
		return $this->id;
	}

	/**
	 * Gets the user ID.
	 *
	 * @return int|null
	 *
	 * @since 10.4.0
	 */
	public function get_user_id(): ?int {
		return $this->user_id;
	}

	/**
	 * Gets the token.
	 *
	 * @return string|null
	 *
	 * @since 10.4.0
	 */
	public function get_token(): ?string {
		return $this->token;
	}

	/**
	 * Gets the device UUID.
	 *
	 * @return string|null
	 *
	 * @since 10.4.0
	 */
	public function get_device_uuid(): ?string {
		return $this->device_uuid;
	}

	/**
	 * Gets the platform.
	 *
	 * @return string|null
	 *
	 * @since 10.4.0
	 */
	public function get_platform(): ?string {
		return $this->platform;
	}

	/**
	 * Gets the origin.
	 *
	 * @return string|null
	 *
	 * @since 10.4.0
	 */
	public function get_origin(): ?string {
		return $this->origin;
	}

	/**
	 * Gets the device locale.
	 *
	 * @return string|null
	 *
	 * @since 10.6.0
	 */
	public function get_device_locale(): ?string {
		return $this->device_locale;
	}

	/**
	 * Gets the metadata.
	 *
	 * @return array|null
	 *
	 * @since 10.6.0
	 */
	public function get_metadata(): ?array {
		return $this->metadata;
	}

	/**
	 * Returns this token formatted for the WPCOM push notifications endpoint.
	 *
	 * @return array{user_id: int|null, token: string|null, origin: string|null, device_locale: string|null}
	 *
	 * @since 10.7.0
	 */
	public function to_wpcom_format(): array {
		return array(
			'user_id'       => $this->user_id,
			'token'         => $this->token,
			'origin'        => $this->origin,
			'device_locale' => $this->device_locale ?? self::DEFAULT_DEVICE_LOCALE,
		);
	}

	/**
	 * Determines whether this token can be created.
	 *
	 * @return bool
	 *
	 * @since 10.4.0
	 */
	public function can_be_created(): bool {
		return ! $this->get_id() && $this->has_required_parameters();
	}

	/**
	 * Determines whether this token can be updated.
	 *
	 * @return bool
	 *
	 * @since 10.4.0
	 */
	public function can_be_updated(): bool {
		return $this->get_id() && $this->has_required_parameters();
	}

	/**
	 * Determines whether this token can be read.
	 *
	 * @return bool
	 *
	 * @since 10.4.0
	 */
	public function can_be_read(): bool {
		return (bool) $this->get_id();
	}

	/**
	 * Determines whether this token can be deleted.
	 *
	 * @return bool
	 *
	 * @since 10.4.0
	 */
	public function can_be_deleted(): bool {
		return (bool) $this->get_id();
	}

	/**
	 * Determines whether all the required non-ID parameters are filled.
	 *
	 * @return bool
	 *
	 * @since 10.4.0
	 */
	private function has_required_parameters(): bool {
		return $this->get_user_id()
			&& $this->get_token()
			&& $this->get_platform()
			&& $this->get_origin()
			&& $this->get_device_locale()
			&& (
				$this->get_device_uuid()
				|| $this->get_platform() === self::PLATFORM_BROWSER
			);
	}
}
