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

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\AdminBundle\Section\SectionComponentInterface;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Repository\ConsentRepository;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
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;

/**
 * Edition-level "Einverständnis der Veranstaltenden" section: the single,
 * optional confidentiality declaration the Veranstaltende (hosts) must sign
 * once per edition before they can open the participant lists of their offers.
 *
 * The declaration is a {@see ConsentForm} with subject `host`
 * ({@see Edition::getHostConsentForm()}); a signature is an ordinary
 * {@see \Ferienpass\CoreBundle\Entity\Consent}. Unlike the participant/account
 * "Einverständniserklärung" ({@see AgreementLetter}) it exposes no scope or type
 * — it is always a required, season-wide signature. The working copy lives in
 * the {@see $enabled}, {@see $title} and {@see $text} LiveProps so the preview
 * reacts to every keystroke; only on save is it reconciled into the edition's
 * host consent form.
 *
 * Once hosts have signed the current text the section is frozen (see
 * {@see isFrozen()}): editing the text mints a new {@see ConsentText} whose
 * changed hash invalidates their signatures and forces a re-signature. The
 * lockbar's "Entsperren" is the deliberate way to do that anyway.
 */
#[AsLiveComponent('Admin:Edition:HostConsent', template: '@FerienpassAdmin/components/Edition/HostConsent.html.twig', route: 'live_component_admin')]
class HostConsent extends AbstractController implements SectionComponentInterface
{
    use ComponentToolsTrait;
    use DefaultActionTrait;

    #[LiveProp]
    public Edition $edition;

    #[LiveProp]
    public bool $enabled = false;

    #[LiveProp(writable: true)]
    public string $title = '';

    #[LiveProp(writable: true)]
    public string $text = '';

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

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

    /**
     * Whether the user explicitly unlocked the editor although signatures exist.
     */
    #[LiveProp]
    public bool $unlocked = false;

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

    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly ConsentRepository $consents,
        private readonly EditionRepository $editions,
    ) {
    }

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

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

    public function mount(Edition $edition): void
    {
        $this->edition = $edition;

        $form = $edition->getHostConsentForm();
        $this->enabled = null !== $form;
        $this->title = (string) ($form?->getTitle() ?? '');
        $this->text = (string) ($form?->getText() ?? '');
    }

    /** Editions are admin-only and never variant — always editable (subject to the freeze). */
    public function isLocked(): bool
    {
        return false;
    }

    public function footerParams(): array
    {
        return ['alias' => $this->edition->getAlias()];
    }

    public function footerEditRoute(): string
    {
        return 'admin_editions_edit';
    }

    public function footerViewRoute(): string
    {
        return 'admin_editions_show';
    }

    /**
     * Distinct Veranstaltende holding a valid signature for the persisted text —
     * feeds the lockbar. Counts by text hash so migrated (form-less) legacy
     * signatures count too, consistent with how {@see \Ferienpass\CoreBundle\ConsentManager\ConsentManager}
     * gates access.
     */
    public function signees(): int
    {
        $hash = $this->edition->getHostConsentForm()?->getConsentText()?->getHash();
        if (null === $hash) {
            return 0;
        }

        return $this->consents->countValidSigneesByHash($hash);
    }

    /**
     * Frozen once anyone signed the current text, until explicitly unlocked.
     */
    public function isFrozen(): bool
    {
        return !$this->unlocked && $this->signees() > 0;
    }

    /**
     * Deep link into the consents admin list, filtered to this declaration's
     * signatures.
     */
    public function signaturesLink(): string
    {
        $form = $this->edition->getHostConsentForm();
        if (null === $form) {
            return $this->generateUrl('admin_consents_index');
        }

        return $this->generateUrl('admin_consents_index', ['filter' => ['consentForm' => [$form->getId()->toRfc4122()]]]);
    }

    /**
     * Prior seasons carrying a declaration, to seed this one from.
     *
     * @return list<array{alias: string, name: string, title: string}>
     */
    public function previousEditions(): array
    {
        $out = [];
        foreach ($this->editions->findWithHostAgreement() as $edition) {
            if ($edition->getId() === $this->edition->getId()) {
                continue;
            }

            $out[] = [
                'alias' => (string) $edition->getAlias(),
                'name' => (string) $edition->getName(),
                'title' => (string) ($edition->getHostConsentForm()?->getTitle() ?? ''),
            ];
        }

        return $out;
    }

    #[LiveAction]
    public function toggleEnabled(): void
    {
        $this->denyAccessUnlessEditable();
        if ($this->isFrozen()) {
            return;
        }

        $this->enabled = !$this->enabled;
    }

    /**
     * The lockbar's Entsperren/Sperren. Re-locking discards pending edits by
     * restoring the persisted values into the working copy.
     */
    #[LiveAction]
    public function toggleLock(): void
    {
        $this->denyAccessUnlessEditable();
        if (0 === $this->signees()) {
            return;
        }

        $this->unlocked = !$this->unlocked;

        if (!$this->unlocked) {
            $this->mount($this->edition);
        }
    }

    #[LiveAction]
    public function importFrom(#[LiveArg] string $alias): void
    {
        $this->denyAccessUnlessEditable();
        if ($this->isFrozen()) {
            return;
        }

        $source = $this->editions->findOneBy(['alias' => $alias]);
        $form = $source?->getHostConsentForm();
        if (null === $form || null === $form->getText()) {
            return;
        }

        $this->title = (string) ($form->getTitle() ?? '');
        $this->text = (string) $form->getText();
    }

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

        $form = $this->edition->getHostConsentForm();

        if ($this->enabled) {
            if (!$form instanceof ConsentForm) {
                $form = new ConsentForm();
                $form->setSubject('host');
                $form->setCompulsory('compulsory');
                $this->edition->addConsentForm($form);
            }

            $form->setTitle('' !== trim($this->title) ? $this->title : null);
            // setText() mints a new ConsentText only when the text changed, so an
            // unchanged text keeps existing signatures valid.
            $form->setText('' !== trim($this->text) ? $this->text : null);
        } elseif ($form instanceof ConsentForm) {
            // Disabled: drop the declaration entirely (no orphanRemoval on the
            // mapping, so remove it explicitly).
            $this->edition->removeConsentForm($form);
            $this->entityManager->remove($form);
        }

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

        $this->mount($this->edition);
        $this->unlocked = false;

        $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_editions_edit', ['alias' => $this->edition->getAlias(), 'section' => $nextSection]);
        }
        if ($redirect) {
            return $this->redirectToRoute($redirect, ['alias' => $this->edition->getAlias()]);
        }

        return null;
    }

    /**
     * Editions are administered by admins only; re-assert it here for the
     * separate live-action request.
     */
    private function denyAccessUnlessEditable(): void
    {
        $this->denyAccessUnlessGranted('ROLE_ADMIN');
    }
}
