<?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\Controller;

use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer;
use Chill\DocStoreBundle\Dav\Response\DavResponse;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;

/**
 * Provide endpoint for editing a document on the desktop using dav.
 *
 * This controller implements the minimal required methods to edit a document on a desktop software (i.e. LibreOffice)
 * and save the document online.
 *
 * To avoid to ask for a password, the endpoints are protected using a JWT access token, which is inside the
 * URL. This avoid the DAV Client (LibreOffice) to keep an access token in query parameter or in some header (which
 * they are not able to understand). The JWT Guard is adapted with a dedicated token extractor which is going to read
 * the segments (separation of "/"): the first segment must be the string "dav", and the second one must be the JWT.
 */
final readonly class WebdavController
{
    private PropfindRequestAnalyzer $requestAnalyzer;

    public function __construct(
        private \Twig\Environment $engine,
        private StoredObjectManagerInterface $storedObjectManager,
        private Security $security,
        private EntityManagerInterface $entityManager,
    ) {
        $this->requestAnalyzer = new PropfindRequestAnalyzer();
    }

    #[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['GET', 'HEAD'], name: 'chill_docstore_dav_directory_get')]
    public function getDirectory(StoredObject $storedObject, string $access_token): Response
    {
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
            throw new AccessDeniedHttpException();
        }

        return new DavResponse(
            $this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [
                'stored_object' => $storedObject,
                'access_token' => $access_token,
            ])
        );
    }

    #[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['OPTIONS'])]
    public function optionsDirectory(StoredObject $storedObject): Response
    {
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
            throw new AccessDeniedHttpException();
        }

        $response = (new DavResponse(''))
            ->setEtag($this->storedObjectManager->etag($storedObject))
        ;

        // $response->headers->add(['Allow' =>  'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT,PROPPATCH,COPY,MOVE,REPORT,PATCH,POST,TRACE']);
        $response->headers->add(['Allow' =>  'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']);

        return $response;
    }

    #[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['PROPFIND'])]
    public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response
    {
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
            throw new AccessDeniedHttpException();
        }

        $depth = $request->headers->get('depth');

        if ('0' !== $depth && '1' !== $depth) {
            throw new BadRequestHttpException('only 1 and 0 are accepted for Depth header');
        }

        [$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject);

        $response = new DavResponse(
            $this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [
                'stored_object' => $storedObject,
                'properties' => $properties,
                'last_modified' => $lastModified,
                'etag' => $etag,
                'content_length' => $length,
                'depth' => (int) $depth,
                'access_token' => $access_token,
            ]),
            207
        );

        $response->headers->add([
            'Content-Type' => 'text/xml',
        ]);

        return $response;
    }

    #[Route(path: '/dav/{access_token}/get/{uuid}/d', name: 'chill_docstore_dav_document_get', methods: ['GET'])]
    public function getDocument(StoredObject $storedObject): Response
    {
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
            throw new AccessDeniedHttpException();
        }

        return (new DavResponse($this->storedObjectManager->read($storedObject)))
            ->setEtag($this->storedObjectManager->etag($storedObject));
    }

    #[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['HEAD'])]
    public function headDocument(StoredObject $storedObject): Response
    {
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
            throw new AccessDeniedHttpException();
        }

        $response = new DavResponse('');

        $response->headers->add(
            [
                'Content-Length' => $this->storedObjectManager->getContentLength($storedObject),
                'Content-Type' => $storedObject->getType(),
                'Etag' => $this->storedObjectManager->etag($storedObject),
            ]
        );

        return $response;
    }

    #[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['OPTIONS'])]
    public function optionsDocument(StoredObject $storedObject): Response
    {
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
            throw new AccessDeniedHttpException();
        }

        $response = (new DavResponse(''))
            ->setEtag($this->storedObjectManager->etag($storedObject))
        ;

        $response->headers->add(['Allow' =>  'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']);

        return $response;
    }

    #[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['PROPFIND'])]
    public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response
    {
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
            throw new AccessDeniedHttpException();
        }

        [$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject);

        $response = new DavResponse(
            $this->engine->render(
                '@ChillDocStore/Webdav/doc_props.xml.twig',
                [
                    'stored_object' => $storedObject,
                    'properties' => $properties,
                    'etag' => $etag,
                    'last_modified' => $lastModified,
                    'content_length' => $length,
                    'access_token' => $access_token,
                ]
            ),
            207
        );

        $response
            ->headers->add([
                'Content-Type' => 'text/xml',
            ]);

        return $response;
    }

    #[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['PUT'])]
    public function putDocument(StoredObject $storedObject, Request $request): Response
    {
        if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
            throw new AccessDeniedHttpException();
        }

        $this->storedObjectManager->write($storedObject, $request->getContent());

        $this->entityManager->flush();

        return new DavResponse('', Response::HTTP_NO_CONTENT);
    }

    /**
     * @return array{0: array, 1: \DateTimeInterface, 2: string, 3: int} properties, lastModified, etag, length
     */
    private function parseDavRequest(string $content, StoredObject $storedObject): array
    {
        $xml = new \DOMDocument();
        $xml->loadXML($content);

        $properties = $this->requestAnalyzer->getRequestedProperties($xml);
        $requested = array_keys(array_filter($properties, fn ($item) => true === $item));

        if (
            in_array('lastModified', $requested, true)
            || in_array('etag', $requested, true)
        ) {
            $lastModified = $this->storedObjectManager->getLastModified($storedObject);
            $etag = $this->storedObjectManager->etag($storedObject);
        }
        if (in_array('contentLength', $requested, true)) {
            $length = $this->storedObjectManager->getContentLength($storedObject);
        }

        return [
            $properties,
            $lastModified ?? null,
            $etag ?? null,
            $length ?? null,
        ];
    }
}
