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

use Contao\CoreBundle\Migration\AbstractMigration;
use Contao\CoreBundle\Migration\MigrationResult;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\Edition;

/**
 * Retires the bespoke per-edition "agreement letter" (`Edition::$agreementLetterText`
 * + the `AgreementLetterSignature` consent subclass) in favour of the general
 * per-edition {@see ConsentForm} model.
 *
 * The deprecated text becomes an account-level {@see ConsentForm} on the edition
 * (seeded with the same sha256 hash). The former signature class is gone; Consent
 * no longer uses a discriminator, so its legacy `edition_agreement_letter`-typed
 * rows simply load as plain Consent. Their `hash` column still holds sha256(text),
 * so {@see \Ferienpass\CoreBundle\ConsentManager\ConsentManager} keeps matching
 * them by hash and applicants who already agreed are not prompted again.
 *
 * Runs before the schema diff drops the `agreementLetterText`/`hasAgreementLetter`
 * columns (Contao runs migrations first), reading the text via raw SQL so it does
 * not depend on the — now removed — entity accessors. Idempotent: once the column
 * is gone {@see schemaReady()} is false and the migration no longer runs; editions
 * that already carry a matching consent form are skipped.
 */
class AgreementLetterToEditionMigration extends AbstractMigration
{
    private const DEFAULT_TITLE = 'Einverständniserklärung';

    public function __construct(
        private readonly Connection $db,
        private readonly EntityManagerInterface $em,
    ) {
    }

    public function shouldRun(): bool
    {
        if (!$this->schemaReady()) {
            return false;
        }

        // Only if there is still an unmigrated letter. Must mirror run()'s skip
        // rule exactly — the legacy columns are dropped separately (--with-deletes),
        // so keying off their mere presence would loop forever once seeded.
        return [] !== $this->pendingEditions();
    }

    public function run(): MigrationResult
    {
        // Seed an edition-level, account-scoped consent form from the deprecated
        // text — but only where no form with that exact text (hash) exists yet.
        $pending = $this->pendingEditions();
        foreach ($pending as ['edition' => $edition, 'text' => $text]) {
            $form = new ConsentForm();
            $form->setSubject('account');
            $form->setCompulsory('compulsory');
            $form->setTitle(self::DEFAULT_TITLE);
            $form->setText($text);
            // The letter existed for the edition's lifetime, so date the form to
            // the edition rather than "now" — keeps it ahead of its signatures
            // and out of the way of freshly-created forms in date-sorted lists.
            $form->setCreatedAt(\DateTimeImmutable::createFromInterface($edition->getCreatedAt()));
            $edition->addConsentForm($form);

            $this->em->persist($form);
        }

        if ([] !== $pending) {
            $this->em->flush();
        }

        return $this->createResult(true, \sprintf('Agreement letter migrated: seeded %d edition(s).', \count($pending)));
    }

    /**
     * Legacy editions whose deprecated text is not yet represented by a matching
     * consent form — the real work list, shared by shouldRun()/run() so the
     * migration reports "nothing to do" once every letter has been migrated.
     *
     * @return list<array{edition: Edition, text: string}>
     */
    private function pendingEditions(): array
    {
        $out = [];
        foreach ($this->legacyEditions() as $row) {
            $edition = $this->em->getRepository(Edition::class)->find((int) $row['id']);
            if (!$edition instanceof Edition) {
                continue;
            }

            $text = trim((string) $row['agreementLetterText']);
            if ('' === $text || $this->editionHasFormWithHash($edition, hash('sha256', $text))) {
                continue;
            }

            $out[] = ['edition' => $edition, 'text' => $text];
        }

        return $out;
    }

    private function editionHasFormWithHash(Edition $edition, string $hash): bool
    {
        foreach ($edition->getConsentForms() as $form) {
            if ($form->getConsentText()?->getHash() === $hash) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return list<array{id: int|string, agreementLetterText: string}>
     */
    private function legacyEditions(): array
    {
        return $this->db
            ->executeQuery(
                "SELECT `id`, `agreementLetterText`
                 FROM `Edition`
                 WHERE `hasAgreementLetter` = 1
                   AND `agreementLetterText` IS NOT NULL
                   AND TRIM(`agreementLetterText`) != ''"
            )
            ->fetchAllAssociative()
        ;
    }

    private function schemaReady(): bool
    {
        $schemaManager = $this->db->createSchemaManager();

        if (!$schemaManager->tablesExist(['Edition', 'Consent', 'ConsentForm'])) {
            return false;
        }

        $columns = $schemaManager->listTableColumns('Edition');

        // Once the schema diff has dropped the legacy columns there is nothing
        // left to migrate.
        if (!isset($columns['agreementlettertext'], $columns['hasagreementletter'])) {
            return false;
        }

        // Stay inert until the schema diff has added ConsentForm.createdAt —
        // creating/loading a ConsentForm before then hits a missing column.
        return isset($schemaManager->listTableColumns('ConsentForm')['createdat']);
    }
}
