<?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\CoreBundle\Tenant;

use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\PageModel;
use Doctrine\DBAL\Connection;
use Ferienpass\CoreBundle\Entity\Tenant;

/**
 * The Contao root page a tenant's website hangs off — the one place that
 * translates between the two worlds.
 *
 * Contao has no notion of tenants, so the page tree, the themes, the layouts
 * and the file manager are shared by the whole installation. What is *not*
 * shared is a root page: `tl_page.tenant` binds each one to exactly one
 * municipality — the cms-bundle's `TenantRootPageListener` keeps that
 * one-to-one when a root page is saved — so every page below it belongs to
 * that municipality and nothing else.
 *
 * Code that used to reach for "the" root page — `findPublishedRootPages()[0]`,
 * which quietly means "whichever municipality sorts first" — must go through
 * here instead.
 *
 * On single-tenant builds this keeps the old behaviour exactly: the first
 * published root page, no tenant involved.
 */
class TenantRootPage
{
    public function __construct(
        private readonly ContaoFramework $framework,
        private readonly Connection $connection,
        private readonly TenantContext $tenantContext,
    ) {
    }

    /**
     * The root page of the tenant the current request, command or message
     * belongs to, or null when that tenant has none yet.
     */
    public function current(): ?PageModel
    {
        if (!$this->tenantContext->isEnabled()) {
            return $this->publishedRootPages()[0] ?? null;
        }

        return $this->forTenant($this->tenantContext->getTenant());
    }

    public function forTenant(?Tenant $tenant): ?PageModel
    {
        $id = $tenant?->getId();
        if (null === $id) {
            return null;
        }

        foreach ($this->publishedRootPages() as $page) {
            if ($id === (int) $page->tenant) {
                return $page;
            }
        }

        return null;
    }

    /**
     * The root pages claiming $tenantId, $exceptPageId aside — the guard behind
     * "one root page, one tenant".
     *
     * Reads the table rather than the model so it can run in the back end
     * before the DCA is saved, and in the admin without booting Contao.
     *
     * @return list<array{id: int, title: string, dns: string}>
     */
    public function rootPagesOfTenant(int $tenantId, ?int $exceptPageId = null): array
    {
        if ($tenantId <= 0) {
            return [];
        }

        $qb = $this->connection->createQueryBuilder()
            ->select('id', 'title', 'dns')
            ->from('tl_page')
            ->where('type = :type')
            ->andWhere('tenant = :tenant')
            ->setParameter('type', 'root')
            ->setParameter('tenant', $tenantId)
        ;

        if (null !== $exceptPageId) {
            $qb->andWhere('id != :except')->setParameter('except', $exceptPageId);
        }

        return array_map(
            static fn (array $row): array => ['id' => (int) $row['id'], 'title' => (string) $row['title'], 'dns' => (string) $row['dns']],
            $qb->executeQuery()->fetchAllAssociative(),
        );
    }

    /**
     * Unassigns a root page, so the tenant it named is free again.
     */
    public function releaseRootPage(int $pageId): void
    {
        $this->connection->update('tl_page', ['tenant' => 0], ['id' => $pageId, 'type' => 'root']);
    }

    /**
     * The tenants that have a root page at all. A tenant missing from this list
     * resolves its host but renders nothing — the admin says so out loud.
     *
     * @return list<int>
     */
    public function tenantIdsWithRootPage(): array
    {
        return array_map(
            static fn ($id): int => (int) $id,
            $this->connection->fetchFirstColumn("SELECT DISTINCT tenant FROM tl_page WHERE type = 'root' AND tenant > 0"),
        );
    }

    /**
     * @return list<PageModel>
     */
    private function publishedRootPages(): array
    {
        $this->framework->initialize();

        $pages = $this->framework->getAdapter(PageModel::class)->findPublishedRootPages();

        return null === $pages ? [] : $pages->getModels();
    }
}
