<?php

declare(strict_types=1);

namespace Kreait\Firebase\Messaging;

use JsonSerializable;
use Kreait\Firebase\Exception\Messaging\InvalidArgument;

use function array_filter;
use function array_key_exists;

/**
 * @see https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification
 * @see https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns
 * @see https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#apnsconfig
 *
 * @phpstan-type ApnsConfigShape array{
 *     headers?: array<non-empty-string, non-empty-string>,
 *     payload?: array<non-empty-string, mixed>,
 *     fcm_options?: array{
 *         analytics_label?: string,
 *         image?: string
 *     }
 * }
 */
final class ApnsConfig implements JsonSerializable
{
    private const PRIORITY_CONSERVE_POWER = '5';
    private const PRIORITY_IMMEDIATE = '10';

    /**
     * @param array<non-empty-string, non-empty-string> $headers
     * @param array<non-empty-string, mixed> $payload
     * @param array<non-empty-string, string> $fcmOptions
     */
    private function __construct(
        private array $headers,
        private array $payload,
        private readonly array $fcmOptions,
    ) {
    }

    public static function new(): self
    {
        return new self([], [], []);
    }

    /**
     * @param ApnsConfigShape $data
     */
    public static function fromArray(array $data): self
    {
        $headers = $data['headers'] ?? [];
        $payload = $data['payload'] ?? [];
        $fcmOptions = $data['fcm_options'] ?? [];

        return new self($headers, $payload, $fcmOptions);
    }

    /**
     * @param non-empty-string $name
     * @param non-empty-string $value
     */
    public function withHeader(string $name, string $value): self
    {
        $config = clone $this;
        $config->headers[$name] = $value;

        return $config;
    }

    public function hasHeader(string $name): bool
    {
        return array_key_exists($name, $this->headers);
    }

    /**
     * @param non-empty-string $key
     */
    public function withApsField(string $key, mixed $value): self
    {
        $config = clone $this;
        $config->payload['aps'] ??= [];
        $config->payload['aps'][$key] = $value;

        return $config;
    }

    /**
     * @param non-empty-string $name
     */
    public function withDataField(string $name, mixed $value): self
    {
        if ($name === 'aps') {
            throw new InvalidArgument('"aps" is a reserved field name');
        }

        $config = clone $this;
        $config->payload[$name] = $value;

        return $config;
    }

    public function withDefaultSound(): self
    {
        return $this->withSound('default');
    }

    /**
     * The name of a sound file in your app’s main bundle or in the Library/Sounds folder of your app’s
     * container directory. Specify the string "default" to play the system sound.
     */
    public function withSound(string $sound): self
    {
        return $this->withApsField('sound', $sound);
    }

    /**
     * The number to display in a badge on your app’s icon. Specify 0 to remove the current badge, if any.
     */
    public function withBadge(int $number): self
    {
        return $this->withApsField('badge', $number);
    }

    public function withImmediatePriority(): self
    {
        return $this->withPriority(self::PRIORITY_IMMEDIATE);
    }

    public function withPowerConservingPriority(): self
    {
        return $this->withPriority(self::PRIORITY_CONSERVE_POWER);
    }

    /**
     * @param non-empty-string $priority
     */
    public function withPriority(string $priority): self
    {
        return $this->withHeader('apns-priority', $priority);
    }

    /**
     * A subtitle of the notification, supported by iOS 9+, silently ignored for others.
     */
    public function withSubtitle(string $subtitle): self
    {
        return $this->withApsField('subtitle', $subtitle);
    }

    public function isAlert(): bool
    {
        return
            isset($this->payload['aps']['alert'])
            || isset($this->payload['aps']['badge'])
            || isset($this->payload['aps']['sound']);
    }

    /**
     * @return array<non-empty-string, mixed>
     */
    public function data(): array
    {
        $payload = $this->payload;

        unset($payload['aps']);

        return $payload;
    }

    /**
     * @return array<non-empty-string, mixed>
     */
    public function toArray(): array
    {
        $filter = static fn($value): bool => $value !== null && $value !== [];

        return array_filter([
            'headers' => array_filter($this->headers, $filter),
            'payload' => array_filter($this->payload, $filter),
            'fcm_options' => array_filter($this->fcmOptions, $filter),
        ], $filter);
    }

    public function jsonSerialize(): array
    {
        return $this->toArray();
    }
}
