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

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Enum\OfferStatus;
use Ferienpass\CoreBundle\Export\Offer\Excel\CellDto;
use Ferienpass\CoreBundle\Export\Offer\Excel\ExcelExportInterface;
use Ferienpass\CoreBundle\Export\Offer\OffersExportInterface;
use Ferienpass\CoreBundle\Export\Offer\PrintSheet\PdfExport;
use Ferienpass\CoreBundle\Message\ExportOffers;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\HostRepository;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

#[AsLiveComponent(route: 'live_component_admin')]
final class Export extends AbstractController
{
    use ComponentToolsTrait;
    use DefaultActionTrait;
    use ValidatableComponentTrait;

    /**
     * Number of offers rendered into the live preview. Kept small so the
     * preview stays cheap to (re-)render on every interaction.
     */
    private const PREVIEW_LIMIT = 6;
    private const PREVIEW_LIMIT_PDF = 4;

    #[LiveProp(writable: true)]
    #[Assert\NotBlank]
    public ?string $export = null;

    #[LiveProp]
    public ?Edition $edition = null;

    #[LiveProp(writable: true)]
    public array $selectedEditions = [];

    #[LiveProp(writable: true)]
    public array $selectedHosts = [];

    #[LiveProp(writable: true)]
    public bool $onlyPublished = true;

    public bool $noEntriesNotification = false;
    public bool $successNotification = false;

