<?php

namespace Abivia\Ledger\Messages;

use Abivia\Ledger\Exceptions\Breaker;
use Abivia\Ledger\Helpers\Revision;
use Illuminate\Http\Request;
use const JSON_BIGINT_AS_STRING;


abstract class Message
{
    // Message flag settings State/Function flags from bit 30 down
    /**
     * Set when the request came from the JSON API.
     */
    public const F_API = 2**30;

    /**
     * Set on a request to validate the message.
     */
    public const F_VALIDATE = 2**29;

    /**
     * Bitmask for all the OP_ constants.
     */
    public const ALL_OPS = 0b11111111;

    // Operation flags from bit 0 up.
    public const OP_ADD = 1;
    public const OP_BATCH = 2**6;
    public const OP_CREATE = 2;
    public const OP_DELETE = 2**2;
    public const OP_GET = 2**3;
    public const OP_LOCK = 2**7;
    public const OP_QUERY = 2**4;
    public const OP_UPDATE = 2**5;

    /**
     * @var array Each element is either a property name or array of
     * [property name, operation mask]. If a mask is provided it must
     * match the current operation flags. The property name can also
     * be an array of [object property, source property].
     */
    protected static array $copyable = [];

    /**
     * @var bool Set when processing the body of a batch message to block nested batches
     */
    protected static bool $inBatch = false;
    /**
     * @var int The operation associated with this message.
     */
    protected int $opFlags = 0;

    private static array $opMap = [
        'add' => self::OP_ADD,
        'batch' => self::OP_BATCH,
        'create' => self::OP_CREATE,
        'delete' => self::OP_DELETE,
        'get' => self::OP_GET,
        'lock' => self::OP_LOCK,
        'query' => self::OP_QUERY,
        'update' => self::OP_UPDATE,
        'validate' => self::F_VALIDATE,
    ];

    /**
     * Selectively copy information from a data array.
     *
     * @param array $data
     * @param int $opFlags
     * @return $this
     */
    public function copy(array $data, int $opFlags): self
    {
        $this->opFlags = $opFlags;
        foreach (static::$copyable as $info) {
            if (is_array($info)) {
                [$property, $mask] = $info;
                if (is_array($property)) {
                    [$property, $fromProperty] = $property;
                } else {
                    $fromProperty = $property;
                }
            } else {
                $property = $info;
                $fromProperty = $info;
                $mask = Message::ALL_OPS;
            }
            if (($opFlags & $mask) && isset($data[$fromProperty])) {
                $this->{$property} = $data[$fromProperty];
            }
        }

        return $this;
    }

    /**
     * Populate the message with data from an array of request data.
     *
     * @param array $data Data generated by the request.
     * @param int $opFlags Bitmask of the request operation (may include FM_VALIDATE)
     * @return static Message initialized with relevant data.
     * @throws Breaker On error, e.g. required data is missing or on validation.
     */
    public abstract static function fromArray(array $data, int $opFlags = 0): self;

    /**
     * Create a message from the request payload, bypassing some middleware.
     * @param Request $request
     * @param int $opFlags
     * @return static
     * @throws Breaker
     */
    public static function fromRequest(Request $request, int $opFlags = 0): self
    {
        try {
            $content = json_decode(
                $request->getContent(),
                true,
                512,
                JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR
            );
        } catch (\JsonException $exception) {
            throw Breaker::withCode(
                Breaker::BAD_REQUEST,
                [__(
                    'Request is not valid JSON: :message',
                    ['message' => $exception->getMessage()]
                )]
            );
        }
        return static::fromArray($content, $opFlags);
    }

    public function getOpFlags(): int {
        return $this->opFlags;
    }

    /**
     * Make sure a revision code is present
     * @param array $errors
     * @return void
     */
    protected function requireRevision(array &$errors)
    {
        if (!isset($this->revision)) {
            if (Revision::isInBatch()) {
                $this->revision = '&';
            } else {
                $errors[] = __("This request must supply a revision.");
            }
        }
    }

    /**
     * Convert a method name to an operation bitmask.
     *
     * @param string $method The method name.
     * @param array $options Options are:
     * add Bitmask of flags to add to the result.
     * allowZero boolean, if not set an exception is thrown when there is no matching flag.
     * disallow Bitmask of methods to ignore.
     * @return int Operation bitmask, zero if not recognized or disallowed.
     * @throws Breaker
     */
    public static function toOpFlags(string $method, array $options = []): int
    {
        $opFlags = self::$opMap[$method] ?? 0;
        if ($opFlags !== 0 && isset($options['disallow'])) {
            $opFlags &= ~$options['disallow'];
        }
        if (!($options['allowZero'] ?? false) && $opFlags === 0) {
            throw Breaker::withCode(
                Breaker::RULE_VIOLATION,
                [__(':operation is not a valid function.', ['operation' => $method])]
            );
        }
        if (isset($options['add'])) {
            $opFlags |= $options['add'];
        }
        return $opFlags;
    }

    /**
     * Check the message for validity.
     *
     * @param int|null $opFlags Operation bitmask.
     * @return self
     * @throws Breaker When data is not valid.
     */
    public abstract function validate(?int $opFlags): self;

}
