<?php
/**
 * Self-contained designed pages.
 *
 * Wraps every Co-pilot designed page's HTML in a <div class="swcp-self-contained">
 * envelope with an inline <style> block that unconstrains the most common host-
 * theme content containers. The page then renders correctly even with the plugin
 * deactivated — WordPress just outputs post_content, the browser reads the inline
 * styles, and the design fills the viewport on Astra / Kadence / GeneratePress /
 * Divi / Twenty-* / FSE block themes.
 *
 * Plugin still owns the prettier path (full-bleed template, theme-CSS strip) via
 * class-fullpage-renderer.php — this layer is the graceful fallback.
 */

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

/**
 * Returns the inline <style> block. Marker comment lets us detect already-wrapped
 * content for idempotent re-saves and the migration script.
 */
function swcp_get_self_contained_css() {
    return <<<CSS
<style data-swcp-self-contained="1">
/* SailWP Co-pilot — self-contained page bypass.
   Keeps the design full-bleed when the plugin is off. */
body:has(.swcp-self-contained) {
    margin: 0 !important;
    padding: 0 !important;
    background: #fff !important;
}
/* Hide host-theme chrome so a deactivated-plugin page still presents
   edge-to-edge. Covers Astra / Kadence / GeneratePress / Twenty-* / FSE
   header & footer regions. Admin bar stays for logged-in editors. */
body:has(.swcp-self-contained) > header,
body:has(.swcp-self-contained) > footer,
body:has(.swcp-self-contained) .site-header,
body:has(.swcp-self-contained) .site-footer,
body:has(.swcp-self-contained) #masthead,
body:has(.swcp-self-contained) #colophon,
body:has(.swcp-self-contained) .ast-primary-header-bar,
body:has(.swcp-self-contained) .ast-above-header,
body:has(.swcp-self-contained) .ast-below-header,
body:has(.swcp-self-contained) .ast-sticky-shrunk,
body:has(.swcp-self-contained) .main-navigation,
body:has(.swcp-self-contained) .wp-block-template-part[data-type="header"],
body:has(.swcp-self-contained) .wp-block-template-part[data-type="footer"],
body:has(.swcp-self-contained) .entry-header,
body:has(.swcp-self-contained) .entry-title,
body:has(.swcp-self-contained) .page-title,
body:has(.swcp-self-contained) .post-title,
body:has(.swcp-self-contained) .wp-block-post-title {
    display: none !important;
}
body:has(.swcp-self-contained) .site,
body:has(.swcp-self-contained) .site-content,
body:has(.swcp-self-contained) .site-main,
body:has(.swcp-self-contained) .content-area,
body:has(.swcp-self-contained) .content-wrap,
body:has(.swcp-self-contained) .entry-content,
body:has(.swcp-self-contained) .entry-content-wrap,
body:has(.swcp-self-contained) .ast-container,
body:has(.swcp-self-contained) .ast-separate-container,
body:has(.swcp-self-contained) .grid-container,
body:has(.swcp-self-contained) .inside-article,
body:has(.swcp-self-contained) #main-content,
body:has(.swcp-self-contained) #content,
body:has(.swcp-self-contained) #primary,
body:has(.swcp-self-contained) .wp-block-post-content {
    max-width: none !important;
    width: 100% !important;
    padding: 0 !important;
    margin: 0 !important;
    background: transparent !important;
}
/* Negative-margin escape: works even on themes whose container class we missed,
   because it's calculated from the viewport, not the parent. */
.swcp-self-contained {
    position: relative;
    width: 100vw;
    margin-left: calc(50% - 50vw);
    max-width: none;
    background: #fff;
}
</style>
CSS;
}

/**
 * Wrap raw design HTML in the self-contained envelope. Always re-wraps so that
 * re-saves and migrations pick up the latest bypass CSS (the CSS itself is
 * versioned via the marker style block; an existing wrapper is stripped first).
 *
 * Input may include or omit the surrounding <!-- wp:html --> markers; output
 * always includes them.
 */
