<?php declare(strict_types=1);
/*
 * Copyright (C) 2015-2017 IRSTEA
 * All rights reserved.
 */

namespace Irstea\FileUploadBundle\Controller;

use Irstea\FileUploadBundle\Entity\UploadedFile;
use Irstea\FileUploadBundle\Exception\RejectedFileException;
use Irstea\FileUploadBundle\Http\UploadedFileResponse;
use Irstea\FileUploadBundle\Model\FileManagerInterface;
use Irstea\FileUploadBundle\Model\UploadedFileInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Templating\EngineInterface;

/**
 * @Route("/api/files", service="irstea_file_upload.upload_controller")
 */
class UploadController extends Controller
{
    public const CSRF_INTENTION = 'uploaded_file';

    /**
     * @var FileManagerInterface
     */
    protected $fileManager;

    /**
     * @var UrlGeneratorInterface
     */
    protected $urlGenerator;

    /**
     * @var CsrfTokenManagerInterface
     */
    protected $csrfTokenManager;

    /**
     * @var TokenStorageInterface
     */
    protected $tokenStorage;

    /**
     * @var EngineInterface
     */
    protected $templating;

    /**
     * UploadController constructor.
     *
     * @param FileManagerInterface      $fileManager
     * @param UrlGeneratorInterface     $urlGenerator
     * @param CsrfTokenManagerInterface $csrfTokenManager
     * @param TokenStorageInterface     $tokenStorage
     * @param EngineInterface           $templating
     */
    public function __construct(
        FileManagerInterface $fileManager,
        UrlGeneratorInterface $urlGenerator,
        CsrfTokenManagerInterface $csrfTokenManager,
        TokenStorageInterface $tokenStorage,
        EngineInterface $templating
    ) {
        $this->fileManager = $fileManager;
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->tokenStorage = $tokenStorage;
        $this->templating = $templating;
    }

    /**
     * @Route("", name="file_upload_create")
     * @Method("POST")
     * @param Request $request
     *
     * @return JsonResponse
     */
    public function createAction(Request $request)
    {
        try {
            $data = $request->request->get('file');

            $token = $this->tokenStorage->getToken();

            $file = $this->fileManager->create(
                $data['name'],
                $data['size'],
                $data['type'],
                $data['lastModified'] ?? null,
                null !== $token ? $token->getUsername() : null,
                $request->getClientIp()
            );

            $parameters = ['id' => $file->getId()];

            $deleteUrl = $this->urlGenerator->generate('file_upload_delete', $parameters);

            return $this->createResponse(
                Response::HTTP_CREATED,
                'New file created',
                array_merge(
                    $file->toArray(),
                    [
                        'put_url'     => $this->urlGenerator->generate('file_upload_put_content', $parameters),
                        'delete_type' => 'DELETE',
                        'delete_url'  => $deleteUrl,
                    ]
                ),
                // On a pas de get pour l'instant, le DELETE et ce qui y ressemble le plus
                ['Location' => $deleteUrl]
            );
        } catch (\Exception $ex) {
            return $this->createExceptionResponse($ex);
        }
    }

    /**
     * @Route("/{id}/content", name="file_upload_put_content")
     * @Method("PUT")
     * @param Request      $request
     * @param UploadedFile $file
     */
    public function putContentAction(Request $request, UploadedFile $file)
    {
        try {
            $this->validateCsrfToken($request);

            list($offset, $maxlen, $complete) = $this->handleRangeHeader($request);

            // Demande un filehandle plutôt que charger le contenu en mémoire
            $input = $request->getContent(true);
            $file->copyFrom($input, $maxlen, $offset);
            fclose($input);

            if ($complete) {
                return $this->completeUpload($file);
            }

            return $this->createResponse(Response::HTTP_OK, 'Chunk received');
        } catch (\Exception $ex) {
            return $this->createExceptionResponse($ex);
        }
    }

