<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
 * Phase 8: give a tenant pair to the aggregate children that are loaded straight
 * from a request uuid.
 *
 * The phase-7 audit concluded that aggregate children needed no column of their
 * own because every repository query anchors to its root. That was true of the
 * repositories — and missed the case where the anchor never happens:
 *
 *     $item = $this->serviceItemRepo->findByUuid($request['service_item_uuid']);
 *
 * A lookup by uuid *is* an unanchored query, and TenantFilter cannot help when
 * the table has no column to filter on. All three leaks found in phase 7 had
 * exactly this shape. With these columns the filter covers them, so a forgotten
 * guard returns nothing instead of another environment's row.
 *
 * Only the eight children actually reachable that way are changed — measured,
 * not guessed. The rest stay aggregate children and are still reached from their
 * root.
 *
 * The pair is derived from the root, and a root never changes environment, so
 * the copy is written once and can never drift.
 */
final class Version20260728170000 extends AbstractMigration
{
    /** table => [join clause, root alias] */
    private const BACKFILL = [
        'patient_attachments'     => ['JOIN patient_records r ON r.id = t.record_id', 'r'],
        'patient_calls'           => ['JOIN patient_records r ON r.id = t.record_id', 'r'],
        'patient_medical_records' => ['JOIN patient_records r ON r.id = t.record_id', 'r'],
        'patient_messages'        => ['JOIN patient_records r ON r.id = t.record_id', 'r'],
        'patient_notes'           => ['JOIN patient_records r ON r.id = t.record_id', 'r'],
        'patient_sessions'        => ['JOIN patient_records r ON r.id = t.record_id', 'r'],
        'session_payments'        => ['JOIN patient_sessions s ON s.id = t.session_id JOIN patient_records r ON r.id = s.record_id', 'r'],
        'service_items'           => ['JOIN service_sections sec ON sec.id = t.section_id', 'sec'],
    ];

    public function getDescription(): string
    {
        return 'Give the request-reachable aggregate children their own tenant pair';
    }

    public function up(Schema $schema): void
    {
        foreach (self::BACKFILL as $table => [$join, $alias]) {
            $this->connection->executeStatement(
                "ALTER TABLE {$table} ADD entity_type VARCHAR(10) NULL, ADD entity_id INT NULL"
            );
            $this->connection->executeStatement(
                "UPDATE {$table} t {$join}
                 SET t.entity_type = {$alias}.entity_type, t.entity_id = {$alias}.entity_id"
            );

            // A row whose root cannot be reached is already broken data; inventing
            // an environment for it would hide the breakage behind a wrong owner.
            $remaining = (int) $this->connection->fetchOne(
                "SELECT COUNT(*) FROM {$table} WHERE entity_type IS NULL OR entity_id IS NULL"
            );
            $this->abortIf(
                $remaining > 0,
                "{$remaining} row(s) in {$table} have no reachable root; fix them before rerunning."
            );

            $this->connection->executeStatement(
                "ALTER TABLE {$table} MODIFY entity_type VARCHAR(10) NOT NULL, MODIFY entity_id INT NOT NULL"
            );
            $this->connection->executeStatement(
                "CREATE INDEX idx_{$table}_entity ON {$table} (entity_type, entity_id)"
            );
        }
    }

    public function down(Schema $schema): void
    {
        foreach (array_keys(self::BACKFILL) as $table) {
            $this->addSql("DROP INDEX idx_{$table}_entity ON {$table}");
            $this->addSql("ALTER TABLE {$table} DROP entity_type, DROP entity_id");
        }
    }

    /** DDL on MariaDB commits implicitly; wrapping up() in a transaction would be a lie. */
    public function isTransactional(): bool
    {
        return false;
    }
}
