<?php

namespace Beebmx\KirbySign;

use ArgumentCountError;
use Beebmx\KirbySign\Exceptions\InvalidSignatureKeyException;
use Carbon\Carbon;
use DateInterval;
use DateTimeInterface;
use Kirby\Cms\App;
use Kirby\Http\Uri;
use Kirby\Http\Url;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Collection;
use Kirby\Toolkit\Date;

class Signed
{
    protected ?string $key;

    protected string $sign;

    protected string $format;

    protected App $kirby;

    public function __construct(?string $key = null)
    {
        $this->kirby = App::instance();
        $this->key = ! empty($key) ? $key : $this->resolveKey();
        $this->sign = $this->getSign();
        $this->format = $this->getFormat();

        if (empty($this->key)) {
            throw new InvalidSignatureKeyException;
        }
    }

    public function url(string $path, array $data = [], Carbon|DateInterval|int|null $expiration = null, bool $absolute = true): string
    {
        $path = $this->path($path, $absolute);

        $parameters = A::wrap($data);

        if (! empty($expiration)) {
            $parameters = $parameters + ['expires' => $this->availableAt($expiration)];
        }

        ksort($parameters);

        $signedURL = hash_hmac(
            'sha256',
            Url::to($path, [$this->format => $parameters]),
            $this->key
        );

        return Url::to($path, [
            $this->format => [
                ...$parameters,
                $this->sign => $signedURL,
            ],
        ]);
    }

    public function isValid(bool $absolute = true): bool
    {
        $path = $this->path(
            path: $this->kirby->request()->path()->toString(),
            absolute: $absolute
        );

        return $this->hasValidSignature($this->kirby->request()->url(), $path)
            && $this->signatureHasNotExpired($this->kirby->request()->url());
    }

    public function hasValidSignature(Uri $request, string $path): bool
    {
        $queryString = (new Collection($this->getData($request)))
            ->remove($this->sign)
            ->toArray();

        $queryString = $this->mapWithKeys($queryString,
            fn ($value, $key) => $this->format === 'params' ? "{$key}:{$value}" : "{$key}={$value}"
        );

        $original = $this->format === 'params'
            ? rtrim($path.'/'.implode('/', $queryString), '/')
            : rtrim($path.'?'.implode('&', $queryString), '?');

        if (hash_equals(
            hash_hmac('sha256', $original, $this->key),
            $this->getSignature($request)
        )) {
            return true;
        }

        return false;
    }

    public function signatureHasNotExpired(Uri $request): bool
    {
        $expires = $request->query()->get('expires');
        $expires = $this->getExpires($request);

        return ! ($expires && Carbon::now()->getTimestamp() > $expires);
    }

    protected function resolveKey(): ?string
    {
        return $this->kirby->option(
            'beebmx.kirby-sign.key',
            $this->kirby->option(
                'app.key', null
            )
        );
    }

    protected function getSign(): string
    {
        return $this->kirby->option('beebmx.kirby-sign.sign', 'signature');
    }

    protected function getFormat(): string
    {
        return $this->kirby->option('beebmx.kirby-sign.format', 'query');
    }

    protected function path(string $path, bool $absolute = true): string
    {
        if ($absolute && Url::isAbsolute($path)) {
            return $path;
        }

        if ($absolute) {
            return Url::makeAbsolute($path, $this->kirby->url());
        }

        $uri = preg_replace('#^(//|[^/?])+#', '', $path);

        if ($base = $this->kirby->url()) {
            $uri = preg_replace('#^'.$base.'#i', '', $uri);
        }

        return '/'.ltrim($uri, '/');
    }

    protected function getData(Uri $request): array
    {
        return $this->format === 'params'
            ? $request->params()->toArray()
            : $request->query()->toArray();
    }

    protected function getSignature(Uri $request): string
    {
        return $this->format === 'params'
            ? $request->params()->get($this->sign, 'signature')
            : $request->query()->get($this->sign, 'signature');
    }

    protected function getExpires(Uri $request): ?string
    {
        return $this->format === 'params'
            ? $request->params()->get('expires')
            : $request->query()->get('expires');
    }

    protected function availableAt(Carbon|DateInterval|Date|int $delay = 0): int
    {
        $delay = $this->parseDateInterval($delay);

        return $delay instanceof DateTimeInterface
            ? $delay->getTimestamp()
            : Carbon::now()->addSeconds($delay)->getTimestamp();
    }

    protected function parseDateInterval($delay)
    {
        if ($delay instanceof DateInterval) {
            $delay = Carbon::now()->add($delay);
        }

        return $delay;
    }

    protected function mapWithKeys(array $array, callable $callback): array
    {
        $keys = array_keys($array);

        try {
            $items = array_map($callback, $array, $keys);
        } catch (ArgumentCountError) {
            $items = array_map($callback, $array);
        }

        return array_combine($keys, $items);
    }
}
