<?php

namespace dokuwiki\Remote;

use dokuwiki\Remote\OpenApiDoc\DocBlockMethod;
use InvalidArgumentException;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use RuntimeException;

class ApiCall
{
    /** @var callable The method to be called for this endpoint */
    protected $method;

    /** @var bool Whether this call can be called without authentication */
    protected bool $isPublic = false;

    /** @var string The category this call belongs to */
    protected string $category;

    /** @var DocBlockMethod The meta data of this call as parsed from its doc block */
    protected $docs;

    /**
     * Make the given method available as an API call
     *
     * @param string|array $method Either [object,'method'] or 'function'
     * @param string $category The category this call belongs to
     */
    public function __construct($method, $category = '')
    {
        if (!is_callable($method)) {
            throw new InvalidArgumentException('Method is not callable');
        }

        $this->method = $method;
        $this->category = $category;
    }

    /**
     * Call the method
     *
     * Important: access/authentication checks need to be done before calling this!
     *
     * @param array $args
     * @return mixed
     */
    public function __invoke($args)
    {
        if (!array_is_list($args)) {
            $args = $this->namedArgsToPositional($args);
        }
        return call_user_func_array($this->method, $args);
    }

    /**
     * Access the method documentation
     *
     * This lazy loads the docs only when needed
     *
     * @return DocBlockMethod
     */
    public function getDocs()
    {
        if ($this->docs === null) {
            try {
                if (is_array($this->method)) {
                    $reflect = new ReflectionMethod($this->method[0], $this->method[1]);
                } else {
                    $reflect = new ReflectionFunction($this->method);
                }
                $this->docs = new DocBlockMethod($reflect);
            } catch (ReflectionException $e) {
                throw new RuntimeException('Failed to parse API method documentation', 0, $e);
            }
        }
        return $this->docs;
    }

    /**
     * Is this a public method?
     *
     * Public methods can be called without authentication
     *
     * @return bool
     */
    public function isPublic()
    {
        return $this->isPublic;
    }

    /**
     * Set the public flag
     *
     * @param bool $isPublic
     * @return $this
     */
    public function setPublic(bool $isPublic = true)
    {
        $this->isPublic = $isPublic;
        return $this;
    }

    /**
     * Get information about the argument of this call
     *
     * @return array
     */
    public function getArgs()
    {
        return $this->getDocs()->getParameters();
    }

    /**
     * Get information about the return value of this call
     *
     * @return array
     */
    public function getReturn()
    {
        return $this->getDocs()->getReturn();
    }

    /**
     * Get the summary of this call
     *
     * @return string
     */
    public function getSummary()
    {
        return $this->getDocs()->getSummary();
    }

    /**
     * Get the description of this call
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->getDocs()->getDescription();
    }

    /**
     * Get the category of this call
     *
     * @return string
     */
    public function getCategory()
    {
        return $this->category;
    }

    /**
     * Converts named arguments to positional arguments
     *
     * @fixme with PHP 8 we can use named arguments directly using the spread operator
     * @param array $params
     * @return array
     */
    protected function namedArgsToPositional($params)
    {
        $args = [];

        foreach ($this->getDocs()->getParameters() as $arg => $arginfo) {
            if (isset($params[$arg])) {
                $args[] = $params[$arg];
            } elseif ($arginfo['optional'] && array_key_exists('default', $arginfo)) {
                $args[] = $arginfo['default'];
            } else {
                throw new InvalidArgumentException("Missing argument $arg");
            }
        }

        return $args;
    }
}