<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore;

use Chill\DocStoreBundle\AsyncUpload\Event\TempUrlGenerateEvent;
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Generate a temp url.
 */
final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterface
{
    private const CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

    private string $base_url;
    private string $key;
    private int $max_expire_delay;
    private int $max_submit_delay;
    private int $max_post_file_size;
    private int $max_file_count;

    public function __construct(
        private LoggerInterface $logger,
        private EventDispatcherInterface $event_dispatcher,
        private ClockInterface $clock,
        ParameterBagInterface $parameterBag,
    ) {
        $config = $parameterBag->get('chill_doc_store')['openstack']['temp_url'];

        $this->key = $config['temp_url_key'];
        $this->base_url = $config['temp_url_base_path'];
        $this->max_expire_delay = $config['max_expires_delay'];
        $this->max_submit_delay = $config['max_submit_delay'];
        $this->max_post_file_size = $config['max_post_file_size'];
        $this->max_file_count = $config['max_post_file_count'];
    }

    /**
     * @throws TempUrlGeneratorException
     */
    public function generatePost(
        ?int $expire_delay = null,
        ?int $submit_delay = null,
        int $max_file_count = 1,
        ?string $object_name = null,
    ): SignedUrlPost {
        $delay = $expire_delay ?? $this->max_expire_delay;
        $submit_delay ??= $this->max_submit_delay;

        if ($delay < 2) {
            throw new TempUrlGeneratorException("The delay of {$delay} is too ".'short (<2 sec) to properly use this token');
        }

        if ($delay > $this->max_expire_delay) {
            throw new TempUrlGeneratorException('The given delay is greater than the max delay authorized.');
        }

        if ($submit_delay < 15) {
            throw new TempUrlGeneratorException("The submit delay of {$delay} is too ".'short (<15 sec) to properly use this token');
        }

        if ($submit_delay > $this->max_submit_delay) {
            throw new TempUrlGeneratorException('The given submit delay is greater than the max submit delay authorized.');
        }

        if ($max_file_count > $this->max_file_count) {
            throw new TempUrlGeneratorException('The number of files is greater than the authorized number of files');
        }

        $expires = $this->clock->now()->add(new \DateInterval('PT'.(string) $delay.'S'));

        if (null === $object_name) {
            $object_name = $this->generateObjectName();
        }

        $g = new SignedUrlPost(
            $url = $this->generateUrl($object_name),
            $expires,
            $object_name,
            $this->max_post_file_size,
            $max_file_count,
            $submit_delay,
            '',
            $object_name,
            $this->generateSignaturePost($url, $expires)
        );

        $this->event_dispatcher->dispatch(
            new TempUrlGenerateEvent($g),
        );

        $this->logger->info(
            'generate signature for url',
            (array) $g
        );

        return $g;
    }

    /**
     * Generate an absolute public url for a GET request on the object.
     */
    public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl
    {
        if ($expire_delay > $this->max_expire_delay) {
            throw new TempUrlGeneratorException(sprintf('The expire delay (%d) is greater than the max_expire_delay (%d)', $expire_delay, $this->max_expire_delay));
        }
        $url = $this->generateUrl($object_name);

        $expires = $this->clock->now()
            ->add(new \DateInterval(sprintf('PT%dS', $expire_delay ?? $this->max_expire_delay)));
        $args = [
            'temp_url_sig' => $this->generateSignature($method, $url, $expires),
            'temp_url_expires' => $expires->format('U'),
        ];
        $url = $url.'?'.\http_build_query($args);

        $signature = new SignedUrl(strtoupper($method), $url, $expires, $object_name);

        $this->event_dispatcher->dispatch(
            new TempUrlGenerateEvent($signature)
        );

        return $signature;
    }

    private function generateUrl($relative_path): string
    {
        return match (str_ends_with($this->base_url, '/')) {
            true => $this->base_url.$relative_path,
            false => $this->base_url.'/'.$relative_path,
        };
    }

    private function generateObjectName()
    {
        // inspiration from https://stackoverflow.com/a/4356295/1572236
        $charactersLength = strlen(self::CHARACTERS);
        $randomString = '';
        for ($i = 0; $i < 21; ++$i) {
            $randomString .= self::CHARACTERS[random_int(0, $charactersLength - 1)];
        }

        return $randomString;
    }

    private function generateSignaturePost($url, \DateTimeImmutable $expires)
    {
        $path = \parse_url((string) $url, PHP_URL_PATH);

        $body = sprintf(
            "%s\n%s\n%s\n%s\n%s",
            $path,
            '', // redirect is empty
            (string) $this->max_post_file_size,
            (string) $this->max_file_count,
            $expires->format('U')
        )
        ;

        $this->logger->debug(
            'generate signature post',
            ['url' => $body, 'method' => 'POST']
        );

        return \hash_hmac('sha512', $body, $this->key, false);
    }

    private function generateSignature(string $method, $url, \DateTimeImmutable $expires)
    {
        if ('POST' === $method) {
            return $this->generateSignaturePost($url, $expires);
        }

        $path = \parse_url((string) $url, PHP_URL_PATH);
        $body = sprintf(
            "%s\n%s\n%s",
            strtoupper($method),
            $expires->format('U'),
            $path
        );

        $this->logger->debug(
            'generate signature GET',
            ['url' => $body, 'method' => 'GET']
        );

        return \hash_hmac('sha512', $body, $this->key, false);
    }
}