    /**
     * @param Request $request
     *
     * @return array
     */
    protected function handleRangeHeader(Request $request)
    {
        if (null === $range = $request->headers->get('Content-Range', null)) {
            return [0, PHP_INT_MAX, true];
        }

        $matches = [];
        if (!preg_match('@^bytes (\d+)-(\d+)/(\d+)$@', $range, $matches)) {
            throw new BadRequestHttpException('Invalid Content-Range');
        }

        $start = intval($matches[1]);
        $end = intval($matches[2]);
        $total = intval($matches[3]);

        if ($start < 0 || $start >= $end || $end >= $total) {
            throw new HttpException(Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
        }

        return [$start, 1 + ($end - $start), $end === ($total - 1)];
    }

    /**
     * @param UploadedFileInterface $file
     *
     * @return Response
     */
    protected function completeUpload(UploadedFileInterface $file)
    {
        try {
            $this->fileManager->completed($file);
        } catch (RejectedFileException $ex) {
            return $this->createResponse(Response::HTTP_FORBIDDEN, 'File rejected: ' . $ex->getMessage());
        }

        $parameters = ['id' => $file->getId()];
        $fileData = array_merge(
            $file->toArray(),
            [
                'url'         => $this->urlGenerator->generate('file_upload_get_content', $parameters),
                'delete_type' => 'DELETE',
                'delete_url'  => $this->urlGenerator->generate('file_upload_delete', $parameters),
                'repr'        => $this->templating->render(
                    'IrsteaFileUploadBundle:Extension:uploaded_file.html.twig',
                    ['file' => $file->toArray()]
                ),
            ]
        );

        return $this->createResponse(Response::HTTP_OK, 'File uploaded.', ['files' => [$fileData]]);
    }

    /**
     * @Route("/{id}/content", name="file_upload_get_content")
     * @Method("GET")
     * @param Request      $request
     * @param UploadedFile $file
     */
    public function getContentAction(Request $request, UploadedFile $file)
    {
        $this->validateCsrfToken($request);

        if (!$file->isValid()) {
            throw new NotFoundHttpException();
        }

        $response = UploadedFileResponse::create($file, 200, [], false, ResponseHeaderBag::DISPOSITION_ATTACHMENT);
        $response->isNotModified($request);

        return $response;
    }

    /**
     * @Route("/{id}", name="file_upload_delete")
     * @Method("DELETE")
     * @param Request      $request
     * @param UploadedFile $file
     */
    public function deleteAction(Request $request, UploadedFile $file)
    {
        try {
            $this->validateCsrfToken($request);

            $this->fileManager->delete($file);

            return $this->createResponse();
        } catch (\Exception $ex) {
            return $this->createExceptionResponse($ex);
        }
    }

    /**
     * @param Request $request
     *
     * @throws HttpException
     */
    protected function validateCsrfToken(Request $request)
    {
        $token = $this->csrfTokenManager->getToken($request->query->get('token', null));
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid CSRF token');
        }
    }

    /**
     * @param int    $status
     * @param string $message
     * @param array  $data
     * @param array  $headers
     *
     * @return JsonResponse
     */
    protected function createResponse($status = Response::HTTP_OK, $message = 'OK', array $data = [], array $headers = [])
    {
        $data['status'] = $status;
        $data['message'] = $message;
        $response = new JsonResponse($data, $status, $headers);
        $response->setStatusCode($status, $message);

        return $response;
    }

    /**
     * @param \Exception $ex
     *
     * @return JsonResponse
     */
    protected function createExceptionResponse(\Exception $ex)
    {
        return $this->createResponse(
            $ex instanceof HttpException ? $ex->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR,
            preg_replace("/[\n\r]+/", ' ', $ex->getMessage()),
            [
                'exception' => [
                    'class' => get_class($ex),
                    'file'  => $ex->getFile(),
                    'line'  => $ex->getLine(),
                    'code'  => $ex->getCode(),
                    'trace' => $ex->getTrace(),
                ],
            ]
        );
    }
}