function swcp_wrap_self_contained($html) {
    $html = (string) $html;

    // Strip the marker style block first (the bypass CSS). Anchored on the
    // data-swcp-self-contained attribute so we never eat the design's own
    // style blocks.
    $html = preg_replace(
        '#<style\s+[^>]*data-swcp-self-contained=[^>]*>.*?</style>#is',
        '',
        $html
    );

    // Strip the wrapper div (keep its contents). Match the opening tag with
    // any whitespace and the corresponding closing </div> as the last </div>
    // before the wp:html closer or end-of-input.
    if (preg_match(
        '#<div\s+class=("|\')swcp-self-contained\1\s*>(.*)</div>\s*(?:<!--\s*/wp:html\s*-->)?\s*$#is',
        $html,
        $m
    )) {
        $html = $m[2];
    }

    // Strip wp:html block markers from input so we can re-wrap cleanly.
    $inner = preg_replace('/<!--\s*\/?wp:html\s*-->/', '', $html);
    $inner = trim($inner);

    $style = swcp_get_self_contained_css();

    return "<!-- wp:html -->\n"
        . $style . "\n"
        . '<div class="swcp-self-contained">' . "\n"
        . $inner . "\n"
        . '</div>' . "\n"
        . "<!-- /wp:html -->";
}

/**
 * Auto-migrate on admin pageview after a plugin update.
 *
 * Stored option `swcp_self_contained_migrated_version` records which plugin
 * version last did the rewrap. If it lags the current SAILWP_COPILOT_VERSION,
 * we run the migration (which is idempotent — same-output posts are skipped),
 * then bump the flag.
 *
 * Debounce: at most one attempt per 30s, so a failing migration on one slow
 * request doesn't burn CPU on every subsequent admin pageview in the same minute.
 *
 * Only runs in wp-admin (admin_init) so the frontend request path stays cold —
 * not on every page render, not for anonymous traffic.
 */
function swcp_maybe_migrate_self_contained_on_upgrade() {
    if (!defined('SAILWP_COPILOT_VERSION')) return;
    $current = SAILWP_COPILOT_VERSION;
    $migrated_version = get_option('swcp_self_contained_migrated_version', '0');

    if (version_compare($migrated_version, $current, '>=')) {
        return; // already migrated against this code
    }

    $last_attempt = (int) get_option('swcp_self_contained_migration_last_attempt', 0);
    if (time() - $last_attempt < 30) {
        return; // backoff
    }
    update_option('swcp_self_contained_migration_last_attempt', time(), false);

    $result = swcp_migrate_self_contained();

    // Only bump the version flag on a clean run. Errors → keep retrying on
    // future admin pageviews (with the 30s debounce in between).
    if (empty($result['errors'])) {
        update_option('swcp_self_contained_migrated_version', $current, false);
        error_log(sprintf(
            '[swcp self-contained migration] v%s: migrated=%d skipped=%d total=%d',
            $current,
            (int) $result['migrated'],
            (int) $result['skipped'],
            (int) $result['total']
        ));
    } else {
        error_log(sprintf(
            '[swcp self-contained migration] v%s: %d errors, will retry',
            $current,
            count($result['errors'])
        ));
    }
}
add_action('admin_init', 'swcp_maybe_migrate_self_contained_on_upgrade', 20);

/**
 * Walk all _swcp_designed=1 posts and rewrite their post_content with the
 * self-contained envelope. Skips posts whose rewrap would be a no-op (same
 * output as input).
 *
 * Returns an array { migrated, skipped, errors, total }.
 */
function swcp_migrate_self_contained() {
    $posts = get_posts([
        'post_type'      => ['page', 'post'],
        'post_status'    => ['publish', 'draft', 'pending', 'private', 'future'],
        'meta_key'       => '_swcp_designed',
        'meta_value'     => '1',
        'numberposts'    => -1,
        'suppress_filters' => false,
    ]);

    $migrated = 0;
    $skipped  = 0;
    $errors   = [];

    foreach ($posts as $p) {
        $new = swcp_wrap_self_contained($p->post_content);
        if ($new === $p->post_content) {
            // Already up to date (same output) — count as skipped.
            $skipped++;
            continue;
        }
        $r = wp_update_post([
            'ID'           => $p->ID,
            'post_content' => $new,
        ], true);
        if (is_wp_error($r)) {
            $errors[] = ['id' => $p->ID, 'msg' => $r->get_error_message()];
            continue;
        }
        clean_post_cache($p->ID);
        $migrated++;
    }

    return [
        'migrated' => $migrated,
        'skipped'  => $skipped,
        'errors'   => $errors,
        'total'    => count($posts),
    ];
}
