<?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\Controller\Page;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\AdminBundle\Breadcrumb\Breadcrumb;
use Ferienpass\AdminBundle\Form\EditTenantType;
use Ferienpass\AdminBundle\Routing\Base58;
use Ferienpass\AdminBundle\Tenant\TenantSwitcher;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Tenant;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;
use Ferienpass\CoreBundle\Repository\TenantRepository;
use Ferienpass\CoreBundle\Tenant\TenantContext;
use Ferienpass\CoreBundle\Tenant\TenantRootPage;
use Ferienpass\CoreBundle\Tenant\TenantTables;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Uid\Uuid;
use Symfony\UX\Turbo\TurboBundle;

/**
 * Tenant administration — the one screen that is not about a municipality but
 * about the installation they share, hence ROLE_GLOBAL_ADMIN throughout.
 *
 * Tenant rows are not themselves tenant-scoped, so nothing here needs to escape
 * the Doctrine filter; the row counts behind the delete guard read through
 * {@see TenantTables}, which queries by tenant id explicitly.
 */
#[IsGranted('ROLE_GLOBAL_ADMIN')]
#[Route('/system/mandanten')]
final class TenantsController extends AbstractController
{
    public function __construct(private readonly TenantContext $tenantContext)
    {
    }

    #[Route('', name: 'admin_tenants_index')]
    public function index(TenantRepository $repository, Breadcrumb $breadcrumb, TenantSwitcher $switcher, EntityManagerInterface $entityManager, OfferRepositoryInterface $offers, ParticipantRepositoryInterface $participants, TenantRootPage $rootPages): Response
    {
        $this->denyUnlessMultiTenant();

        $user = $this->getUser();

        return $this->render('@FerienpassAdmin/page/tenants/index.html.twig', [
            'tenants' => $repository->findAllOrdered(),
            'inventory' => $this->inventory($entityManager, $offers, $participants),
            'ownTenant' => $user instanceof User ? $user->getTenant() : null,
            'switchedTenant' => $switcher->selected(),
            'createUrl' => $this->generateUrl('admin_tenants_create'),
            'withoutRootPage' => $this->withoutRootPage($repository, $rootPages),
            'breadcrumb' => $breadcrumb->generate('tools.title', ['tenants.title', ['route' => 'admin_tenants_index']]),
        ]);
    }

    // The `section` parameter is legacy: the Stammdaten screen renders as one
    // flat page, but bookmarked /bearbeiten/domains links must keep resolving.
    #[Route('/neu/{section?base}', name: 'admin_tenants_create')]
    #[Route('/{uuid}/bearbeiten/{section?base}', name: 'admin_tenants_edit', requirements: ['uuid' => Base58::UUID], priority: -1)]
    #[Route('/{uuid}/bearbeiten/{section?base}', name: 'admin_tenants_edit_legacy', requirements: ['uuid' => Requirement::UUID], priority: -1)]
    public function edit(?Uuid $uuid, Request $request, TenantRepository $repository, EntityManagerInterface $entityManager, Breadcrumb $breadcrumb, TenantSwitcher $switcher): Response
    {
        $this->denyUnlessMultiTenant();

        $item = null !== $uuid ? $repository->findOneBy(['uuid' => $uuid]) : null;
        if (null !== $uuid && null === $item) {
            throw $this->createNotFoundException();
        }

        $item ??= new Tenant();

        $form = $this->createForm(EditTenantType::class, $item);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager->persist($item);
            $entityManager->flush();

            $this->addFlash('success', 'Die Änderungen wurden erfolgreich übernommen.');

            return $this->redirectToRoute('admin_tenants_index');
        }

        $user = $this->getUser();

