<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Controller\Page;

use Contao\ArticleModel;
use Contao\CoreBundle\DependencyInjection\Attribute\AsPage;
use Contao\CoreBundle\Exception\PageNotFoundException;
use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor;
use Contao\Input;
use Contao\PageModel;
use Ferienpass\CmsBundle\Controller\AbstractController;
use Ferienpass\CmsBundle\Filter\OfferListFilter;
use Ferienpass\CmsBundle\Fragment\FragmentReference;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\OfferAttachment;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Ferienpass\CoreBundle\Routing\OfferReaderUrl;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Uid\Uuid;

#[AsPage('offer_list')]
class OfferListPage extends AbstractController
{
    public function __construct(private readonly OfferRepositoryInterface $offers, private readonly VirtualFilesystemInterface $attachmentsStorage, private readonly EditionRepository $editions, private readonly OfferListFilter $filter)
    {
    }

    public function __invoke(?string $auto_item, Request $request, ResponseContextAccessor $responseContextAccessor): Response
    {
        $this->initializeContaoFramework();

        //        $responseContext = $responseContextAccessor->getResponseContext();

        $pageModel = $request->attributes->get('pageModel');

        //        $edition = $offer->getEdition();
        //        if (null !== $edition && $edition->getActiveTasks('show_offers')->isEmpty()) {
        //            throw new PageNotFoundException();
        //        }

        if (null !== $auto_item) {
            Input::resetUnusedGet();

            return $this->handleOfferDetails($auto_item, $request, $pageModel);
        }

        return $this->handleOfferList($pageModel, $request);
    }

    private function handleOfferDetails(string $autoItem, Request $request, PageModel $pageModel): Response
    {
        if (!($offer = $this->resolveOffer($autoItem)) instanceof OfferInterface) {
            throw new PageNotFoundException();
        }

        if (!$request->attributes->getBoolean('preview') && !$offer->isPublished()) {
            throw new PageNotFoundException();
        }

        // Self-canonicalise the URL. Since aliases are no longer unique
        // (variants share a slug), the base58 UUID is the authoritative lookup
        // key and the slug a decorative tail — mirroring EditionLandingController's
        // /o/ URLs. Any non-canonical form (bare UUID, stale slug, or a legacy
        // plain-alias bookmark like /angebote/e-gitarre-schnupperkurs) gets a
        // 301 to {base58}-{slug}. The download endpoint (?file=…) is served in
        // place — its links are regenerated with the canonical auto_item anyway.
        $canonical = OfferReaderUrl::autoItem($offer);
        if ($autoItem !== $canonical && !$request->query->has('file') && !$request->attributes->getBoolean('preview')) {
            $params = ['to' => 'offer_list', 'auto_item' => $canonical];
            if ($offer->getEdition() instanceof Edition) {
                $params['edition'] = $offer->getEdition();
            }

            return $this->redirectToRoute('cms', $params, Response::HTTP_MOVED_PERMANENTLY);
        }

        //        $htmlHeadBag = $responseContext->get(HtmlHeadBag::class);
        //        $htmlHeadBag->setTitle($offer->getName());
        //        if ($date = $offer->getDates()->first()) {
        //            $htmlHeadBag->setTitle(sprintf('%s am %s', $htmlHeadBag->getTitle(), $date->getBegin()->format('d.m.Y')));
        //        }

        if ($request->query->has('file')) {
            return $this->handleDownload($request, $offer);
        }

        $pageBuilder = $this->createPageBuilder($pageModel);

        // The canonical home of every offer is the /o/{edition}/{base58}-{slug}
        // URL served by EditionLandingController. This legacy reader keeps
        // serving its own 200 but points search engines there.
        if ($offer->getEdition() instanceof Edition) {
            [$route, $params] = EditionLandingController::offerRouteAndParams($offer);
            $pageBuilder->setCanonicalUri($this->generateUrl($route, $params));
        }

        return $pageBuilder
            // Pass the UUID string (not the Offer entity) so Contao's
            // FragmentHandler doesn't silently downgrade the renderer to
            // 'forward' — see OfferDetailsController docblock for the full
            // explanation.
            ->addFragment('main', new FragmentReference('ferienpass.fragment.offer_details', ['offer' => $offer->getUuid()->toRfc4122()]))
            ->getResponse()
        ;
    }