    public function __construct(private readonly OfferRepositoryInterface $offers, private readonly EditionRepository $editions, #[AutowireLocator(OffersExportInterface::class, indexAttribute: 'name', defaultIndexMethod: 'getName')] private readonly ServiceLocator $exporters, private readonly HostRepository $hostRepository)
    {
    }

    public function mount(?Edition $edition = null): void
    {
        $this->edition = $edition;
        if ($this->edition) {
            $this->selectedEditions = [$this->edition->getId()];
        }

        // Pre-select the first available exporter so the live preview shows
        // immediately, matching the redesigned page.
        if (null === $this->export) {
            $this->export = $this->exporterOptions()[0] ?? null;
        }
    }

    #[ExposeInTemplate]
    public function exporterOptions(): array
    {
        return array_keys($this->exporters->getProvidedServices());
    }

    /**
     * Metadata for each configured exporter, driving the format cards.
     *
     * @return list<array{name: string, type: string, ext: string}>
     */
    #[ExposeInTemplate]
    public function formats(): array
    {
        return array_map(static function (string $name): array {
            $type = self::typeOf($name);

            return [
                'name' => $name,
                'type' => $type,
                'ext' => strtoupper($type),
            ];
        }, $this->exporterOptions());
    }

    #[ExposeInTemplate]
    public function previewType(): ?string
    {
        return null !== $this->export ? self::typeOf($this->export) : null;
    }

    /**
     * A cheap fingerprint of everything that influences the preview. Used as a
     * DOM key for the shadow-root host so a Live Component morph re-attaches the
     * shadow root when the rendered preview actually changes.
     */
    #[ExposeInTemplate]
    public function previewSignature(): string
    {
        return substr(md5(implode('|', [
            $this->export,
            implode(',', $this->selectedEditions),
            implode(',', $this->selectedHosts),
            $this->onlyPublished ? '1' : '0',
        ])), 0, 10);
    }

    #[ExposeInTemplate]
    public function editionOptions(): array
    {
        return $this->editions->findBy(['archived' => 0]);
    }

    #[ExposeInTemplate]
    public function hostOptions(): array
    {
        return $this->hostRepository->findBy([]);
    }

    /**
     * Offer count per edition id (honouring the "only published" toggle),
     * shown next to the season chips.
     *
     * @return array<int, int>
     */
    #[ExposeInTemplate]
    public function editionCounts(): array
    {
        return $this->countGroupedBy('offer.edition');
    }

    /**
     * Offer count per host id (honouring the "only published" toggle), shown
     * next to the host checkboxes.
     *
     * @return array<int, int>
     */
    #[ExposeInTemplate]
    public function hostCounts(): array
    {
        $qb = $this->offers->createQueryBuilder('offer')
            ->select('hosts.id AS k', 'COUNT(DISTINCT offer.id) AS c')
            ->innerJoin('offer.hosts', 'hosts')
            ->groupBy('hosts.id')
        ;

        $this->applyPublishedConstraint($qb);

        $counts = [];
        foreach ($qb->getQuery()->getScalarResult() as $row) {
            $counts[(int) $row['k']] = (int) $row['c'];
        }

        return $counts;
    }

    /**
     * Total number of offers that match the current filter selection.
     */
    #[ExposeInTemplate]
    public function totalCount(): int
    {
        $qb = $this->offers->createQueryBuilder('offer')->select('COUNT(DISTINCT offer.id)');
        $this->applyFilters($qb);

        return (int) $qb->getQuery()->getSingleScalarResult();
    }

    /**
     * A handful of real offers matching the current filters, used to populate
     * the live preview.
     *
     * @return list<\Ferienpass\CoreBundle\Entity\Offer\OfferInterface>
     */
    #[ExposeInTemplate]
    public function previewOffers(): array
    {
        $limit = 'pdf' === $this->previewType() ? self::PREVIEW_LIMIT_PDF : self::PREVIEW_LIMIT;

        $qb = $this->offers
            ->createQueryBuilder('offer')
            ->addSelect('MIN(dates.begin) AS HIDDEN earliestDate')
            ->leftJoin('offer.dates', 'dates')
            ->groupBy('offer.id')
            ->orderBy('earliestDate', 'ASC')
            ->setMaxResults($limit)
        ;

        $this->applyFilters($qb);

        return $qb->getQuery()->getResult();
    }

    /**
     * Header names and a few real rows for the spreadsheet preview, read from
     * the selected exporter's column definition.
     *
     * @return array{headers: list<string>, rows: list<list<string>>}|null
     */
    #[ExposeInTemplate]
    public function previewColumns(): ?array
    {
        if ('xlsx' !== $this->previewType()) {
            return null;
        }

        $exporter = $this->selectedExporter();
        if (!$exporter instanceof ExcelExportInterface) {
            return null;
        }

        $cols = $exporter->columns();
        $cols = $cols instanceof \Traversable ? iterator_to_array($cols, true) : $cols;
        $headers = array_keys($cols);

        $rows = [];
        $scratch = new Spreadsheet();
        $sheet = $scratch->getActiveSheet();
        $iRow = 1;
        foreach ($this->previewOffers() as $offer) {
            $cells = [];
            $iCol = 1;
            foreach ($cols as $col) {
                $cell = $sheet->getCell(CellAddress::fromColumnAndRow($iCol, $iRow));
                $style = $sheet->getStyle(CellAddress::fromColumnAndRow($iCol, $iRow));
                $val = $col(new CellDto($offer, $cell, $style));
                $val = null !== $val ? (string) $val : (string) $cell->getValue();
                // Formula cells (e.g. =SUM(...)) cannot be evaluated here.
                $cells[] = str_starts_with($val, '=') ? '—' : $val;
                ++$iCol;
            }
            $rows[] = $cells;
            ++$iRow;
        }
        $scratch->disconnectWorksheets();

        return ['headers' => $headers, 'rows' => $rows];
    }

    /**
     * The real print-sheet HTML (cmyk converted to rgb for the browser) for the
     * PDF preview, isolated later inside a shadow root.
     */
    #[ExposeInTemplate]
    public function previewPdfHtml(): ?string
    {
        if ('pdf' !== $this->previewType()) {
            return null;
        }

        $exporter = $this->selectedExporter();
        if (!$exporter instanceof PdfExport) {
            return null;
        }

        $offers = $this->previewOffers();
        if ([] === $offers) {
            return null;
        }

        return $exporter->previewHtml($offers);
    }

    #[LiveAction]
    #[LiveListener('selectExport')]
    public function selectExport(#[LiveArg] string $name): void
    {
        $this->export = $name;
    }

    #[LiveAction]
    public function resetHosts(): void
    {
        $this->selectedHosts = [];
    }

    #[LiveAction]
    public function submit(#[CurrentUser] User $user, MessageBusInterface $commandBus): void
    {
        $this->validate();

        $offers = $this->queryOffers();
        if ([] === $offers) {
            $this->noEntriesNotification = true;

            return;
        }

        $uuids = array_map(static fn (string $uuid) => Uuid::fromBinary($uuid)->toRfc4122(), $offers);
        $commandBus->dispatch(new ExportOffers($this->export, $uuids, $user->getEmail()));

        $this->export = null;
        $this->successNotification = true;
    }

    private function queryOffers(): array
    {
        $qb = $this->offers
            ->createQueryBuilder('offer')
            ->select('offer.uuid')
            ->addSelect('MIN(dates.begin) AS earliestDate')
            ->leftJoin('offer.dates', 'dates')
            ->orderBy('earliestDate', 'ASC')
            ->groupBy('offer.uuid')
        ;

        $this->applyFilters($qb);

        return $qb->getQuery()->getSingleColumnResult();
    }

    /**
     * Apply the "only published" / season / host filters shared by the export
     * query, the preview query and the total count.
     */
    private function applyFilters(QueryBuilder $qb): void
    {
        $this->applyPublishedConstraint($qb);

        if ($this->selectedEditions) {
            $qb->andWhere('offer.edition IN (:editions)')->setParameter('editions', $this->selectedEditions, Types::SIMPLE_ARRAY);
        }

        if ($this->selectedHosts) {
            $qb->innerJoin('offer.hosts', 'hosts')->andWhere('hosts.id IN (:hosts)')->setParameter('hosts', $this->selectedHosts, Types::SIMPLE_ARRAY);
        }
    }

    private function applyPublishedConstraint(QueryBuilder $qb): void
    {
        if ($this->onlyPublished) {
            $published = OfferStatus::PUBLISHED;
            $qb->andWhere("JSON_CONTAINS_PATH(offer.status, 'one', '$.$published') = 1");
        }
    }

    /**
     * @return array<int, int>
     */
    private function countGroupedBy(string $groupField): array
    {
        $qb = $this->offers->createQueryBuilder('offer')
            ->select(\sprintf('IDENTITY(%s) AS k', $groupField), 'COUNT(DISTINCT offer.id) AS c')
            ->groupBy($groupField)
        ;

        $this->applyPublishedConstraint($qb);

        $counts = [];
        foreach ($qb->getQuery()->getScalarResult() as $row) {
            $counts[(int) $row['k']] = (int) $row['c'];
        }

        return $counts;
    }

    private function selectedExporter(): ?OffersExportInterface
    {
        if (null === $this->export || !$this->exporters->has($this->export)) {
            return null;
        }

        return $this->exporters->get($this->export);
    }

    private static function typeOf(string $name): string
    {
        $suffix = strrchr($name, '.');

        return false !== $suffix ? strtolower(substr($suffix, 1)) : '';
    }
}