        return $this->render('@FerienpassAdmin/page/tenants/edit.html.twig', [
            'item' => $item,
            'form' => $form,
            'ownTenant' => $user instanceof User ? $user->getTenant() : null,
            'switchedTenant' => $switcher->selected(),
            'breadcrumb' => $breadcrumb->generate('tools.title', ['tenants.title', ['route' => 'admin_tenants_index']], $item->getId() ? (string) $item->getName() : 'tenants.new'),
        ]);
    }

    #[Route('/{uuid}/löschen', name: 'admin_tenants_delete', requirements: ['uuid' => Base58::UUID])]
    #[Route('/{uuid}/löschen', name: 'admin_tenants_delete_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function delete(Uuid $uuid, Request $request, TenantRepository $repository, EntityManagerInterface $entityManager, TenantTables $tenantTables, TenantRootPage $rootPages): Response
    {
        $this->denyUnlessMultiTenant();

        $item = $repository->findOneBy(['uuid' => $uuid]);
        if (null === $item) {
            throw $this->createNotFoundException();
        }

        // Deleting a municipality's children, bookings and receipts must never
        // be a side effect of tidying up the tenant list — the foreign keys are
        // RESTRICT, and this turns the resulting database error into an answer.
        $rows = $tenantTables->countRows($item);

        // `tl_page` is Contao's, so there is no foreign key to stop this one:
        // deleting the tenant would leave a root page pointing at nothing, and
        // the next tenant to reuse that id would inherit a website.
        $boundRootPages = $rootPages->rootPagesOfTenant((int) $item->getId());

        if ([] !== $rows || [] !== $boundRootPages) {
            $classByTable = $tenantTables->classByTable();
            $blockers = [];
            foreach ($rows as $table => $count) {
                $class = $classByTable[$table] ?? '';
                $blockers[] = [
                    'table' => $table,
                    'short' => false === ($pos = strrpos($class, '\\')) ? $class : substr($class, $pos + 1),
                    'count' => $count,
                ];
            }

            return $this->render('@FerienpassAdmin/page/tenants/delete_blocked.html.twig', [
                'item' => $item,
                'blockers' => $blockers,
                'rootPages' => $boundRootPages,
            ]);
        }

        $form = $this->createForm(FormType::class, options: [
            'action' => $this->generateUrl('admin_tenants_delete', ['uuid' => $item->getUuid()->toBase58()]),
        ]);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $deletedId = $item->getId();

            $entityManager->remove($item);
            $entityManager->flush();

            if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
                $request->setRequestFormat(TurboBundle::STREAM_FORMAT);

                return $this->renderBlock('@FerienpassAdmin/page/tenants/index.html.twig', 'deleted_stream', ['id' => $deletedId]);
            }

            return $this->redirectToRoute('admin_tenants_index');
        }

        return $this->render('@FerienpassAdmin/page/tenants/delete.html.twig', [
            'item' => $item,
            'form' => $form,
        ]);
    }

    /**
     * Takes a tenant offline in one step, or brings it back. This is the
     * alternative the delete guard offers: the domains answer 404, the data
     * stays untouched.
     */
    #[Route('/{uuid}/offline', name: 'admin_tenants_toggle_disabled', requirements: ['uuid' => Base58::UUID], methods: ['POST'])]
    public function toggleDisabled(Uuid $uuid, TenantRepository $repository, EntityManagerInterface $entityManager): Response
    {
        $this->denyUnlessMultiTenant();

        $item = $repository->findOneBy(['uuid' => $uuid]);
        if (null === $item) {
            throw $this->createNotFoundException();
        }

        $item->setDisabled(!$item->isDisabled());
        $entityManager->flush();

        $this->addFlash('success', $item->isDisabled()
            ? \sprintf('„%s“ ist offline. Die Domains antworten mit 404, die Daten bleiben unberührt.', $item->getName())
            : \sprintf('„%s“ ist wieder erreichbar.', $item->getName()));

        return $this->redirectToRoute('admin_tenants_index');
    }

    /**
     * Steps into a tenant for the rest of the session. The redirect matters:
     * the switch only takes hold on the next request, once the firewall has
     * refreshed the account in its own tenant.
     */
    #[Route('/{uuid}/wechseln', name: 'admin_tenants_switch', requirements: ['uuid' => Base58::UUID], methods: ['POST'])]
    public function switch(Uuid $uuid, TenantRepository $repository, TenantSwitcher $switcher): Response
    {
        $this->denyUnlessMultiTenant();

        $tenant = $repository->findOneBy(['uuid' => $uuid]);
        if (null === $tenant) {
            throw $this->createNotFoundException();
        }

        $switcher->switchTo($tenant);

        $this->addFlash('success', \sprintf('Sie arbeiten jetzt im Datenraum „%s“. Jede Ansicht, jede Änderung und jede Mail betrifft diesen Mandanten.', $tenant->getName()));

        return $this->redirectToRoute('admin_index');
    }

    #[Route('/zurücksetzen', name: 'admin_tenants_switch_reset', methods: ['POST'])]
    public function resetSwitch(TenantSwitcher $switcher): Response
    {
        $this->denyUnlessMultiTenant();

        $switcher->switchTo(null);

        $this->addFlash('success', 'Sie arbeiten wieder in Ihrem eigenen Mandanten.');

        return $this->redirectToRoute('admin_tenants_index');
    }

    /**
     * Tenants that have no Contao root page yet.
     *
     * Such a tenant is half-configured rather than broken: its hosts resolve
     * and the admin works, but the public site has no page tree to render, so
     * the list says so instead of leaving it to be discovered from a 404.
     *
     * A tenant that names admin domains only has no public site by design, so
     * it is not missing anything and is left out.
     *
     * @return list<string>
     */
    private function withoutRootPage(TenantRepository $repository, TenantRootPage $rootPages): array
    {
        $claimed = $rootPages->tenantIdsWithRootPage();

        $names = [];
        foreach ($repository->findAllOrdered() as $tenant) {
            if ($tenant->hasFrontend() && !\in_array((int) $tenant->getId(), $claimed, true)) {
                $names[] = (string) $tenant->getName();
            }
        }

        return $names;
    }

    private function denyUnlessMultiTenant(): void
    {
        if (!$this->tenantContext->isEnabled()) {
            throw $this->createNotFoundException('Multi-tenancy is disabled on this installation.');
        }
    }

    /**
     * Participants, offers and seasons per tenant id — the "Bestand" column of
     * the tenant list. Read through DBAL like {@see TenantTables}: this screen
     * compares municipalities, which is exactly what the tenant filter would
     * reshape into a view of the active one.
     *
     * @return array<int, array<string, int>> tenant id => [participants, offers, editions]
     */
    private function inventory(EntityManagerInterface $entityManager, OfferRepositoryInterface $offers, ParticipantRepositoryInterface $participants): array
    {
        $connection = $entityManager->getConnection();
        $inventory = [];

        $entities = [
            'participants' => $participants->getClassName(),
            'offers' => $offers->getClassName(),
            'editions' => Edition::class,
        ];

        foreach ($entities as $key => $class) {
            $metadata = $entityManager->getClassMetadata($class);
            $table = $connection->quoteSingleIdentifier($metadata->getTableName());
            $column = $connection->quoteSingleIdentifier($metadata->getSingleAssociationJoinColumnName('tenant'));

            $counts = $connection->fetchAllKeyValue(
                \sprintf('SELECT %1$s, COUNT(*) FROM %2$s WHERE %1$s IS NOT NULL GROUP BY %1$s', $column, $table),
            );

            foreach ($counts as $tenantId => $count) {
                $inventory[(int) $tenantId][$key] = (int) $count;
            }
        }

        return $inventory;
    }
}