    /**
     * Resolve the offer referenced by an `auto_item` segment via its
     * authoritative base58 UUID prefix. Falls back to {@see self::resolveLegacyAlias()}
     * for plain-alias URLs; the caller then 301s the offer to its canonical
     * {base58}-{slug} form.
     */
    private function resolveOffer(string $autoItem): ?OfferInterface
    {
        $base58 = false !== ($pos = strpos($autoItem, '-')) ? substr($autoItem, 0, $pos) : $autoItem;

        try {
            $uuid = Uuid::fromBase58($base58);
        } catch (\InvalidArgumentException) {
            $uuid = null;
        }

        if (null !== $uuid && ($offer = $this->offers->findByUuid($uuid)) instanceof OfferInterface) {
            return $offer;
        }

        return $this->resolveLegacyAlias($autoItem);
    }

    /**
     * Resolve a bare-alias `auto_item` (e.g. /angebote/e-gitarre-schnupperkurs).
     *
     * @deprecated Temporary BC shim. A malfunctioning version was deployed that
     *   emitted offer URLs as a plain alias, without the base58 UUID prefix.
     *   Aliases are not unique (variants share a slug), so this lookup is
     *   AMBIGUOUS — it returns whichever variant the DB yields first; the caller
     *   then 301s it to that variant's canonical {base58}-{slug} URL. This keeps
     *   the already-deployed/indexed/bookmarked plain-alias links alive. Remove
     *   in a future version once those links and HTTP caches have aged out — at
     *   which point a bare alias should simply 404.
     */
    private function resolveLegacyAlias(string $autoItem): ?OfferInterface
    {
        return $this->offers->findByAlias($autoItem);
    }

    private function handleOfferList(PageModel $pageModel, Request $request): Response
    {
        $pageBuilder = $this->createPageBuilder($pageModel);

        // Canonicalise to the /o/{edition} landing page (resolved the same
        // way the offer_list fragment scopes itself).
        $edition = $pageModel->edition ? $this->editions->find($pageModel->edition) : $this->editions->findOneToShow($pageModel);
        if ($edition instanceof Edition && $edition->getAlias()) {
            $pageBuilder->setCanonicalUri($this->generateUrl('cms_edition_landing', ['alias' => $edition->getAlias()]));
        }

        // A filtered list is an unbounded duplicate of this one page, and
        // JS-executing crawlers walk the whole permutation space (see
        // OfferListRobotsTxtListener, which stops the fetch for anything that
        // reads robots.txt). `follow`, so the offers linked from a filtered list
        // stay reachable — they are not in the sitemap.
        if ($this->filter->isFiltered($request)) {
            $pageBuilder->setMetaRobots('noindex,follow');
        }

        $articles = ArticleModel::findPublishedByPidAndColumn($pageModel->id, 'main');
        while (null !== $articles && $articles->next()) {
            $pageBuilder->addFragment('main', new FragmentReference('ferienpass.fragment.article', ['id' => $articles->id]));
        }

        $pageBuilder->addFragment('main', new FragmentReference('ferienpass.fragment.offer_list'));

        return $pageBuilder->getResponse();
    }

    private function handleDownload(Request $request, OfferInterface $offer): Response
    {
        $path = $request->query->get('file');

        if (!array_any($offer->getAttachments()->toArray(), static fn (OfferAttachment $a) => $path === $a->getAttachment()->getPath())) {
            throw new PageNotFoundException('Invalid download');
        }

        $file = $this->attachmentsStorage->get($path);
        $stream = $this->attachmentsStorage->readStream($path);

        $response = new StreamedResponse(
            static function () use ($stream) {
                $outputStream = fopen('php://output', 'w');

                stream_copy_to_stream($stream, $outputStream);
            }
        );

        $response->headers->set('Content-Transfer-Encoding', 'binary');
        $response->headers->set('Content-Type', $file->getMimeType());
        $response->headers->set('Content-Length', (string) fstat($stream)['size']);

        return $response;
    }
}
