<?php
/**
 * Version timeline — list past versions of a page and restore any of
 * them. Builds on WordPress's built-in revisions (every wp_update_post
 * the Co-pilot tools make creates a revision automatically) so we get
 * point-in-time history for free, without a custom table.
 */
if (!defined('ABSPATH')) exit;

class SailWP_Copilot_Versions {
    // 14 revisions + 1 synthetic "current" = 15 dots total. Joost on
    // 2026-05-17: anything beyond ~15 dots feels visually busy in the rail.
    const MAX_VERSIONS = 14;

    public function __construct() {
        add_action('rest_api_init', [$this, 'register_routes']);
        // Front-end render of a single revision in an iframe-safe no-chrome
        // template, so the rail can preview an old version before the user
        // commits with Restore.
        add_action('template_redirect', [__CLASS__, 'maybe_render_revision_preview'], 1);
    }

    public function register_routes() {
        register_rest_route(SailWP_Copilot_REST::NAMESPACE_V1, '/page-versions', [
            'methods'             => 'GET',
            'callback'            => [$this, 'list_versions'],
            'permission_callback' => function () { return current_user_can('edit_posts'); },
            'args'                => [
                'post_id' => [
                    'required'          => true,
                    'sanitize_callback' => 'absint',
                ],
            ],
        ]);

        register_rest_route(SailWP_Copilot_REST::NAMESPACE_V1, '/restore-version', [
            'methods'             => 'POST',
            'callback'            => [$this, 'restore'],
            'permission_callback' => function () { return current_user_can('edit_posts'); },
        ]);
    }

    /**
     * GET /page-versions?post_id=X
     * Returns newest-first list of {revision_id, ts_iso, age_label, label, is_current}.
     * The first entry is a synthetic "current" so the rail can render a
     * full timeline ending on the live state.
     */
    public function list_versions(WP_REST_Request $request) {
        $post_id = (int) $request->get_param('post_id');
        $post = get_post($post_id);
        if (!$post) {
            return rest_ensure_response(['ok' => true, 'versions' => []]);
        }

        // Hide the timeline for pages owned by a third-party builder.
        // WP revisions only snapshot post_content, so a restore would wipe
        // the Elementor / Divi / etc design that lives in post meta. Better
        // to never offer it than to risk a destructive click.
        if (function_exists('swcp_detect_page_builder')) {
            $builder = swcp_detect_page_builder($post_id);
            if (!in_array($builder, ['gutenberg', 'classic', 'unknown'], true)) {
                return rest_ensure_response([
                    'ok'              => true,
                    'versions'        => [],
                    'disabled_reason' => 'page_builder',
                    'page_builder'    => $builder,
                ]);
            }
        }

        $versions = [];
        // Synthetic "current" head so the timeline always has at least one
        // dot the user can hover ("just now").
        $versions[] = [
            'revision_id' => 0,
            'ts_iso'      => mysql_to_rfc3339($post->post_modified_gmt),
            'ts'          => (int) strtotime($post->post_modified_gmt . ' UTC'),
            'age_label'   => 'now',
            'label'       => 'Current',
            'is_current'  => true,
        ];

        $revisions = wp_get_post_revisions($post_id, [
            'orderby' => 'date',
            'order'   => 'DESC',
            'number'  => self::MAX_VERSIONS,
        ]);
        foreach ($revisions as $rev) {
            $ts = (int) strtotime($rev->post_modified_gmt . ' UTC');
            $versions[] = [
                'revision_id' => (int) $rev->ID,
                'ts_iso'      => mysql_to_rfc3339($rev->post_modified_gmt),
                'ts'          => $ts,
                'age_label'   => self::humanise_age(time() - $ts),
                'label'       => sprintf('Version from %s', self::humanise_age(time() - $ts)),
                'is_current'  => false,
                'preview_url' => add_query_arg('swcp_render_revision', (int) $rev->ID, home_url('/')),
            ];
        }

        return rest_ensure_response(['ok' => true, 'versions' => $versions]);
    }

    /**
     * POST /restore-version  { post_id, revision_id }
     * Roll the parent post back to that revision. Captures the current
     * state as a fresh revision first so the restore itself is undoable.
     */
    public function restore(WP_REST_Request $request) {
        $post_id     = (int) $request->get_param('post_id');
        $revision_id = (int) $request->get_param('revision_id');
        if (!$post_id || !$revision_id) {
            return new WP_Error('missing_params', 'post_id and revision_id are required', ['status' => 400]);
        }
        $post = get_post($post_id);
        $rev  = get_post($revision_id);
        if (!$post || !$rev || $rev->post_parent !== $post_id) {
            return new WP_Error('not_found', 'That version is not linked to this page.', ['status' => 404]);
        }

        // Reject the restore if the page is now owned by a third-party
        // builder. Otherwise we overwrite post_content (which the builder
        // doesn't use) and leave the builder data orphaned — the live
        // page would render the old Gutenberg snapshot and the builder
        // editor would still show the new design. Foot-gun.
        if (function_exists('swcp_detect_page_builder')) {
            $builder = swcp_detect_page_builder($post_id);
            if (!in_array($builder, ['gutenberg', 'classic', 'unknown'], true)) {
                return new WP_Error(
                    'unsupported_builder',
                    'I cannot restore older versions of pages built with ' . ucfirst($builder) . '. Open the page in their editor to roll it back there.',
                    ['status' => 409]
                );
            }
        }

        $before_html = (string) $post->post_content;
        $update = wp_update_post([
            'ID'           => $post_id,
            'post_content' => (string) $rev->post_content,
            'post_title'   => $rev->post_title !== '' ? $rev->post_title : $post->post_title,
        ], true);
        if (is_wp_error($update)) {
            return new WP_Error('restore_failed', $update->get_error_message(), ['status' => 500]);
        }

        // Log the rollback to the cloud activity feed (best-effort, ignored
        // for free-tier sites without a license).
        if (function_exists('swcp_log_page_version')) {
            swcp_log_page_version($post_id, $before_html, (string) $rev->post_content);
        }

        return rest_ensure_response([
            'ok'        => true,
            'message'   => 'Restored that version.',
            'view_url'  => get_permalink($post_id),
            'edit_url'  => get_edit_post_link($post_id, ''),
        ]);
    }

