<?php

declare(strict_types=1);

namespace MemberPress\GroundLevel\InProductNotifications\Services;

use MemberPress\GroundLevel\InProductNotifications\Models\Notification;
use MemberPress\GroundLevel\Support\Time;
use InvalidArgumentException;

/**
 * Store service for managing IPN notification data.
 */
class Store
{
    /**
     * Filter for unread notifications.
     *
     * @var string
     */
    public const FILTER_UNREAD = 'unread';

    /**
     * The option key/name where notification data will be stored.
     *
     * @var string
     */
    protected string $key;

    /**
     * The data to be stored.
     *
     * @var array
     */
    protected array $data = [];

    /**
     * Constructor.
     *
     * @param \MemberPress\GroundLevel\InProductNotifications\Util $util The IPN utility service.
     */
    public function __construct(\MemberPress\GroundLevel\InProductNotifications\Util $util)
    {
        $this->key = $util->prefixId('store');
    }

    /**
     * Adds a notification to the store.
     *
     * @param  array   $raw        The raw notification data.
     * @param  boolean $withLastId If true, set the last notification ID.
     * @return self
     */
    public function add(array $raw, bool $withLastId = false): self
    {
        $notification                  = new Notification($raw);
        $this->data[$notification->id] = $notification->toArray();
        if ($withLastId) {
            $this->setLastId($notification->id);
        }
        return $this;
    }

    /**
     * Clear all stored data.
     *
     * @return self
     */
    public function clear(): self
    {
        $this->data = [];
        return $this;
    }

    /**
     * Delete a notification from the store.
     *
     * @param  string $id The notification ID.
     * @return self
     */
    public function delete(string $id): self
    {
        unset($this->data[$id]);
        return $this;
    }

    /**
     * Retrieves stored data from the database.
     *
     * @param  boolean $force If true, force a fetch from the database.
     * @return self
     */
    public function fetch(bool $force = false): self
    {
        if (empty($this->data) || $force) {
            $data       = get_option($this->key, []);
            $this->data = is_array($data) ? $data : [];
        }
        return $this;
    }

    /**
     * Retrieves a notification by ID.
     *
     * @param  string $id      The notification ID.
     * @param  string $context The notification context.
     * @return Notification|null
     */
    public function get(string $id, string $context = Notification::CONTEXT_DISPLAY): ?Notification
    {
        $raw = $this->data[$id] ?? [];
        return empty($raw) ? null : new Notification($raw, $context);
    }

    /**
     * Determines if a notification exists.
     *
     * @param  string $id The notification ID.
     * @return boolean
     */
    public function has(string $id): bool
    {
        return isset($this->data[$id]);
    }

    /**
     * Retrieves the last notification ID.
     *
     * @return string
     */
    public function lastId(): string
    {
        return $this->data['__lastId'] ?? '';
    }

    /**
     * Mark a notification as read.
     *
     * @param  string $id The notification ID.
     * @return self
     */
    public function markRead(string $id): self
    {
        if (isset($this->data[$id])) {
            $this->update(
                $id,
                [
                    'read'   => true,
                    'readAt' => Time::now(),
                ]
            );
        }
        return $this;
    }

    /**
     * Retrieves notifications for display.
     *
     * This method converts notifications to an array of arrays to which can be
     * converted to JSON and passed to the React inbox component.
     *
     * @param  boolean $asArray If true, return an array of arrays instead of
     *                          Notification objects.
     * @param  string  $filter  The filter to apply, must be one of {@see self::FILTER_*}
     *                          constants.
     * @return array<array|Notification>
     * @throws InvalidArgumentException When an invalid $filter is provided.
     */
    public function notifications(bool $asArray = true, string $filter = ''): array
    {
        if (! empty($filter) && self::FILTER_UNREAD !== $filter) {
            throw new InvalidArgumentException(
                "Invalid filter: {$filter}, must be one of: " . self::FILTER_UNREAD
            );
        }

        $notifications = $this->data;
        unset($notifications['__lastId']);
        return array_values(
            array_filter(
                array_map(
                    function (array $notification) use ($asArray, $filter) {
                        $notification = new Notification(
                            $notification,
                            Notification::CONTEXT_DISPLAY
                        );

                        // Remove expired notifications.
                        if (! $notification->shouldDisplay()) {
                            return null;
                        }

                        // If filtering by unread only, remove read notifications.
                        if (self::FILTER_UNREAD === $filter && $notification->read) {
                            return null;
                        }

                        return $asArray ? $notification->toArray() : $notification;
                    },
                    $notifications
                )
            )
        );
    }

    /**
     * Persist data to the database.
     *
     * @return self
     */
    public function persist(): self
    {
        update_option($this->key, $this->data, false);
        return $this;
    }

    /**
     * Sets the last notification ID.
     *
     * @param  string $id The notification ID.
     * @return self
     */
    public function setLastId(string $id): self
    {
        $this->data['__lastId'] = $id;
        return $this;
    }

    /**
     * Updates a notification in the store.
     *
     * @param  string $id   The notification ID.
     * @param  array  $data The notification data.
     * @return \MemberPress\GroundLevel\InProductNotifications\Services\Store
     */
    public function update(string $id, array $data): self
    {
        if (isset($this->data[$id])) {
            $this->data[$id] = array_merge($this->data[$id], $data);
        }
        return $this;
    }
}
