<?php

/**
 * Migration 0020 — Normalize all page element trees (retroactive cleanup)
 *
 * Walks every row in instinctor_pages and runs PageService::normalizeTree()
 * on the elements array, writing the cleaned tree back. This sweeps out any
 * orphan elements that accumulated before the normaliser was wired into
 * PageService::saveElements (2.1.433):
 *
 *   - elements with `type` not in PageService::ALLOWED_ELEMENT_TYPES
 *     (e.g. the deleted `image_popup` block that surfaced in page 112)
 *   - elements with `parent_id` referencing a non-existent element
 *   - top-level elements that aren't `section` (only sections may be top-level)
 *   - sections nested as children of other elements (corrupt placement)
 *
 * Per-page cascading: dropping a parent drops all descendants.
 *
 * Idempotent — running this migration twice has no effect after the first
 * pass; running PageService::normalizeTree() on already-clean data is a
 * no-op. Logs the cleanup count per page to error_log so the action is
 * auditable.
 *
 * Going forward: every PageService::getElements() AND PageService::saveElements()
 * call runs normalizeTree(), so no new orphans can be introduced. Stored data
 * cannot diverge from invariants for more than a single read/write cycle.
 *
 * @since 2.1.433
 */

if ( ! defined( 'ABSPATH' ) ) exit;

return [
    'description' => 'Normalize element trees of all pages (drops orphans, unknown types, dangling parent refs, misplaced sections)',

    // No schema changes — pure data migration.
    'up' => [],

    'php' => function() {
        $table = \Instinctor\Core\DB::prefix() . 'instinctor_pages';

        // Walk all pages including trashed ones — a trashed page restored
        // later should also have clean data.
        $rows = \Instinctor\Core\DB::get_results(
            "SELECT id FROM {$table} ORDER BY id ASC",
            ARRAY_A
        );

        if (!is_array($rows) || empty($rows)) {
            // phpcs:disable WordPress.PHP.DevelopmentFunctions -- server-side error capture / one-time migration logging, not debug output.
            error_log('[Instinctor migration 0020] No pages to normalize.');
            // phpcs:enable WordPress.PHP.DevelopmentFunctions
            return;
        }

        $processed   = 0;
        $cleanedPages = 0;
        $totalDropped = 0;

        foreach ($rows as $row) {
            $pageId = (int) ($row['id'] ?? 0);
            if (!$pageId) continue;
            $processed++;

            // Read raw JSON directly — bypassing PageService::getElements()
            // (which would auto-normalize and we want to count what we drop).
            $jsonRow = \Instinctor\Core\DB::get_row(
                \Instinctor\Core\DB::prepare(
                    "SELECT elements FROM {$table} WHERE id = %d LIMIT 1",
                    $pageId
                ),
                ARRAY_A
            );
            if (!$jsonRow) continue;

            $json = $jsonRow['elements'] ?? '';
            if (!$json) continue;

            $before = json_decode($json, true);
            if (!is_array($before) || empty($before)) continue;

            $beforeCount = count($before);
            $after       = \Instinctor\Services\PageService::normalizeTree($before);
            $afterCount  = count($after);

            if ($afterCount < $beforeCount) {
                $dropped = $beforeCount - $afterCount;
                $totalDropped += $dropped;
                $cleanedPages++;
                // phpcs:disable WordPress.PHP.DevelopmentFunctions -- server-side error capture / one-time migration logging, not debug output.
                error_log(sprintf(
                    '[Instinctor migration 0020] page %d: %d -> %d elements (dropped %d)',
                    $pageId, $beforeCount, $afterCount, $dropped
                ));
                // phpcs:enable WordPress.PHP.DevelopmentFunctions

                // Write back via direct UPDATE (not saveElements — that
                // would create a revision snapshot, which we don't want
                // for a migration sweep).
                $newJson = wp_json_encode($after);
                if ($newJson === false || $newJson === null) {
                    // phpcs:disable WordPress.PHP.DevelopmentFunctions -- server-side error capture / one-time migration logging, not debug output.
                    error_log(sprintf(
                        '[Instinctor migration 0020] page %d: json_encode failed, skipping',
                        $pageId
                    ));
                    // phpcs:enable WordPress.PHP.DevelopmentFunctions
                    continue;
                }
                $now = current_time('mysql');
                \Instinctor\Core\DB::query(
                    \Instinctor\Core\DB::prepare(
                        "UPDATE {$table} SET elements = %s, updated_at = %s WHERE id = %d",
                        $newJson, $now, $pageId
                    )
                );
            }
        }

        // phpcs:disable WordPress.PHP.DevelopmentFunctions -- server-side error capture / one-time migration logging, not debug output.
        error_log(sprintf(
            '[Instinctor migration 0020] DONE — processed %d pages, cleaned %d, dropped %d elements total',
            $processed, $cleanedPages, $totalDropped
        ));
        // phpcs:enable WordPress.PHP.DevelopmentFunctions
    },

    'down' => [
        // No rollback — this is a data cleanup migration. The dropped
        // elements were invalid by definition (unknown type, dangling
        // parent, misplaced section) and have no preservation value.
    ],
];
