<?php

declare(strict_types=1);

/*
 * This file is part of contao-weinanlieferung-bundle.
 *
 * (c) vonRotenberg
 *
 * @license commercial
 */

namespace vonRotenberg\WeinanlieferungBundle\Controller\Frontend\Ajax;

use Contao\Controller;
use Contao\CoreBundle\Controller\AbstractController;
use Contao\CoreBundle\Exception\PageNotFoundException;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Security\Authentication\Token\TokenChecker;
use Contao\Environment;
use Contao\File;
use Contao\FilesModel;
use Contao\FormFileUpload;
use Contao\Frontend;
use Contao\FrontendUser;
use Contao\Input;
use Contao\MemberModel;
use Contao\StringUtil;
use Contao\System;
use Doctrine\DBAL\Connection;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use vonRotenberg\WeinanlieferungBundle\Event\CheckInCompletedEvent;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLageModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungRebsorteModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungUnitModel;

/**
 * @Route("/_ajax/vr_wa/v1/slot", name="vr_wa_slot_ajax", defaults={"_scope" = "frontend", "_token_check" = false})
 */
class SlotAjaxController extends AbstractController
{
    private $tokenChecker;
    private $translator;
    private $framework;
    private $eventDispatcher;

    public function __construct(
        ContaoFramework $framework,
        TokenChecker $tokenChecker,
        TranslatorInterface $translator,
        EventDispatcherInterface $eventDispatcher
    )
    {
        $this->framework = $framework;
        $this->tokenChecker = $tokenChecker;
        $this->translator = $translator;
        $this->eventDispatcher = $eventDispatcher;
    }

    public function __invoke(Request $request)
    {
        System::loadLanguageFile('default');

        if (!$this->tokenChecker->hasFrontendUser())
        {
            return $this->renderUnauthorized();
        }

        if (empty($_REQUEST['do']))
        {
            return new Response('Required parameter missing',412);
        }

        $blnModal = true;

        if (!empty($_REQUEST['modal']))
        {
            $blnModal = !(strtolower($_REQUEST['modal']) == 'false');
        }

        switch ($_REQUEST['do'])
        {
            case 'details':
                return $this->renderDetails($blnModal);

            case 'annotation':
                return $this->renderAnnotation($blnModal);

            case 'booking':
                return $this->renderBooking($blnModal);

            case 'reservate':
                return $this->reservate();

            case 'updateReservation':
                return $this->updateReservation();

            case 'delete':
                return $this->deleteReservation();

            case 'checkin':
                return $this->renderCheckin($blnModal);

            case 'updateCheckin':
                return $this->updateCheckin();

            case 'getAvailableNumbers':
                return $this->getAvailableNumbers();

            case 'validateNumber':
                return $this->validateNumber();
        }

        return new Response('',500);
    }

    protected function renderDetails(bool $blnModal=true,string $error=null)
    {
        $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser');

        if (empty($_REQUEST['id']))
        {
            return new Response('Required parameter missing',412);
        }

        if (($Slot = WeinanlieferungSlotsModel::findPublishedById($_REQUEST['id'])) === null)
        {
            return new Response('Could not load slot data',500);
        }

        // Get slot reservations from user
        $arrReservations = [];

        if (($Reservations = WeinanlieferungReservationModel::findBy(["uid = ?","pid = ?"],[FrontendUser::getInstance()->id,$Slot->id])) !== null)
        {
            foreach ($Reservations as $reservation)
            {
                $arrSortenBooked = [];
                $SortenLeseart = explode(';',$reservation->sorten);
                foreach($SortenLeseart as $sorteLeseart)
                {
                    list($sorte,$leseart) = explode(',',$sorteLeseart);
                    $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte);
                    $objLeseart = WeinanlieferungLeseartModel::findByPk($leseart);
                    $arrSortenBooked[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null  ? $objSorte->title : '') . ' ' . ($objLeseart !== null  ? $objLeseart->title : '');
                }
                /*if (($Sorten = $reservation->getRelated('sorten')) !== null)
                {
                    $arrSortenBooked = $Sorten->fetchEach('title');
                }*/
                // Compute unit display fields for this reservation
                $unitTitle = $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter';
                $unitAmountDisplay = (int) $reservation->behaelter;
                if ((int)$reservation->unit_id > 0) {
                    $unitModel = WeinanlieferungUnitModel::findByPk((int)$reservation->unit_id);
                    if (null !== $unitModel) {
                        $unitTitle = (string)$unitModel->title;
                    }
                    $unitAmountDisplay = (int) ($reservation->unit_amount ?: 0);
                    if ($unitAmountDisplay <= 0) {
                        $mult = (int) ($unitModel ? $unitModel->multiplier : 0);
                        $unitAmountDisplay = $mult > 0 ? max(1, (int) ($reservation->behaelter / $mult)) : (int) $reservation->behaelter;
                    }
                }

                $arrReservations[] = array_merge($reservation->row(),[
                    'sorten' => $arrSortenBooked,
                    'unit_title' => $unitTitle,
                    'unit_amount_display' => $unitAmountDisplay,
                ]);
            }
        }