    /**
     * GET /?swcp_render_revision=ID
     * Renders the revision's post_content inside the same no-chrome "stage"
     * template the design preview uses. Editors only. The rail iframes this
     * so users can SEE an old version before committing with Restore.
     */
    public static function maybe_render_revision_preview() {
        if (!isset($_GET['swcp_render_revision'])) return;
        $rev_id = (int) $_GET['swcp_render_revision'];
        if (!$rev_id) return;
        if (!current_user_can('edit_posts')) {
            wp_die('You need editor permissions to view a co-pilot version preview.', 'Forbidden', ['response' => 403]);
        }
        $rev = wp_get_post_revision($rev_id);
        if (!$rev) {
            wp_die('That version is not here anymore.', 'Not found', ['response' => 404]);
        }
        $parent_id = (int) $rev->post_parent;
        if (!$parent_id) {
            wp_die('That version is not linked to a page.', 'Not found', ['response' => 404]);
        }

        // Render the revision's gutenberg blocks server-side so the iframe
        // shows the actual layout, not raw block markup.
        $content = do_blocks((string) $rev->post_content);

        nocache_headers();
        header('Content-Type: text/html; charset=UTF-8');
        header('X-Frame-Options: SAMEORIGIN');

        $title = esc_html($rev->post_title !== '' ? $rev->post_title : get_the_title($parent_id));
        $locale = esc_attr(get_locale());
        $tool_styles_url = SAILWP_COPILOT_URL . 'assets/css/tool-styles.css?v=' . SAILWP_COPILOT_VERSION;
        $stamp = mysql2date(get_option('date_format') . ' \a\t ' . get_option('time_format'), $rev->post_modified);
        ?>
<!DOCTYPE html>
<html lang="<?php echo $locale; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title><?php echo $title; ?> · Version from <?php echo esc_html($stamp); ?></title>
<link rel="stylesheet" href="<?php echo esc_url($tool_styles_url); ?>">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
    min-height: 100vh;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, "Helvetica Neue", sans-serif;
    color: #3a2e24;
    background: radial-gradient(circle at 18% -10%, #fff5e8 0%, #fbf6ee 38%, #f5ecde 100%);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
body { padding: clamp(20px, 4vw, 56px) clamp(12px, 3vw, 40px); overflow-x: hidden; }
.swcp-stage {
    position: relative; z-index: 1;
    max-width: 1080px; margin: 0 auto;
    background: #ffffff; border-radius: 20px;
    box-shadow:
        0 60px 120px rgba(232, 132, 92, 0.18),
        0 20px 50px rgba(122, 81, 56, 0.12),
        0 0 0 1px rgba(122, 81, 56, 0.05);
    overflow: hidden;
    padding: clamp(32px, 6vw, 64px) clamp(20px, 5vw, 56px);
    color: #2b2018;
}
.swcp-stage > * + * { margin-top: 0.9rem; }
.swcp-stage h1 { font-size: clamp(2rem, 4vw, 2.8rem); line-height: 1.2; margin-top: 1.5rem; font-weight: 700; }
.swcp-stage h2 { font-size: clamp(1.5rem, 2.8vw, 2rem); line-height: 1.3; margin-top: 2.2rem; font-weight: 700; }
.swcp-stage h3 { font-size: clamp(1.2rem, 2.1vw, 1.5rem); line-height: 1.35; margin-top: 1.6rem; font-weight: 600; }
.swcp-stage p, .swcp-stage li { font-size: 1.05rem; line-height: 1.7; }
.swcp-stage ul, .swcp-stage ol { padding-inline-start: 1.5rem; }
.swcp-stage img { max-width: 100%; height: auto; display: block; }
.swcp-stage figure { margin: 1.5rem 0; }
.swcp-stage a { color: #c4593a; text-decoration: underline; text-underline-offset: 3px; }
.swcp-stage .wp-block-group.alignfull {
    margin-inline: calc(-1 * clamp(20px, 5vw, 56px));
    padding-inline: clamp(20px, 5vw, 56px);
}
</style>
</head>
<body>
<div class="swcp-stage">
<?php echo $content; ?>
</div>
</body>
</html>
        <?php
        exit;
    }

    /**
     * "10 seconds" → "just now"; "5 minutes" → "5 minutes ago"; etc.
     * Returns SHORT phrases so the timeline tooltip stays readable.
     */
    private static function humanise_age($seconds) {
        $seconds = max(0, (int) $seconds);
        if ($seconds < 45)      return 'just now';
        if ($seconds < 90)      return '1 minute ago';
        $minutes = (int) round($seconds / 60);
        if ($minutes < 60)      return $minutes . ' minutes ago';
        $hours = (int) round($minutes / 60);
        if ($hours === 1)       return '1 hour ago';
        if ($hours < 24)        return $hours . ' hours ago';
        $days = (int) round($hours / 24);
        if ($days === 1)        return 'yesterday';
        if ($days < 30)         return $days . ' days ago';
        $months = (int) round($days / 30);
        if ($months === 1)      return '1 month ago';
        return $months . ' months ago';
    }
}
