<?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\AdminBundle\Components\Offer;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\AdminBundle\Section\SectionComponentInterface;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Uid\Uuid;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

/**
 * The "Anmeldeformular" offer-edit section: a two-pane editor for the extra
 * application questions ({@see OfferInterface::getApplicationExtra()}) with a
 * live preview of how the participant application form will look.
 *
 * Replaces the legacy (now removed) `ApplicationExtra` LiveCollection form
 * field. The working copy of the questions lives in the
 * {@see $questions} LiveProp (not persisted until save), so the preview can
 * react to every keystroke / type switch without touching the database.
 *
 * The whole section is locked when the offer has no online application
 * (`!$offer->isOnlineApplication()`): without an online form there is nothing
 * to ask extra questions on. It is also locked for variant offers and for
 * users without the 'edit' voter.
 */
#[AsLiveComponent(template: '@FerienpassAdmin/components/Offer/ApplicationForm.html.twig', route: 'live_component_admin')]
class ApplicationForm extends AbstractController implements SectionComponentInterface
{
    use ComponentToolsTrait;
    use DefaultActionTrait;

    private const TYPES = ['yn', 'int', 'txt'];

    #[LiveProp]
    public OfferInterface $offer;

    /**
     * Working copy of the extra questions, each `{uuid, question, type, required}`.
     * Writable so the question text rides along via `data-model`; structural
     * changes (add / remove / type / required) go through LiveActions.
     *
     * @var list<array{uuid: string, question: string, type: string, required: bool}>
     */
    #[LiveProp(writable: true)]
    public array $questions = [];

    /**
     * Standard "collect date of birth" toggle. A standard field (it surfaces on
     * the participant list for offline applications too), so it is editable
     * regardless of the online-application lock that gates the extra questions.
     */
    #[LiveProp(writable: true)]
    public bool $collectDateOfBirth = false;

    #[LiveProp]
    public array $sections = [];

    #[LiveProp]
    public ?string $section = null;

    #[LiveProp]
    public bool $isReadonly = false;

    public bool $success = false;
    public string $message = '';

    public function __construct(private readonly EntityManagerInterface $entityManager)
    {
    }

    public static function getSectionName(): string
    {
        return 'applicationForm';
    }

    public static function getSectionScope(): string
    {
        return 'offer';
    }

    public function mount(OfferInterface $offer): void
    {
        // A prop consumed by mount() is NOT auto-assigned to its matching
        // property by TwigComponent, so set $this->offer explicitly.
        $this->offer = $offer;

        $this->collectDateOfBirth = $offer->isCollectDateOfBirth();

        $this->questions = array_map(static fn (array $e): array => [
            'uuid' => ($e['uuid'] ?? '') ?: Uuid::v7()->toRfc4122(),
            'question' => (string) ($e['question'] ?? ''),
            'type' => \in_array($e['type'] ?? null, self::TYPES, true) ? $e['type'] : 'txt',
            'required' => (bool) ($e['required'] ?? false),
        ], $offer->getApplicationExtra() ?? []);
    }

    /**
     * Whether the extra-questions editor + live preview are locked. The
     * date-of-birth toggle is NOT covered by this — see {@see $collectDateOfBirth}.
     */
    public function isLocked(): bool
    {
        return !$this->offer->isOnlineApplication() || $this->isOfferReadonly();
    }

    /**
     * Offer-level read-only: variant offers and users without the 'edit' voter.
     * Gates the date-of-birth toggle and the save button.
     */
    public function isOfferReadonly(): bool
    {
        return $this->isReadonly || $this->offer->isVariant();
    }

    #[LiveAction]
    public function addQuestion(): void
    {
        $this->denyAccessUnlessQuestionsEditable();

        $this->questions[] = ['uuid' => Uuid::v7()->toRfc4122(), 'question' => '', 'type' => 'txt', 'required' => false];
        $this->questions = array_values($this->questions);
    }

    #[LiveAction]
    public function removeQuestion(#[LiveArg] int $index): void
    {
        $this->denyAccessUnlessQuestionsEditable();

        unset($this->questions[$index]);
        $this->questions = array_values($this->questions);
    }

    #[LiveAction]
    public function setType(#[LiveArg] int $index, #[LiveArg] string $value): void
    {
        $this->denyAccessUnlessQuestionsEditable();

        if (isset($this->questions[$index]) && \in_array($value, self::TYPES, true)) {
            $this->questions[$index]['type'] = $value;
        }
    }

    #[LiveAction]
    public function toggleRequired(#[LiveArg] int $index): void
    {
        $this->denyAccessUnlessQuestionsEditable();

        if (isset($this->questions[$index])) {
            $this->questions[$index]['required'] = !($this->questions[$index]['required'] ?? false);
        }
    }

    #[LiveAction]
    public function save(#[LiveArg] ?string $nextSection = null, #[LiveArg] ?string $redirect = null): ?Response
    {
        $this->denyAccessUnlessOfferEditable();

        // The date-of-birth toggle is always editable, so persist it whenever
        // the offer is editable.
        $this->offer->setCollectDateOfBirth($this->collectDateOfBirth);

        // Only write the extra questions when the editor was actually unlocked
        // (online application on). Otherwise the editor showed the lock notice
        // and $this->questions is just the untouched DB snapshot — guard against
        // a tampered client overwriting it.
        if ($this->offer->isOnlineApplication()) {
            // Drop blank rows (no question text) so empty editors don't persist —
            // mirrors the legacy collection's delete_empty behaviour. Entity
            // setter re-normalises and back-fills any missing uuid.
            $this->offer->setApplicationExtra(array_values(array_filter(
                $this->questions,
                static fn (array $q): bool => '' !== trim((string) ($q['question'] ?? '')),
            )));
        }

        $this->entityManager->flush();
        $this->entityManager->refresh($this->offer);

        // Reflect the cleaned list back into the editor.
        $this->mount($this->offer);

        $this->success = true;
        $this->dispatchBrowserEvent('live:saved', ['success' => true]);

        if ($nextSection || $redirect) {
            $this->addFlash('success', ['type' => 'success', 'title' => 'Daten gespeichert', 'message' => 'Die Änderungen wurden erfolgreich übernommen.']);
        } else {
            $this->message = 'Die Änderungen wurden erfolgreich übernommen.';
        }

        if ($nextSection) {
            return $this->redirectToRoute('admin_offers_edit', ['uuid' => $this->offer->getUuid()->toBase58(), 'section' => $nextSection]);
        }
        if ($redirect) {
            return $this->redirectToRoute($redirect, ['uuid' => $this->offer->getUuid()->toBase58()]);
        }

        return null;
    }

    /**
     * Offer-level write guard (save + date-of-birth): variant offers are
     * read-only and the user needs the 'edit' voter. Re-checks the offer rather
     * than trusting the {@see $isReadonly} LiveProp, which the client could flip.
     */
    private function denyAccessUnlessOfferEditable(): void
    {
        if ($this->offer->isVariant()) {
            throw new BadRequestHttpException('Cannot modify the application form on a variant offer.');
        }

        $this->denyAccessUnlessGranted('edit', $this->offer);
    }

    /**
     * Extra-questions write guard: everything {@see denyAccessUnlessOfferEditable()}
     * checks, plus the online-application requirement — without an online form
     * there is nothing to attach extra questions to.
     */
    private function denyAccessUnlessQuestionsEditable(): void
    {
        $this->denyAccessUnlessOfferEditable();

        if (!$this->offer->isOnlineApplication()) {
            throw new BadRequestHttpException('The offer has no online application; extra questions cannot be edited.');
        }
    }
}
