<?php

declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

/**
 * Point this package's stored permission grants at the io 6 enum names.
 *
 * Permissions are stored as PHP-`serialize()`d enum cases, so a namespace change does not
 * just move a class — it strands every grant that names the old one. `unserialize()` raises
 * on a missing class rather than returning false, and `PermissionCollection::createFromQueryResult()`
 * maps it over every row, so one stranded grant answers 500 for every page that builds a
 * menu.
 *
 * Rewriting the payload is not a string replacement: the serialised form is
 * `E:<length>:"<class>:<case>";`, and the length prefix has to be recomputed or
 * `unserialize()` reads past the end of the value.
 *
 * Grants naming any other class — an app's own permission enums, or another package's —
 * are left exactly as they are.
 */
final class RenameAdminPermissionEnums extends AbstractMigration
{
    /** Both tables are io-user's; this package only rewrites the class names it owns. */
    private const array TABLES = ['permissions__roles', 'permissions__users'];

    /** Pre-io 6 fully-qualified enum name => its current name. */
    private const array RENAMES = [
        'apexl\Io\modules\admin\enums\permissions\Admin' => 'Apexl\Io\Admin\Enums\Permissions\Admin',
    ];

    public function up(): void
    {
        $this->rename(self::RENAMES);
    }

    public function down(): void
    {
        $this->rename(array_flip(self::RENAMES));
    }

    /**
     * @param array<string, string> $map
     */
    private function rename(array $map): void
    {
        foreach (self::TABLES as $table) {
            if (!$this->hasTable($table)) {
                continue;
            }

            foreach ($this->storedPermissions($table) as $stored) {
                if (preg_match('/^E:\\d+:"([^":]+):([^"]+)";$/', $stored, $m) !== 1) {
                    continue;
                }

                [, $class, $case] = $m;

                if (isset($map[$class])) {
                    $this->rewrite($table, $stored, $this->serialise($map[$class], $case));
                }
            }
        }
    }

    /**
     * Every distinct value, filtered in PHP rather than by a LIKE: the payloads are full of
     * backslashes, and LIKE reads a backslash as its own escape character, so a pattern
     * carrying a class name silently matches nothing.
     *
     * @return string[]
     */
    private function storedPermissions(string $table): array
    {
        return array_column(
            $this->fetchAll(sprintf('SELECT DISTINCT permission FROM %s', $table)),
            'permission',
        );
    }

    /**
     * The case name is whatever was stored, not whatever the enum declares today: a grant
     * naming a case that has since been removed still has to be renamed, or it is left
     * behind as the same fatal under a different class name.
     */
    private function serialise(string $class, string $case): string
    {
        $body = $class . ':' . $case;

        return sprintf('E:%d:"%s";', strlen($body), $body);
    }

    /**
     * `(role, permission)` and `(user, permission)` are the whole row, so an UPDATE onto a
     * grant that already exists under the new name is a duplicate-key error rather than a
     * no-op. Ignore that collision and delete the stale row instead — the grant survives
     * either way.
     */
    private function rewrite(string $table, string $from, string $to): void
    {
        $this->execute(
            sprintf('UPDATE IGNORE %s SET permission = ? WHERE permission = ?', $table),
            [$to, $from],
        );

        $this->execute(
            sprintf('DELETE FROM %s WHERE permission = ?', $table),
            [$from],
        );
    }
}
