<?php

declare(strict_types=1);

namespace Kreait\Firebase\Messaging;

use Kreait\Firebase\Exception\InvalidArgumentException;

final class MessageData implements \JsonSerializable
{
    /** @var array<string, string> */
    private array $data = [];

    private function __construct()
    {
    }

    /**
     * @param array<array-key, mixed> $data
     */
    public static function fromArray(array $data): self
    {
        $messageData = new self();

        foreach ($data as $key => $value) {
            if (!self::isStringable($key) || !self::isStringable($value)) {
                throw new InvalidArgumentException('Message data must be a one-dimensional array of string(able) keys and values.');
            }

            $key = (string) $key;
            $value = (string) $value;

            if (self::isBinary($value)) {
                throw new InvalidArgumentException(
                    "The message data field '{$key}' seems to contain binary data. As this can lead to broken messages, "
                    .'please convert it to a string representation first, e.g. with bin2hex() or base64encode().'
                );
            }

            self::assertValidKey($key);

            $messageData->data[$key] = $value;
        }

        return $messageData;
    }

    /**
     * @return array<string, string>
     */
    public function jsonSerialize(): array
    {
        return $this->data;
    }

    /**
     * @param mixed $value
     */
    private static function isStringable($value): bool
    {
        return null === $value || \is_scalar($value) || (\is_object($value) && \method_exists($value, '__toString'));
    }

    private static function isBinary(string $value): bool
    {
        return \mb_detect_encoding($value, (array) \mb_detect_order(), true) === false;
    }

    /**
     * @see https://firebase.google.com/docs/cloud-messaging/concept-options#data_messages
     */
    private static function assertValidKey(string $value): void
    {
        $value = \mb_strtolower($value);
        // According to the docs, "notification" is reserved, but it's still accepted ¯\_(ツ)_/¯
        $reservedWords = ['from', /*'notification',*/ 'message_type'];
        $reservedPrefixes = ['google', 'gcm'];

        if (\in_array($value, $reservedWords, true)) {
            throw new InvalidArgumentException("'{$value}' is a reserved word and can not be used as a key in FCM data payloads");
        }

        foreach ($reservedPrefixes as $prefix) {
            if (\str_starts_with($value, $prefix)) {
                throw new InvalidArgumentException("'{$prefix}' is a reserved prefix and can not be used as a key in FCM data payloads");
            }
        }
    }
}