        // Build data
        $arrSorten = [];
        $Sorten = StringUtil::deserialize($Slot->sorten,true);
        foreach($Sorten as $sorte)
        {
            $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte['sorte']);
            $objLeseart = WeinanlieferungLeseartModel::findByPk($sorte['leseart']);
            $arrSorten[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null  ? $objSorte->title : '') . ' ' . ($objLeseart !== null  ? $objLeseart->title : '');
        }
        /*if (($Sorten = $Slot->getRelated('sorte')) !== null)
        {
            $arrSorten = array_combine($Sorten->fetchEach('id'),$Sorten->fetchEach('title'));
        }*/

        $arrErnteart = [];
        if ($Slot->ernteart !== null)
        {
            foreach (explode(',', $Slot->ernteart) as $ernteart)
            {
                $arrErnteart[$ernteart] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart;
            }
        }

        $arrLage = [];
        if (($Lage = $Slot->getRelated('lage')) !== null)
        {
            $arrLage = array_combine($Lage->fetchEach('id'),$Lage->fetchEach('title'));
        }

        $intReservedBehaelter = $Slot->getReservedBehaelter();
        $intAvailableBehaelter = max(0,$Slot->getAvailableBehaelter());

        $arrData = [
            'modal' => $blnModal,
            'id'       => $Slot->id,
            'slot'     => array_merge($Slot->row(),[
                'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''),
                'sorte' => $arrSorten,
                'behaelterAvailable' => $intAvailableBehaelter,
                'behaelterBooked' => $Slot->getReservedBehaelter(),
            ]),
            'standort' => $Slot->getRelated('pid'),
            'lage' => $arrLage,
            'ernteart' => $arrErnteart,
            'buchen' => [
                'buchbar' => (boolean) ($Slot->behaelter*3 > $intReservedBehaelter),
                'behaelter' => range(min($intAvailableBehaelter,1),$Slot->behaelter*3-$intReservedBehaelter),
                'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter),
                'sorten' => $arrSorten,
                'lage' => $arrLage,
                'ernteart' => $arrErnteart,
            ],
            'reservations' => $arrReservations
        ];

        if (!empty($error))
        {
            $arrData['toast'] = $error;
        }

        return $this->render('@Contao/modal_slot_details.html.twig',$arrData);
    }

    protected function renderAnnotation(bool $blnModal=true,string $error=null)
    {
        $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser');

        if (empty($_REQUEST['id']))
        {
            return new Response('Required parameter missing',412);
        }

        if (($Slot = WeinanlieferungSlotsModel::findPublishedById($_REQUEST['id'])) === null)
        {
            return new Response('Could not load slot data',500);
        }

        // Build data
        $arrSorten = [];
        $Sorten = StringUtil::deserialize($Slot->sorten,true);
        foreach($Sorten as $sorte)
        {
            $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte['sorte']);
            $objLeseart = WeinanlieferungLeseartModel::findByPk($sorte['leseart']);
            $arrSorten[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null  ? $objSorte->title : '') . ' ' . ($objLeseart !== null  ? $objLeseart->title : '');
        }

        $arrErnteart = [];
        if ($Slot->ernteart !== null)
        {
            foreach (explode(',', $Slot->ernteart) as $ernteart)
            {
                $arrErnteart[] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart;
            }
        }

        $arrLage = [];
        if (($Lage = $Slot->getRelated('lage')) !== null)
        {
            $arrLage = $Lage->fetchEach('title');
        }

        $intAvailableBehaelter = max(0,$Slot->getAvailableBehaelter());

        $arrData = [
            'modal' => $blnModal,
            'id'       => $Slot->id,
            'slot'     => array_merge($Slot->row(),[
                'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''),
                'sorte' => $arrSorten,
                'behaelterAvailable' => $intAvailableBehaelter
            ]),
            'standort' => $Slot->getRelated('pid'),
            'lage' => $arrLage,
            'ernteart' => $arrErnteart,
            'buchen' => [
                'buchbar' => (boolean) $intAvailableBehaelter,
                'behaelter' => range(min($intAvailableBehaelter,1),$intAvailableBehaelter),
                'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter),
                'sorten' => $arrSorten
            ],
        ];

        return $this->render('@Contao/modal_slot_annotation.html.twig',$arrData);
    }

    protected function renderBooking(bool $blnModal=true,string $error=null)
    {
        $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser');

        $arrData = [];

        if (empty($_REQUEST['id']))
        {
            return new Response('Required parameter missing',412);
        }

        /** @var WeinanlieferungSlotsModel $Slot */
        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null)
        {
            return new Response('Could not load booking data',500);
        }

        if ($Booking->approved === '0')
        {
            return $this->render('@Contao/modal_message.html.twig',['type'=>'danger','content'=>'Diese Buchungsanfrage wurde abgelehnt und kann nicht mehr geändert werden.']);
        }

        $objFile = FilesModel::findByUuid($Booking->upload);

        if (!empty($_REQUEST['deleteFile']) && $_REQUEST['deleteFile'] && $objFile !== null)
        {
            $File = new File($objFile->path);
            if ($File->delete())
            {
                $objFile->delete();
                $objFile = null;
            }
        }

        if (!empty($Booking->upload) && $objFile !== null)
        {
            $File = new File($objFile->path);
            $strHref = Environment::get('request');

            // Remove an existing file parameter (see #5683)
            if (isset($_GET['file']))
            {
                $strHref = preg_replace('/(&(amp;)?|\?)file=[^&]+/', '', $strHref);
            }

            $strHref .= (strpos($strHref, '?') !== false ? '&amp;' : '?') . 'file=' . System::urlEncode($File->value);

            $arrData['file'] = [
                'link' => $strHref,
                'filename' => $File->filename,
                'extension' => $File->extension,
                'name' => $File->name,
                'path' => $File->dirname
            ];
        }

        // Send the file to the browser (see #4632 and #8375)
        if ($objFile !== null && ($file = Input::get('file', true)))
        {
            if ($file == $objFile->path)
            {
                Controller::sendFileToBrowser($file);
            }
        }

        $arrSortenAvailable = [];
        $Sorten = StringUtil::deserialize($Slot->sorten,true);
        foreach($Sorten as $sorte)
        {
            $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte['sorte']);
            $objLeseart = WeinanlieferungLeseartModel::findByPk($sorte['leseart']);
            $arrSortenAvailable[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null  ? $objSorte->title : '') . ' ' . ($objLeseart !== null  ? $objLeseart->title : '');
        }
        $arrSortenBooked = [];
        $SortenLeseart = explode(';',$Booking->sorten);
        foreach($SortenLeseart as $sorteLeseart)
        {
            list($sorte,$leseart) = explode(',',$sorteLeseart);
            $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte);
            $objLeseart = WeinanlieferungLeseartModel::findByPk($leseart);
            $arrSortenBooked[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null  ? $objSorte->title : '') . ' ' . ($objLeseart !== null  ? $objLeseart->title : '');
        }

        $arrErnteartAvailable = [];
        if ($Slot->ernteart !== null)
        {
            foreach (explode(',', $Slot->ernteart) as $ernteart)
            {
                $arrErnteartAvailable[$ernteart] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart;
            }
        }
        $arrErnteartBooked = [];
        if ($Booking->ernteart !== null)
        {
            $arrErnteartBooked = explode(',', $Booking->ernteart);
        }

        $arrLagenAvailable = [];
        if (($Lagen = $Slot->getRelated('lage')) !== null)
        {
            foreach ($Lagen as $lage)
            {
                $arrLagenAvailable[$lage->id] = $lage->title;
            }
        }
        $arrLagenBooked = [];
        if ($Booking->lage !== null)
        {
            $arrLagenBooked = explode(',', $Booking->lage);
        }

        $intReservedBehaelter = $Slot->getReservedBehaelter();
        // While editing, available base units should include the current booking's base units,
        // because the edit will override the former amount.
        $intAvailableBehaelter = max(0, $Slot->getAvailableBehaelter() + (int) $Booking->behaelter);
        $intOcTreshold = $intAvailableBehaelter - $intReservedBehaelter + $Slot->behaelter;

        $arrData = array_merge($arrData,[
            'modal' => $blnModal,
            'id'       => $Booking->id,
            'slot'     => array_merge($Slot->row(),[
                'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''),
                'sorte' => $arrSortenAvailable,
                'behaelterAvailable' => $intAvailableBehaelter,
                'behaelterOcThreshold' => $intOcTreshold,
                'behaelterBooked' => $Slot->getReservedBehaelter(),
            ]),
            'buchung' => array_merge($Booking->row(),[
                'sorten' => $arrSortenBooked,
                'ernteart' => $arrErnteartBooked,
                'lage' => $arrLagenBooked,
                // display fields
                'unit_title' => (function() use ($Booking) {
                    if ((int)$Booking->unit_id > 0) {
                        $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id);
                        return $m ? (string)$m->title : ($GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter');
                    }
                    return $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter';
                })(),
                'unit_amount_display' => (function() use ($Booking) {
                    if ((int)$Booking->unit_id > 0) {
                        $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id);
                        $amount = (int) ($Booking->unit_amount ?: 0);
                        if ($amount > 0) return $amount;
                        $mult = (int) ($m ? $m->multiplier : 0);
                        return $mult > 0 ? max(1, (int) ($Booking->behaelter / $mult)) : (int) $Booking->behaelter;
                    }
                    return (int) $Booking->behaelter;
                })(),
            ]),
            'standort' => $Slot->getRelated('pid'),
            'buchen' => [
                'buchbar' => (boolean) $intAvailableBehaelter,
                'behaelter' => range(min($intAvailableBehaelter,1),$Slot->behaelter*3-$Slot->getReservedBehaelter()+$Booking->behaelter),
                'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter),
                'sorten' => $arrSortenAvailable,
                'ernteart' => $arrErnteartAvailable,
                'lage' => $arrLagenAvailable,
            ]
        ]);

        if (!empty($error))
        {
            $arrData['toast'] = $error;
        }

        return $this->render('@Contao/modal_booking_details.html.twig',$arrData);
    }

    protected function reservate()
    {
        Controller::loadDataContainer('tl_vr_wa_reservation');
        $arrData = [];

        if (($rootPage = Frontend::getRootPageFromUrl()) !== null && !empty($rootPage->vr_wa_uploadFolderSRC))
        {
            $File = new FormFileUpload(\Contao\Widget::getAttributesFromDca($GLOBALS['TL_DCA']['tl_vr_wa_reservation']['fields']['upload'], 'upload'));
            $File->storeFile = true;
            $File->doNotOverwrite = true;
            $File->uploadFolder = $rootPage->vr_wa_uploadFolderSRC;

            $File->validate();

            if ($File->hasErrors())
            {
                return $this->renderDetails(false,'<div class="toast toast--danger mx-0">' . $File->getErrorAsHTML() . '</div>');
            }

            if (!empty($_SESSION['FILES'][$File->name]))
            {
                $arrData['filename'] = $_SESSION['FILES'][$File->name]['name'] ?? '';
                $arrData['upload'] = $_SESSION['FILES'][$File->name]['uuid'] ? StringUtil::uuidToBin($_SESSION['FILES'][$File->name]['uuid']) : null;
            }
        }

        if (empty($_REQUEST['id']))
        {
            return new Response('Missing parameter',412);
        }

        // Form validation and unit capacity
        if (($Slot = WeinanlieferungSlotsModel::findByPk($_REQUEST['id'])) !== null)
        {
            $unitId = (int) Input::post('unit_id');
            $unitAmount = (int) Input::post('unit_amount');
            $multiplier = 1;
            if ($unitId > 0) {
                if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) {
                    $multiplier = max(1, (int) $u->multiplier);
                }
            }
            $baseUnitsRequested = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter');
            if ($baseUnitsRequested > $Slot->behaelter*3 - $Slot->getReservedBehaelter())
            {
                return $this->renderDetails(false,sprintf('<div class="toast toast--danger mx-0">Fehler: Es sind mittlerweile nur noch %s Behälter verfügbar.</div>',$Slot->getAvailableBehaelter()));
            }
        }
        $arrError = [];
        foreach (['sorten','ernteart','lage'] as $field)
        {
            if (empty(Input::post($field)))
            {
                $arrError = [$field];
            }
        }
        // Require either (unit selection (allowing 0) with unit_amount, or behaelter fallback)
        $postedUnitId = Input::post('unit_id');
        $hasUnitId = ($postedUnitId !== null && $postedUnitId !== '' && $postedUnitId !== false);
        if (!$hasUnitId || empty(Input::post('unit_amount')))
        {
            if (empty(Input::post('behaelter')))
            {
                $arrError[] = 'behaelter';
            }
        }

        if (count($arrError))
        {
            return $this->renderDetails(false,'<div class="toast toast--danger mx-0">Bitte geben Sie alle Pflichtangaben (mit * markierte Felder) an</div>');
        }

        $arrSorten = [];
        if (!is_array(Input::post('sorten')))
        {
            $arrSorten[] = Input::post('sorten');
        } else {
            $arrSorten = implode(';', Input::post('sorten'));
        }

        $arrErnteart = [];
        if (!is_array(Input::post('ernteart')))
        {
            $arrErnteart[] = Input::post('ernteart');
        } else {
            $arrErnteart = implode(',', Input::post('ernteart'));
        }

        $arrLage = [];
        if (!is_array(Input::post('lage')))
        {
            $arrLage[] = Input::post('lage');
        } else {
            $arrLage = implode(',', Input::post('lage'));
        }

        $Reservation = new WeinanlieferungReservationModel();

        $time = time();

        if (Input::post('behaelter') > $Slot->behaelter - $Slot->getReservedBehaelter())
        {
            $arrData['approved'] = '';
            $arrData['approved_on'] = 0;
        } else {
            $arrData['approved'] = '1';
            $arrData['approved_on'] = $time;
        }

        // Determine fields to store
        $unitId = (int) Input::post('unit_id');
        $unitAmount = (int) Input::post('unit_amount');
        $multiplier = 1;
        if ($unitId > 0) {
            if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) {
                $multiplier = max(1, (int) $u->multiplier);
            }
        }
        $baseUnits = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter');

        $arrData = array_merge($arrData,[
            'pid' => $_REQUEST['id'],
            'tstamp' => $time,
            'uid' => FrontendUser::getInstance()->id,
            'behaelter' => $baseUnits,
            'unit_id' => $unitId,
            'unit_amount' => $unitAmount,
            'sorten' => $arrSorten,
            'ernteart' => $arrErnteart,
            'lage' => $arrLage
        ]);
        $Reservation->setRow($arrData);

        $Reservation->save();

        if (empty($Reservation->approved))
        {
            return new Response('<div class="toast toast--warning mx-0"><p>Wir haben Ihre Anfrage erhalten. Bitte warten Sie auf eine Freigabe durch uns.</p></div>',200,['HX-Trigger'=> 'updateWaBooking']);
        }

        return new Response('<div class="toast toast--success mx-0"><p>Reservierung erfolgreich</p></div>',200,['HX-Trigger'=> 'updateWaList']);
    }

    protected function updateReservation()
    {
        Controller::loadDataContainer('tl_vr_wa_reservation');

        if (empty($_REQUEST['id']))
        {
            return new Response('Missing parameter',412);
        }

        if (($Reservation = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null)
        {
            return new Response('Could not load booking data',500);
        }

        if (FrontendUser::getInstance()->id != $Reservation->uid)
        {
            return new Response('Member not authorized tu update this reservation',500);
        }

        if (($rootPage = Frontend::getRootPageFromUrl()) !== null && !empty($rootPage->vr_wa_uploadFolderSRC))
        {
            $File = new FormFileUpload(\Contao\Widget::getAttributesFromDca($GLOBALS['TL_DCA']['tl_vr_wa_reservation']['fields']['upload'], 'upload'));
            $File->storeFile = true;
            $File->doNotOverwrite = true;
            $File->uploadFolder = $rootPage->vr_wa_uploadFolderSRC;

            $File->validate();

            if ($File->hasErrors())
            {
                return $this->renderBooking(false,'<div class="toast toast--danger mx-0">' . $File->getErrorAsHTML() . '</div>');
            }

            if (!empty($_SESSION['FILES'][$File->name]))
            {
                $Reservation->filename = $_SESSION['FILES'][$File->name]['name'] ?? '';
                $Reservation->upload = $_SESSION['FILES'][$File->name]['uuid'] ? StringUtil::uuidToBin($_SESSION['FILES'][$File->name]['uuid']) : null;
            }
        }

        // Form validation
        /** @var WeinanlieferungSlotsModel $Slot */
        if (($Slot = $Reservation->getRelated('pid')) !== null)
        {
            $unitId = (int) Input::post('unit_id');
            $unitAmount = (int) Input::post('unit_amount');
            $multiplier = 1;
            if ($unitId > 0) {
                if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) {
                    $multiplier = max(1, (int) $u->multiplier);
                }
            }
            $baseUnitsRequested = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter');
            if ($baseUnitsRequested > $Slot->behaelter*3 - $Slot->getReservedBehaelter() + $Reservation->behaelter)
            {
                return $this->renderBooking(false,sprintf('<div class="toast toast--danger mx-0">Fehler: Es sind mittlerweile nur noch %s Behälter verfügbar.</div>',$Slot->getAvailableBehaelter()+$Reservation->behaelter));
            }
        }
        $arrError = [];
        foreach (['sorten','ernteart','lage'] as $field)
        {
            if (empty(Input::post($field)))
            {
                $arrError = [$field];
            }
        }
        $postedUnitId = Input::post('unit_id');
        $hasUnitId = ($postedUnitId !== null && $postedUnitId !== '' && $postedUnitId !== false);
        if (!$hasUnitId || empty(Input::post('unit_amount')))
        {
            if (empty(Input::post('behaelter')))
            {
                $arrError[] = 'behaelter';
            }
        }

        if (count($arrError))
        {
            return $this->renderBooking(false,'<div class="toast toast--danger mx-0">Bitte geben Sie alle Pflichtangaben (mit * markierte Felder) an</div>');
        }

        $arrSorten = [];
        if (!is_array(Input::post('sorten')))
        {
            $arrSorten[] = Input::post('sorten');
        } else {
            $arrSorten = implode(';', Input::post('sorten'));
        }

        $arrErnteart = [];
        if (!is_array(Input::post('ernteart')))
        {
            $arrErnteart[] = Input::post('ernteart');
        } else {
            $arrErnteart = implode(',', Input::post('ernteart'));
        }

        $arrLage = [];
        if (!is_array(Input::post('lage')))
        {
            $arrLage[] = Input::post('lage');
        } else {
            $arrLage = implode(',', Input::post('lage'));
        }

        $time = time();

        if (Input::post('behaelter') > $Slot->behaelter - $Slot->getReservedBehaelter() + $Reservation->behaelter)
        {
            $Reservation->approved = '';
            $Reservation->approved_on = 0;
        } else {
            $Reservation->approved = '1';
            $Reservation->approved_on = $time;
        }

        $Reservation->tstamp = $time;
        // recompute base units
        $unitId = (int) Input::post('unit_id');
        $unitAmount = (int) Input::post('unit_amount');
        $multiplier = 1;
        if ($unitId > 0) {
            if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) {
                $multiplier = max(1, (int) $u->multiplier);
            }
        }
        $Reservation->behaelter = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter');
        $Reservation->unit_id = $unitId;
        $Reservation->unit_amount = $unitAmount;
        $Reservation->sorten = $arrSorten;
        $Reservation->ernteart = $arrErnteart;
        $Reservation->lage = $arrLage;

        $Reservation->save();

        if (empty($Reservation->approved))
        {
            return new Response('<div class="toast toast--warning mx-0"><p>Wir haben Ihre Anfrage erhalten. Bitte warten Sie auf eine Freigabe durch uns.</p></div>',200,['HX-Trigger'=> 'updateWaBooking']);
        }

        return new Response('<div class="toast toast--success mx-0"><p>Reservierung erfolgreich geändert</p></div>',200,['HX-Trigger'=> 'updateWaBooking']);
    }

    protected function deleteReservation()
    {
        if (empty($_REQUEST['id']))
        {
            return new Response('Missing parameter',412);
        }

        /** @var Connection $db */
        $db = Controller::getContainer()->get('database_connection');

        $arrCriteria = [
            'uid' => FrontendUser::getInstance()->id,
            'id' => $_REQUEST['id']
        ];

        if ($db->delete('tl_vr_wa_reservation',$arrCriteria))
        {
            return new Response(null,203,['HX-Trigger'=> 'updateWaBooking']);
        }

        return new Response('Could not delete',500);
    }

    protected function renderUnauthorized()
    {
        return $this->render('@Contao/modal_unauthorized.html.twig');
    }

    protected function renderCheckin(bool $blnModal=true, string $error=null, array $formData=null)
    {
        $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser');

        $arrData = [];

        if (empty($_REQUEST['id']))
        {
            return new Response('Required parameter missing', 412);
        }

        /** @var WeinanlieferungReservationModel $Booking */
        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null)
        {
            return new Response('Could not load booking data', 500);
        }

        if ($Booking->approved === '0')
        {
            return $this->render('@Contao/modal_message.html.twig', ['type'=>'danger', 'content'=>'Diese Buchungsanfrage wurde abgelehnt und kann nicht eingecheckt werden.']);
        }

        if ($Booking->checked_in === '1')
        {
            return $this->render('@Contao/modal_message.html.twig', ['type'=>'info', 'content'=>'Diese Buchung wurde bereits eingecheckt.']);
        }

        // Get the standort to access the number_ranges
        $Standort = $Slot->getRelated('pid');
        if ($Standort === null)
        {
            return new Response('Could not load standort data', 500);
        }

        // Prepare data for the template
        $arrSortenBooked = [];
        $SortenLeseart = explode(';', $Booking->sorten);
        foreach($SortenLeseart as $sorteLeseart)
        {
            list($sorte, $leseart) = explode(',', $sorteLeseart);
            $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte);
            $objLeseart = WeinanlieferungLeseartModel::findByPk($leseart);
            $arrSortenBooked[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null ? $objSorte->title : '') . ' ' . ($objLeseart !== null ? $objLeseart->title : '');
        }

        $arrErnteartBooked = [];
        if ($Booking->ernteart !== null)
        {
            foreach (explode(',', $Booking->ernteart) as $ernteart)
            {
                $arrErnteartBooked[] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart;
            }
        }

        $arrLagenBooked = [];
        if ($Booking->lage !== null)
        {
            if (($Lagen = $Booking->getRelated('lage')) !== null)
            {
                $arrLagenBooked = $Lagen->fetchEach('title');
            }
        }

        // Load the member model for the booking's user
        $memberModel = MemberModel::findById($Booking->uid);
        $currentMemberModel = MemberModel::findById(FrontendUser::getInstance()->id);

        $arrData = array_merge($arrData, [
            'modal' => $blnModal,
            'id' => $Booking->id,
            'slot' => array_merge($Slot->row(), [
                'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''),
            ]),
            'buchung' => array_merge($Booking->row(), [
                'sorten' => $arrSortenBooked,
                'ernteart' => $arrErnteartBooked,
                'lage' => $arrLagenBooked,
            ]),
            'standort' => $Standort,
            'checkin' => [
                'behaelter' => $Booking->behaelter,
                'expected' => ($Booking->unit_amount ?? 0) > 0 ? (int)$Booking->unit_amount : (int)$Booking->behaelter,
                'unit_title' => (function() use ($Booking) {
                    if ((int)$Booking->unit_id > 0) {
                        $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id);
                        return $m ? (string)$m->title : ($GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter');
                    }
                    return $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter';
                })(),
                'unit_amount_display' => (function() use ($Booking) {
                    if ((int)$Booking->unit_id > 0) {
                        $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id);
                        $amount = (int) ($Booking->unit_amount ?: 0);
                        if ($amount > 0) return $amount;
                        $mult = (int) ($m ? $m->multiplier : 0);
                        return $mult > 0 ? max(1, (int) ($Booking->behaelter / $mult)) : (int) $Booking->behaelter;
                    }
                    return (int) $Booking->behaelter;
                })(),
            ],
            'member' => $memberModel ? $memberModel->row() : null,
            'current_member' => $currentMemberModel ? $currentMemberModel->row() : null
        ]);

        // Add form data if provided (to preserve values after validation errors)
        if ($formData !== null) {
            $arrData['form_data'] = $formData;
        }

        if (!empty($error))
        {
            $arrData['toast'] = $error;
        }

        return $this->render('@Contao/modal_checkin.html.twig', $arrData);
    }

    protected function updateCheckin()
    {
        if (empty($_REQUEST['id']))
        {
            return new Response('Missing parameter', 412);
        }

        /** @var WeinanlieferungReservationModel $Booking */
        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null)
        {
            return new Response('Could not load booking data', 500);
        }

        // Validate that we have the correct number of behaelter numbers
        $behaelterNumbers = Input::post('behaelter_numbers');
        $expectedCount = ($Booking->unit_amount ?? 0) > 0 ? (int)$Booking->unit_amount : (int)$Booking->behaelter;
        if (!is_array($behaelterNumbers) || count($behaelterNumbers) != $expectedCount)
        {
            // Prepare form data to preserve input values
            $formData = [
                'behaelter_numbers' => $behaelterNumbers ?: [],
                'member_numbers' => Input::post('member_numbers') ?: []
            ];
            return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Bitte wählen Sie für jede Einheit eine Nummer aus.</div>', $formData);
        }

        // Get member numbers from the form
        $memberNumbers = Input::post('member_numbers');

        // If member numbers are not provided or not an array, initialize an empty array
        if (!is_array($memberNumbers) || count($memberNumbers) != $Booking->behaelter)
        {
            $memberNumbers = array_fill(0, count($behaelterNumbers), '');
        }

        // Get the current member's number to use as fallback
        $currentMember = MemberModel::findById(FrontendUser::getInstance()->id);
        $currentMemberNo = $currentMember ? $currentMember->memberno : '';

        // Filter out the special value 9999 ("Nummer nicht bekannt") for duplicate check
        $numbersForDuplicateCheck = array_filter($behaelterNumbers, function($number) {
            return $number !== '9999';
        });

        // Check for duplicate numbers (excluding the special value 9999)
        if (count(array_unique($numbersForDuplicateCheck)) != count($numbersForDuplicateCheck))
        {
            // Prepare form data to preserve input values
            $formData = [
                'behaelter_numbers' => $behaelterNumbers,
                'member_numbers' => $memberNumbers
            ];
            return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Jede Nummer kann nur einmal verwendet werden.</div>', $formData);
        }

        // Validate all numbers on the server side as a final check
        $invalidNumbers = [];
        $currentTime = time();
        $Slot = $Booking->getRelated('pid');
        $Standort = $Slot->getRelated('pid');

        // Get all used numbers from current bookings (excluding past bookings)
        $usedNumbers = [];

        // Get the database connection
        $db = Controller::getContainer()->get('database_connection');

        // Query to get used numbers from current bookings
        $sql = "SELECT r.behaelter_numbers
                FROM tl_vr_wa_reservation r
                JOIN tl_vr_wa_slot s ON r.pid = s.id
                WHERE r.behaelter_numbers != ''
                AND s.time >= ?
                AND r.id != ?
                AND s.pid = ?"; // Only check for the same standort

        $stmt = $db->prepare($sql);
        $stmt->bindValue(1, $currentTime);
        $stmt->bindValue(2, $Booking->id);
        $stmt->bindValue(3, $Standort->id);
        $result = $stmt->executeQuery();

        while ($row = $result->fetchAssociative()) {
            $numbers = json_decode($row['behaelter_numbers'], true);
            if (is_array($numbers)) {
                foreach ($numbers as $item) {
                    $usedNumbers[] = isset($item['behaelter']) ? $item['behaelter'] : $item;
                }
            }
        }

        // Check each number
        foreach ($behaelterNumbers as $index => $number) {
            // Skip the special value 9999
            if ($number === '9999') {
                continue;
            }

            // Check if the number is numeric
            if (!is_numeric($number)) {
                $invalidNumbers[] = "Behälter " . ($index + 1) . ": Die eingegebene Nummer ist keine gültige Zahl.";
                continue;
            }

            // Check if the number is already in use
            if (in_array($number, $usedNumbers)) {
                $invalidNumbers[] = "Behälter " . ($index + 1) . ": Diese Nummer wird bereits in einer anderen aktiven Buchung verwendet.";
                continue;
            }

            // Check if the number is within the valid ranges for this standort
            $validRanges = $Standort->extractNumbersFromRanges([], 10000); // Get all possible numbers
            if (!in_array($number, $validRanges) && !empty($validRanges)) {
                $invalidNumbers[] = "Behälter " . ($index + 1) . ": Die eingegebene Nummer liegt nicht im gültigen Bereich für diesen Standort.";
            }
        }

        // If there are invalid numbers, return an error
        if (!empty($invalidNumbers)) {
            $errorMessage = '<div class="toast toast--danger mx-0"><p>Folgende Fehler wurden gefunden:</p><ul>';
            foreach ($invalidNumbers as $error) {
                $errorMessage .= '<li>' . $error . '</li>';
            }
            $errorMessage .= '</ul></div>';

            // Prepare form data to preserve input values
            $formData = [
                'behaelter_numbers' => $behaelterNumbers,
                'member_numbers' => $memberNumbers,
                'invalid_fields' => array_map(function($error) {
                    // Extract the behälter number from the error message
                    if (preg_match('/Behälter (\d+):/', $error, $matches)) {
                        return (int)$matches[1] - 1; // Convert to zero-based index
                    }
                    return null;
                }, $invalidNumbers)
            ];

            return $this->renderCheckin(false, $errorMessage, $formData);
        }

        // Create combined array with behaelter numbers and member numbers
        $combinedData = [];
        foreach ($behaelterNumbers as $index => $behaelterNumber) {
            $memberNumber = !empty($memberNumbers[$index]) ? $memberNumbers[$index] : $currentMemberNo;
            $combinedData[] = [
                'behaelter' => $behaelterNumber,
                'member' => $memberNumber
            ];
        }

        // Save the check-in data
        $time = time();
        $Booking->checked_in = '1';
        $Booking->checked_in_on = $time;
        $Booking->behaelter_numbers = json_encode($combinedData);
        $Booking->tstamp = $time;
        $Booking->save();

        // Create reservation data array for the event
        $reservationData = $Booking->row();

        // Dispatch the check-in completed event
        $event = new CheckInCompletedEvent($reservationData, $Booking);
        $this->eventDispatcher->dispatch($event, CheckInCompletedEvent::NAME);

        return new Response('<div class="toast toast--success mx-0"><p>Check-in erfolgreich durchgeführt</p></div>', 200, ['HX-Trigger'=> 'updateWaBooking']);
    }

    protected function getAvailableNumbers()
    {
        if (empty($_REQUEST['id']))
        {
            return new Response('Required parameter missing', 412);
        }

        /** @var WeinanlieferungReservationModel $Booking */
        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null)
        {
            return new Response('Could not load booking data', 500);
        }

        if ($Booking->approved === '0')
        {
            return new Response(json_encode(['error' => 'Diese Buchungsanfrage wurde abgelehnt und kann nicht eingecheckt werden.']), 400, ['Content-Type' => 'application/json']);
        }

        if ($Booking->checked_in === '1')
        {
            return new Response(json_encode(['error' => 'Diese Buchung wurde bereits eingecheckt.']), 400, ['Content-Type' => 'application/json']);
        }

        // Get the standort to access the number_ranges
        $Standort = $Slot->getRelated('pid');
        if ($Standort === null)
        {
            return new Response(json_encode(['error' => 'Could not load standort data']), 500, ['Content-Type' => 'application/json']);
        }

        // Get all used numbers from current bookings (excluding past bookings)
        $usedNumbers = [];
        $currentTime = time();

        // Get the database connection
        $db = Controller::getContainer()->get('database_connection');

        // More efficient query to get used numbers from current bookings
        $sql = "SELECT r.behaelter_numbers
                FROM tl_vr_wa_reservation r
                JOIN tl_vr_wa_slot s ON r.pid = s.id
                WHERE r.behaelter_numbers != ''
                AND s.time >= ?
                AND r.id != ?";

        $stmt = $db->prepare($sql);
        $stmt->bindValue(1, $currentTime);
        $stmt->bindValue(2, $Booking->id);
        $result = $stmt->executeQuery();

        while ($row = $result->fetchAssociative()) {
            $numbers = json_decode($row['behaelter_numbers'], true);
            if (is_array($numbers)) {
                foreach ($numbers as $number) {
                    $usedNumbers[] = isset($number['behaelter']) ? $number['behaelter'] : $number;
                }
            }
        }

        // Get a batch of available numbers
        // We'll limit to a reasonable number to improve performance
        $limit = 100; // Adjust this based on your needs
        if (!empty($_REQUEST['limit']) && is_numeric($_REQUEST['limit'])) {
            $limit = (int)$_REQUEST['limit'];
        }

        // Get available numbers directly, excluding used ones
        $availableNumbers = $Standort->extractNumbersFromRanges($usedNumbers, $limit);

        // Add the special option "Nummer nicht bekannt" with value 9999
        // This option should always be available and can be used multiple times
        array_unshift($availableNumbers, '9999');

        // Return the numbers as JSON
        return new Response(json_encode(['numbers' => $availableNumbers]), 200, ['Content-Type' => 'application/json']);
    }

    protected function validateNumber()
    {
        if (empty($_REQUEST['id']) || !isset($_REQUEST['number']))
        {
            return new Response(json_encode(['valid' => false, 'message' => 'Required parameters missing']), 412, ['Content-Type' => 'application/json']);
        }

        $number = $_REQUEST['number'];

        // Special case: "Nummer nicht bekannt" is always valid
        if ($number === '9999') {
            return new Response(json_encode(['valid' => true]), 200, ['Content-Type' => 'application/json']);
        }

        // Check if the number is a valid number format
        if (!is_numeric($number)) {
            return new Response(json_encode([
                'valid' => false,
                'message' => 'Die eingegebene Nummer ist keine gültige Zahl.'
            ]), 200, ['Content-Type' => 'application/json']);
        }

        /** @var WeinanlieferungReservationModel $Booking */
        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null)
        {
            return new Response(json_encode(['valid' => false, 'message' => 'Could not load booking data']), 500, ['Content-Type' => 'application/json']);
        }

        // Get the standort to access the number_ranges
        $Standort = $Slot->getRelated('pid');
        if ($Standort === null)
        {
            return new Response(json_encode(['valid' => false, 'message' => 'Could not load standort data']), 500, ['Content-Type' => 'application/json']);
        }

        // Get all used numbers from current bookings (excluding past bookings)
        $usedNumbers = [];
        $currentTime = time();

        // Get the database connection
        $db = Controller::getContainer()->get('database_connection');

        // Query to get used numbers from current bookings
        $sql = "SELECT r.behaelter_numbers
                FROM tl_vr_wa_reservation r
                JOIN tl_vr_wa_slot s ON r.pid = s.id
                WHERE r.behaelter_numbers != ''
                AND s.time >= ?
                AND r.id != ?
                AND s.pid = ?"; // Only check for the same standort

        $stmt = $db->prepare($sql);
        $stmt->bindValue(1, $currentTime);
        $stmt->bindValue(2, $Booking->id);
        $stmt->bindValue(3, $Standort->id);
        $result = $stmt->executeQuery();

        while ($row = $result->fetchAssociative()) {
            $numbers = json_decode($row['behaelter_numbers'], true);
            if (is_array($numbers)) {
                foreach ($numbers as $item) {
                    $usedNumbers[] = isset($item['behaelter']) ? $item['behaelter'] : $item;
                }
            }
        }

        // Check if the number is already in use
        if (in_array($number, $usedNumbers)) {
            return new Response(json_encode([
                'valid' => false,
                'message' => 'Diese Nummer wird bereits in einer anderen aktiven Buchung verwendet.'
            ]), 200, ['Content-Type' => 'application/json']);
        }

        // Check if the number is within the valid ranges for this standort
        $validRanges = $Standort->extractNumbersFromRanges([], 10000); // Get all possible numbers
        if (!in_array($number, $validRanges) && !empty($validRanges)) {
            return new Response(json_encode([
                'valid' => false,
                'message' => 'Die eingegebene Nummer liegt nicht im gültigen Bereich für diesen Standort.'
            ]), 200, ['Content-Type' => 'application/json']);
        }

        // If we got here, the number is valid
        return new Response(json_encode(['valid' => true]), 200, ['Content-Type' => 'application/json']);
    }

    protected function getAvailableUnitsForCapacity(int $availableBaseUnits): array
    {
        // Always include the base unit (Behälter) with multiplier 1
        $units = [];
        $units[] = [
            'id' => 0,
            'title' => $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter',
            'multiplier' => 1,
            'max_amount' => $availableBaseUnits, // one check-in per base unit
        ];

        // Load custom units from DB
        if (($all = WeinanlieferungUnitModel::findAll()) !== null) {
            foreach ($all as $unit) {
                $mult = (int)($unit->multiplier ?? 0);
                if ($mult < 1) {
                    continue; // skip invalid
                }
                // A unit fits if at least one of it can be placed into remaining capacity
                $maxAmount = intdiv(max(0, $availableBaseUnits), $mult);
                if ($maxAmount < 1) {
                    continue; // does not fit current capacity
                }
                $units[] = [
                    'id' => (int)$unit->id,
                    'title' => (string)$unit->title,
                    'multiplier' => $mult,
                    'max_amount' => $maxAmount,
                ];
            }
        }

        return $units;
    }
}