<?php
defined( 'ABSPATH' ) || exit;

class Jalpi_API {

    private static $instance = null;
    const NAMESPACE = 'jalpi-page-builder/v1';

    public static function instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        add_action( 'rest_api_init', [ $this, 'register_routes' ] );
    }

    // -------------------------------------------------------------------------
    // Route registration
    // -------------------------------------------------------------------------

    public function register_routes() {
        register_rest_route( self::NAMESPACE, '/page/(?P<post_id>\d+)', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_page' ],
                'permission_callback' => [ $this, 'edit_permission' ],
                'args'                => [
                    'post_id' => [
                        'required'          => true,
                        'type'              => 'integer',
                        'minimum'           => 1,
                        'sanitize_callback' => 'absint',
                        'validate_callback' => [ $this, 'validate_post_id' ],
                    ],
                ],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'save_page' ],
                'permission_callback' => [ $this, 'edit_permission' ],
                'args'                => [
                    'post_id' => [
                        'required'          => true,
                        'type'              => 'integer',
                        'minimum'           => 1,
                        'sanitize_callback' => 'absint',
                        'validate_callback' => [ $this, 'validate_post_id' ],
                    ],
                    'page_data' => [
                        'required'          => true,
                        'type'              => 'object',
                        'validate_callback' => [ $this, 'validate_page_data' ],
                    ],
                ],
            ],
        ] );

        register_rest_route( self::NAMESPACE, '/page/(?P<post_id>\d+)/publish', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [ $this, 'publish_page' ],
            'permission_callback' => [ $this, 'publish_permission' ],
            'args'                => [
                'post_id' => [
                    'required'          => true,
                    'type'              => 'integer',
                    'minimum'           => 1,
                    'sanitize_callback' => 'absint',
                    'validate_callback' => [ $this, 'validate_post_id' ],
                ],
            ],
        ] );

        register_rest_route( self::NAMESPACE, '/page/(?P<post_id>\d+)/history', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [ $this, 'get_history' ],
            'permission_callback' => [ $this, 'edit_permission' ],
            'args'                => [
                'post_id' => [
                    'required'          => true,
                    'type'              => 'integer',
                    'minimum'           => 1,
                    'sanitize_callback' => 'absint',
                ],
            ],
        ] );

        register_rest_route( self::NAMESPACE, '/dynamic-tags', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [ $this, 'get_dynamic_tags' ],
            'permission_callback' => [ $this, 'edit_permission' ],
            'args'                => [
                'post_id' => [
                    'required'          => false,
                    'type'              => 'integer',
                    'default'           => 0,
                    'sanitize_callback' => 'absint',
                ],
            ],
        ] );

        // Nav menus available to the Menu widget's source picker.
        register_rest_route( self::NAMESPACE, '/nav-menus', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [ $this, 'get_nav_menus' ],
            'permission_callback' => [ $this, 'edit_permission' ],
        ] );

        register_rest_route( self::NAMESPACE, '/design-system', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_design_system' ],
                'permission_callback' => [ $this, 'design_system_permission' ],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'save_design_system' ],
                'permission_callback' => [ $this, 'design_system_permission' ],
                'args'                => [
                    'design_system' => [
                        'required'          => true,
                        'type'              => 'object',
                        'validate_callback' => fn( $v ) => is_array( $v ) || is_object( $v ),
                    ],
                ],
            ],
        ] );

        register_rest_route( self::NAMESPACE, '/media/upload', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [ $this, 'upload_media' ],
            'permission_callback' => [ $this, 'upload_permission' ],
        ] );

        register_rest_route( self::NAMESPACE, '/media', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [ $this, 'get_media' ],
            'permission_callback' => [ $this, 'edit_permission' ],
        ] );

        register_rest_route( self::NAMESPACE, '/import-website', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [ $this, 'import_website' ],
            'permission_callback' => function () {
                return current_user_can( 'manage_options' );
            },
            'args'                => [
                'website_id' => [
                    'required'          => true,
                    'type'              => 'string',
                    'sanitize_callback' => 'sanitize_key',
                ],
            ],
        ] );
    }

    // -------------------------------------------------------------------------
    // Permission callbacks
    // -------------------------------------------------------------------------

    /**
     * Require the user to be able to edit the specific post.
     * The WordPress REST framework validates the X-WP-Nonce header before this
     * callback ever runs, so CSRF is already covered at the framework level.
     */
    public function edit_permission( WP_REST_Request $request ): bool {
        if ( ! is_user_logged_in() ) {
            return false;
        }
        $post_id = absint( $request->get_param( 'post_id' ) );
        if ( $post_id ) {
            return current_user_can( 'edit_post', $post_id );
        }
        return current_user_can( 'edit_posts' );
    }

    /**
     * Publishing requires the 'publish_posts' capability for the post's type.
     */
    public function publish_permission( WP_REST_Request $request ): bool {
        if ( ! is_user_logged_in() ) {
            return false;
        }
        $post_id = absint( $request->get_param( 'post_id' ) );
        if ( ! $post_id ) {
            return false;
        }
        // Verify user can edit this specific post
        if ( ! current_user_can( 'edit_post', $post_id ) ) {
            return false;
        }
        // Check post-type-specific publish capability
        $post = get_post( $post_id );
        if ( ! $post ) {
            return false;
        }
        $post_type = get_post_type_object( $post->post_type );
        $publish_cap = $post_type->cap->publish_posts ?? 'publish_posts';
        return current_user_can( $publish_cap );
    }

    /**
     * The site-wide Design System affects every page, not just the one the
     * current user happens to be editing — unlike edit_permission() (which
     * accepts anyone who can edit_posts), this is gated behind a dedicated,
     * filterable capability defaulting to manage_options (matching the
     * existing admin-only Settings → Global Design tab). Sites that want
     * Authors/Editors to manage the Design System too can loosen this via
     * the jalpi_design_system_capability filter.
     */
    public function design_system_permission(): bool {
        if ( ! is_user_logged_in() ) {
            return false;
        }
        $capability = apply_filters( 'jalpi_design_system_capability', 'manage_options' );
        return current_user_can( $capability );
    }

    /**
     * Media uploads require upload_files capability.
     */
    public function upload_permission(): bool {
        return is_user_logged_in() && current_user_can( 'upload_files' );
    }

    // -------------------------------------------------------------------------
    // Argument validators
    // -------------------------------------------------------------------------

    public function validate_post_id( $value ): bool {
        $id = absint( $value );
        if ( $id < 1 ) {
            return false;
        }
        // Confirm the post exists and is an allowed type.
        $post = get_post( $id );
        if ( ! $post ) {
            return false;
        }
        $allowed_types  = (array) get_option( 'jalpi_enabled_post_types', [ 'page', 'post' ] );
        $internal_types = (array) apply_filters( 'jalpi_internal_editor_post_types', [] );
        return in_array( $post->post_type, array_merge( $allowed_types, $internal_types ), true );
    }

    public function validate_page_data( $value ): bool {
        if ( ! is_array( $value ) && ! is_object( $value ) ) {
            return false;
        }
        // Must contain a sections key
        $data = (array) $value;
        return isset( $data['sections'] ) || array_key_exists( 'sections', $data );
    }

    // -------------------------------------------------------------------------
    // Route callbacks
    // -------------------------------------------------------------------------

    public function get_page( WP_REST_Request $request ): WP_REST_Response {
        $post_id = absint( $request->get_param( 'post_id' ) );
        $data    = $this->load_page_data( $post_id );
        $post    = get_post( $post_id );

        return rest_ensure_response( [
            'post_id'     => $post_id,
            'post_title'  => $post ? sanitize_text_field( $post->post_title ) : '',
            'post_status' => $post ? sanitize_key( $post->post_status )       : '',
            'page_data'   => $data,
        ] );
    }

    public function get_design_system( WP_REST_Request $request ): WP_REST_Response {
        return rest_ensure_response( Jalpi_Settings::get_design_system() );
    }

    public function save_design_system( WP_REST_Request $request ) {
        $raw   = (array) $request->get_param( 'design_system' );
        $clean = Jalpi_Settings::update_design_system( $raw );

        // Verify by reading the option back rather than trusting update_option()'s
        // return value, which is also `false` on a legitimate no-op save (new
        // value === old value) — that ambiguity makes it unsuitable as an error
        // signal on its own.
        if ( Jalpi_Settings::get_design_system() !== $clean ) {
            return new WP_Error( 'db_error', __( 'Failed to save the Design System.', 'jalpi-page-builder' ), [ 'status' => 500 ] );
        }

        return rest_ensure_response( [ 'success' => true, 'design_system' => $clean ] );
    }

    public function save_page( WP_REST_Request $request ) {
        global $wpdb;

        $post_id   = absint( $request->get_param( 'post_id' ) );
        $raw_data  = $request->get_param( 'page_data' );

        // Sanitize every node in the tree before touching the database.
        $page_data = Jalpi_Sanitizer::sanitize_page_data( (array) $raw_data );

        $json = wp_json_encode( $page_data );
        if ( false === $json ) {
            return new WP_Error( 'encode_failed', __( 'Could not encode page data.', 'jalpi-page-builder' ), [ 'status' => 500 ] );
        }

        $table    = esc_sql( $wpdb->prefix . 'jalpi_pages' );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $existing = $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM `{$table}` WHERE post_id = %d LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $post_id
        ) );

        if ( $existing ) {
            // Single atomic query — no race condition on version counter.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $result = $wpdb->query( $wpdb->prepare(
                "UPDATE `{$table}` SET page_data = %s, version = version + 1, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $json,
                current_time( 'mysql' ),
                $post_id
            ) );
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $result = $wpdb->insert(
                $table,
                [
                    'post_id'   => $post_id,
                    'page_data' => $json,
                    'version'   => 1,
                ],
                [ '%d', '%s', '%d' ]
            );
        }

        if ( false === $result ) {
            return new WP_Error( 'db_error', __( 'Failed to save page data.', 'jalpi-page-builder' ), [ 'status' => 500 ] );
        }

        // Set the meta flag so other hooks can detect Jalpi pages without a DB query.
        update_post_meta( $post_id, '_jalpi_has_canvas_data', '1' );

        $this->save_history_snapshot( $post_id, $page_data );

        // Invalidate the render cache so dynamic tags reflect the latest data.
        delete_transient( Jalpi_Renderer::instance()->cache_key( $post_id ) );

        return rest_ensure_response( [ 'success' => true, 'post_id' => $post_id ] );
    }

    public function publish_page( WP_REST_Request $request ) {
        $post_id = absint( $request->get_param( 'post_id' ) );
        $post    = get_post( $post_id );

        if ( ! $post ) {
            return new WP_Error( 'invalid_post', __( 'Post not found.', 'jalpi-page-builder' ), [ 'status' => 404 ] );
        }

        // Guard: do not allow publishing posts that belong to others unless the
        // current user has edit_others_posts.
        if ( (int) $post->post_author !== get_current_user_id()
             && ! current_user_can( 'edit_others_posts' ) ) {
            return new WP_Error( 'rest_forbidden', __( 'You cannot publish this post.', 'jalpi-page-builder' ), [ 'status' => 403 ] );
        }

        $result = wp_update_post(
            [
                'ID'          => $post_id,
                'post_status' => 'publish',
            ],
            true
        );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        // Write rendered HTML into post_content for SEO fallback.
        $page_data   = $this->load_page_data( $post_id );
        $render_warn = '';
        if ( $page_data ) {
            try {
                $html = Jalpi_Renderer::instance()->render_to_string( $page_data, $post_id );
                wp_update_post( [
                    'ID'           => $post_id,
                    'post_content' => $html,
                ] );
            } catch ( \Throwable $e ) {
                // Publish succeeds but SEO fallback HTML could not be generated.
                $render_warn = $e->getMessage();
                update_post_meta( $post_id, '_jalpi_render_error', $render_warn );
            }
        }

        // Invalidate render cache for the published post.
        delete_transient( Jalpi_Renderer::instance()->cache_key( $post_id ) );

        $response = [
            'success'   => true,
            'post_id'   => $post_id,
            'permalink' => esc_url_raw( get_permalink( $post_id ) ),
        ];
        if ( $render_warn ) {
            $response['render_warning'] = sanitize_text_field( $render_warn );
        }
        return rest_ensure_response( $response );
    }

    /**
     * List the site's nav menus for the Menu widget's source picker.
     *
     * Returns only id/name/count — the editor needs enough to populate a
     * dropdown, and the actual items are resolved server-side at render time so
     * the markup always reflects the current menu.
     */
    public function get_nav_menus(): WP_REST_Response {
        $out = [];

        // Every menu the site has, exactly as Appearance → Menus lists them.
        // Deliberately NOT filtered by website kit: the widget mirrors the
        // native WordPress menu system, so a menu created, renamed or deleted
        // there is reflected here immediately. Kit ownership is still recorded
        // in the _jalpi_kit term meta for other features, but it does not
        // affect this list.
        foreach ( wp_get_nav_menus() as $menu ) {

            // Top-level labels are included so the editor canvas can show a
            // real preview of the chosen menu instead of just its name. Only
            // labels and nesting are sent — the published markup is always
            // rebuilt server-side from the live menu.
            $items = (array) wp_get_nav_menu_items( $menu->term_id );

            // Single pass: collect top-level entries keyed by id, and record
            // which ids are referenced as a parent.
            $tops       = [];
            $has_child  = [];
            foreach ( $items as $item ) {
                $parent = (int) $item->menu_item_parent;
                if ( 0 === $parent ) {
                    $tops[ (int) $item->ID ] = $item->title;
                } else {
                    $has_child[ $parent ] = true;
                }
            }

            $labels = [];
            foreach ( $tops as $id => $title ) {
                $labels[] = [
                    // The menu-item id is what the Advanced Menu keys its
                    // mega-panel map on, so a panel stays attached to its item
                    // when the menu is reordered in Appearance → Menus.
                    'id'          => (int) $id,
                    'label'       => $title,
                    'hasChildren' => isset( $has_child[ $id ] ),
                ];
            }

            $out[] = [
                'id'    => (int) $menu->term_id,
                'name'  => $menu->name,
                'slug'  => $menu->slug,
                'count' => (int) $menu->count,
                // Capped only to bound the response. The Advanced Menu lists
                // these to choose which items get a mega panel, so the limit
                // has to comfortably exceed a realistic top-level menu.
                'items' => array_slice( $labels, 0, 60 ),
            ];
        }

        // Registered theme locations, so a menu can follow the theme's slot
        // rather than being pinned to one menu by id.
        $locations = [];
        $assigned  = get_theme_mod( 'nav_menu_locations', [] );
        foreach ( get_registered_nav_menus() as $slug => $label ) {
            $locations[] = [
                'slug'    => $slug,
                'label'   => $label,
                'menu_id' => isset( $assigned[ $slug ] ) ? (int) $assigned[ $slug ] : 0,
            ];
        }

        return new WP_REST_Response( [
            'menus'     => $out,
            'locations' => $locations,
        ], 200 );
    }

    public function get_dynamic_tags( WP_REST_Request $request ): WP_REST_Response {
        $post_id = absint( $request->get_param( 'post_id' ) );
        $tags    = Jalpi_Dynamic_Tags::get_available_tags( $post_id );
        return rest_ensure_response( array_values( $tags ) );
    }

    public function get_history( WP_REST_Request $request ): WP_REST_Response {
        $post_id = absint( $request->get_param( 'post_id' ) );
        $history = get_post_meta( $post_id, '_jalpi_history', true );
        $history = is_array( $history ) ? $history : [];

        // Strip raw page_data from history entries to keep the response lean
        // and avoid sending unsanitized historical data.
        $safe = array_map( function ( $entry ) {
            return [
                'timestamp' => sanitize_text_field( $entry['timestamp'] ?? '' ),
                'label'     => sanitize_text_field( $entry['label']     ?? '' ),
            ];
        }, $history );

        return rest_ensure_response( $safe );
    }

    public function upload_media( WP_REST_Request $request ) {
        if ( ! function_exists( 'wp_handle_upload' ) ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            require_once ABSPATH . 'wp-admin/includes/image.php';
            require_once ABSPATH . 'wp-admin/includes/media.php';
        }

        $files = $request->get_file_params();
        if ( empty( $files['file'] ) ) {
            return new WP_Error( 'no_file', __( 'No file uploaded.', 'jalpi-page-builder' ), [ 'status' => 400 ] );
        }

        // Validate MIME type before handing to WP's uploader.
        $allowed_mimes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif' ];
        $finfo = new finfo( FILEINFO_MIME_TYPE );
        $detected_mime = $finfo->file( $files['file']['tmp_name'] );
        if ( ! in_array( $detected_mime, $allowed_mimes, true ) ) {
            return new WP_Error( 'invalid_file_type', __( 'Only image files are allowed.', 'jalpi-page-builder' ), [ 'status' => 400 ] );
        }

        $attachment_id = media_handle_upload( 'file', 0 );
        if ( is_wp_error( $attachment_id ) ) {
            return $attachment_id;
        }

        return rest_ensure_response( [
            'id'  => absint( $attachment_id ),
            'url' => esc_url_raw( wp_get_attachment_url( $attachment_id ) ),
            'alt' => sanitize_text_field( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ),
        ] );
    }

    public function get_media( WP_REST_Request $request ): WP_REST_Response {
        $query = new WP_Query( [
            'post_type'      => 'attachment',
            'post_status'    => 'inherit',
            'posts_per_page' => 40,
            'orderby'        => 'date',
            'order'          => 'DESC',
        ] );

        $items = [];
        foreach ( $query->posts as $post ) {
            $items[] = [
                'id'    => absint( $post->ID ),
                'url'   => esc_url_raw( wp_get_attachment_url( $post->ID ) ),
                'alt'   => sanitize_text_field( get_post_meta( $post->ID, '_wp_attachment_image_alt', true ) ),
                'title' => sanitize_text_field( $post->post_title ),
            ];
        }

        return rest_ensure_response( $items );
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    /**
     * Load stored page data for a post. Returns default scaffold if none saved yet.
     *
     * @param  int   $post_id
     * @return array
     */
    public function load_page_data( int $post_id ): array {
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'jalpi_pages' );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $json  = $wpdb->get_var( $wpdb->prepare(
            "SELECT page_data FROM `{$table}` WHERE post_id = %d LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $post_id
        ) );
        if ( ! $json ) {
            return $this->default_page_data( $post_id );
        }
        $data = json_decode( $json, true );
        if ( ! is_array( $data ) ) {
            return $this->default_page_data( $post_id );
        }
        return $data;
    }

    private function default_page_data( int $post_id ): array {
        $post = get_post( $post_id );
        return [
            'version'  => '1.0',
            'sections' => [
                [
                    'id'       => 'section_'   . bin2hex( random_bytes( 4 ) ),
                    'type'     => 'section',
                    'settings' => [
                        'background' => [ 'type' => 'color', 'value' => '#ffffff' ],
                        'padding'    => [ 'top' => '80px', 'bottom' => '80px', 'left' => '0', 'right' => '0' ],
                    ],
                    'children' => [
                        [
                            'id'       => 'container_' . bin2hex( random_bytes( 4 ) ),
                            'type'     => 'container',
                            'settings' => [ 'columns' => 1 ],
                            'children' => [
                                [
                                    'id'      => 'heading_' . bin2hex( random_bytes( 4 ) ),
                                    'type'    => 'heading',
                                    'content' => [
                                        'text' => $post ? sanitize_text_field( $post->post_title ) : __( 'Page Title', 'jalpi-page-builder' ),
                                        'tag'  => 'h1',
                                    ],
                                    'styles'  => [
                                        'fontSize'   => '48px',
                                        'fontWeight' => '700',
                                        'color'      => '#1a1a1a',
                                        'textAlign'  => 'center',
                                        'lineHeight' => '1.2',
                                    ],
                                ],
                                [
                                    'id'      => 'text_' . bin2hex( random_bytes( 4 ) ),
                                    'type'    => 'text',
                                    'content' => [
                                        'html' => '<p>' . esc_html__( 'Start editing this page with Jalpi Page Builder.', 'jalpi-page-builder' ) . '</p>',
                                    ],
                                    'styles'  => [
                                        'fontSize'  => '18px',
                                        'color'     => '#555555',
                                        'textAlign' => 'center',
                                        'marginTop' => '20px',
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ];
    }

    // ── Starter Sites import ──────────────────────────────────────────────────

    public function import_website( WP_REST_Request $request ) {
        $website_id = sanitize_key( $request->get_param( 'website_id' ) );

        // Reject path separators to prevent directory traversal.
        if ( ! $website_id || false !== strpos( $website_id, '/' ) || false !== strpos( $website_id, '\\' ) ) {
            return new WP_Error( 'invalid_id', __( 'Invalid website ID.', 'jalpi-page-builder' ), [ 'status' => 400 ] );
        }

        $kit_file = JALPI_DIR . 'websites/' . $website_id . '.json';

        if ( ! file_exists( $kit_file ) ) {
            return new WP_Error( 'not_found', __( 'Website kit not found.', 'jalpi-page-builder' ), [ 'status' => 404 ] );
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading a local plugin JSON file; WP_Filesystem requires credentials and is not suitable here
        $kit = json_decode( file_get_contents( $kit_file ), true );
        if ( ! $kit || empty( $kit['pages'] ) ) {
            return new WP_Error( 'invalid_kit', __( 'Invalid website kit data.', 'jalpi-page-builder' ), [ 'status' => 400 ] );
        }

        // Record which kit is active so the front-end/preview can emit a
        // `jalpi-kit-<id>` body class. Kit-specific theming (e.g. the light PULSE
        // vs. dark LUXE WooCommerce card styling) is scoped to that class so two
        // kits never bleed into each other. Importing a kit replaces the site, so
        // a single active-kit id is correct.
        update_option( 'jalpi_active_kit', $website_id );

        // ── WooCommerce bootstrap (before pages) ─────────────────────────────
        // A kit may require WooCommerce (e-commerce kits). Install + activate it,
        // seed the demo catalogue, and configure the store BEFORE pages are
        // created so hybrid pages (WC shortcodes) render real products on import.
        $woo = [ 'required' => false ];
        if ( ! empty( $kit['requires_woocommerce'] ) ) {
            $woo['required'] = true;
            $ensure = Jalpi_Woo::ensure_woocommerce();
            $woo    = array_merge( $woo, $ensure );

            if ( empty( $ensure['ok'] ) ) {
                // WC could not be made ready — abort so we never import a broken store.
                return new WP_Error(
                    'woocommerce_failed',
                    sprintf(
                        /* translators: %s: underlying error message */
                        __( 'This kit requires WooCommerce, which could not be installed automatically: %s', 'jalpi-page-builder' ),
                        $ensure['message']
                    ),
                    [ 'status' => 500 ]
                );
            }

            // Retire any previous kit's demo catalogue first, so the new store
            // doesn't show a mix of two kits' products. (Drafted, never deleted.)
            $woo['retired'] = Jalpi_Woo::retire_foreign_products( $website_id );

            // Seed products + configure store.
            if ( ! empty( $kit['products'] ) && is_array( $kit['products'] ) ) {
                $woo['products'] = Jalpi_Woo::seed_products( $kit['products'] );
            }
            Jalpi_Woo::configure( $kit['woocommerce'] ?? [] );
        }

        $created   = [];
        $front_id  = 0;
        $menu_items = [];

        foreach ( $kit['pages'] as $page_def ) {
            $title    = sanitize_text_field( $page_def['title'] ?? __( 'Page', 'jalpi-page-builder' ) );
            $slug     = sanitize_title( $page_def['slug'] ?? $title );
            $is_front = ! empty( $page_def['is_front'] );

            $post_id = wp_insert_post( [
                'post_title'  => $title,
                'post_name'   => $slug ?: sanitize_title( $title ),
                'post_type'   => 'page',
                'post_status' => 'publish',
            ], true );

            if ( is_wp_error( $post_id ) ) continue;

            if ( ! empty( $page_def['canvas_data'] ) && is_array( $page_def['canvas_data'] ) ) {
                $this->write_page_data( $post_id, $page_def['canvas_data'] );
            }

            $created[]    = [
                'id'    => $post_id,
                'title' => $title,
                'slug'  => $slug ?: sanitize_title( $title ),
                'url'   => get_permalink( $post_id ),
                'edit'  => admin_url( 'admin.php?page=jalpi-page-builder-editor&post_id=' . $post_id ),
            ];
            $menu_items[] = [ 'post_id' => $post_id, 'title' => $title ];

            if ( $is_front ) $front_id = $post_id;
        }

        // ── Wire WooCommerce page assignments to the kit's own pages ─────────
        // So /shop/ is the kit's designed Shop (with real products via shortcode)
        // and cart/checkout/account use the kit's pages, not WC's auto-created
        // ones (which may collide on the same slug). Only when WC is active.
        if ( ! empty( $woo['required'] ) && Jalpi_Woo::is_active() ) {
            $by_slug = [];
            foreach ( $created as $c ) {
                $by_slug[ $c['slug'] ] = $c['id'];
            }

            // Resolve slug collisions: the kit's own Shop/Cart pages must own the
            // clean slugs (/shop/, /cart/). WC's auto-created shop archive page
            // gets moved to /store/ so it stops shadowing the kit's designed page.
            if ( ! empty( $by_slug['shop'] ) ) {
                $wc_shop_id = (int) get_option( 'woocommerce_shop_page_id' );
                if ( $wc_shop_id && $wc_shop_id !== (int) $by_slug['shop'] ) {
                    wp_update_post( [ 'ID' => $wc_shop_id, 'post_name' => 'store' ] );
                }
                // Ensure the kit's Shop page actually holds the /shop/ slug.
                if ( get_post_field( 'post_name', (int) $by_slug['shop'] ) !== 'shop' ) {
                    wp_update_post( [ 'ID' => (int) $by_slug['shop'], 'post_name' => 'shop' ] );
                }
            }
            // NOTE: do NOT point woocommerce_shop_page_id at the kit's Shop page —
            // WC would then render it as the product ARCHIVE and discard the kit's
            // designed content + [products] shortcode. The kit's /shop/ stays a
            // normal page (hero + shortcode); WC keeps its own separate shop page.
            $map = [
                'cart'     => 'woocommerce_cart_page_id',
                'checkout' => 'woocommerce_checkout_page_id',
                'account'  => 'woocommerce_myaccount_page_id',
            ];
            foreach ( $map as $slug => $option ) {
                if ( ! empty( $by_slug[ $slug ] ) ) {
                    update_option( $option, (int) $by_slug[ $slug ] );
                }
            }
            // Re-assert launched state + flush so the storefront is live at import end.
            Jalpi_Woo::configure( $kit['woocommerce'] ?? [] );
        }

        // ── Global templates (Header / Footer / etc.) ────────────────────────
        // A kit may ship a `templates` array. Each entry becomes a Theme Builder
        // template (jalpi_theme_tpl) so the kit's header/footer render site-wide
        // and REPLACE the active theme's header/footer (the theme-builder
        // suppresses the theme chrome when a Jalpi header/footer is active).
        $templates_created = [];
        if ( ! empty( $kit['templates'] ) && is_array( $kit['templates'] ) ) {
            $templates_created = $this->create_website_templates( $kit['templates'], $kit['name'] ?? 'Kit' );
        }

        // Set front page
        if ( $front_id ) {
            update_option( 'page_on_front', $front_id );
            update_option( 'show_on_front', 'page' );
        }

        // Create navigation menu
        if ( ! empty( $menu_items ) ) {
            $menu_name = sanitize_text_field( $kit['menu_name'] ?? ( $kit['name'] . ' Menu' ) );
            $menu_id   = wp_create_nav_menu( $menu_name );
            if ( ! is_wp_error( $menu_id ) ) {
                // Record which website kit created this menu. The Menu widget
                // filters its picker on this meta, so a kit's menus never leak
                // into another kit's templates. Matching on the menu NAME would
                // break the moment someone renames it.
                update_term_meta( $menu_id, '_jalpi_kit', $website_id );

                foreach ( $menu_items as $item ) {
                    wp_update_nav_menu_item( $menu_id, 0, [
                        'menu-item-title'     => $item['title'],
                        'menu-item-object'    => 'page',
                        'menu-item-object-id' => $item['post_id'],
                        'menu-item-type'      => 'post_type',
                        'menu-item-status'    => 'publish',
                    ] );
                }
                // Assign the menu to the theme's primary nav location.
                //
                // Kit headers ship with menuLocation 'primary', so assigning to
                // whatever location happened to be registered FIRST would leave
                // the two pointing at different slots on any theme that does not
                // list primary first — the menu would then resolve nothing and
                // the header would render without navigation. Prefer the
                // conventional slugs, and fall back to the first registered one
                // so a theme using none of them still gets the menu.
                $locs = get_registered_nav_menus();
                if ( ! empty( $locs ) ) {
                    $loc_key = null;
                    foreach ( [ 'primary', 'main', 'menu-1', 'header', 'top' ] as $preferred ) {
                        if ( isset( $locs[ $preferred ] ) ) {
                            $loc_key = $preferred;
                            break;
                        }
                    }
                    $loc_key ??= array_key_first( $locs );

                    $assigned             = get_theme_mod( 'nav_menu_locations', [] );
                    $assigned[ $loc_key ] = $menu_id;
                    set_theme_mod( 'nav_menu_locations', $assigned );

                    // Keep the kit's menu nodes pointing at the location that
                    // was actually used, so 'primary' in the JSON does not go
                    // stale on a theme that names its slot something else.
                    $this->bind_menu_to_templates( $templates_created, $menu_id, $loc_key );
                } else {
                    // No nav locations at all (most block themes). menuLocation
                    // can never resolve, so pin the real id instead — otherwise
                    // Jalpi_Element_Menu resolves no items and renders NOTHING,
                    // silently dropping the navigation from every kit header.
                    $this->bind_menu_to_templates( $templates_created, $menu_id, '' );
                }
            }
        }

        return rest_ensure_response( [
            'success'           => true,
            'pages_created'     => count( $created ),
            'pages'             => $created,
            'front_page_id'     => $front_id,
            'templates_created' => $templates_created,
            'woocommerce'       => $woo,
        ] );
    }

    /**
     * Create Theme Builder templates (header/footer/etc.) from a kit's
     * `templates` array. For every location the kit provides, any existing
     * ACTIVE template at that location is first deactivated so the kit's
     * template becomes the single active one (no stacked/duplicate headers,
     * and the active theme's own header/footer is suppressed by the
     * theme-builder when a Jalpi header/footer is active).
     *
     * Each template entry: { location, title?, canvas_data, conditions?, priority? }.
     * location must be one of the theme-builder LOCATIONS (header/footer/…).
     *
     * @return array List of { id, location, title } for created templates.
     */
    private function create_website_templates( array $templates, string $kit_name ): array {
        if ( ! class_exists( 'Jalpi_Theme_Builder' ) ) {
            return [];
        }

        $cpt          = Jalpi_Theme_Builder::CPT;
        $valid_locs   = array_keys( Jalpi_Theme_Builder::LOCATIONS );
        $created       = [];
        $deactivated   = []; // locations already cleared this run (clear once each)

        foreach ( $templates as $tpl_def ) {
            if ( ! is_array( $tpl_def ) ) {
                continue;
            }

            $location = sanitize_key( $tpl_def['location'] ?? '' );
            if ( ! $location || ! in_array( $location, $valid_locs, true ) ) {
                continue;
            }

            // Deactivate existing active templates at this location (once).
            if ( ! isset( $deactivated[ $location ] ) ) {
                $this->deactivate_templates_at_location( $cpt, $location );
                $deactivated[ $location ] = true;
            }

            $title = sanitize_text_field(
                $tpl_def['title'] ?? ( $kit_name . ' ' . ucfirst( $location ) )
            );

            $tpl_id = wp_insert_post( [
                'post_title'  => $title,
                'post_type'   => $cpt,
                'post_status' => 'publish',
            ], true );

            if ( is_wp_error( $tpl_id ) ) {
                continue;
            }

            // Store the canvas in the same jalpi_pages table pages use, so the
            // theme-builder's render_template()/load_page_data() reads it back.
            if ( ! empty( $tpl_def['canvas_data'] ) && is_array( $tpl_def['canvas_data'] ) ) {
                $this->write_page_data( $tpl_id, $tpl_def['canvas_data'] );
            }

            // Conditions default to entire-site so the header/footer shows everywhere.
            $conditions = $tpl_def['conditions'] ?? [ [ 'type' => 'entire_site', 'operator' => 'is', 'value' => '' ] ];
            $priority   = isset( $tpl_def['priority'] ) ? (int) $tpl_def['priority'] : 10;

            update_post_meta( $tpl_id, '_jalpi_tpl_location',   $location );
            update_post_meta( $tpl_id, '_jalpi_tpl_status',     'active' );
            update_post_meta( $tpl_id, '_jalpi_tpl_priority',   $priority );
            update_post_meta( $tpl_id, '_jalpi_tpl_conditions', wp_json_encode( $conditions ) );
            update_post_meta( $tpl_id, '_jalpi_has_canvas_data', '1' );

            $created[] = [ 'id' => $tpl_id, 'location' => $location, 'title' => $title ];
        }

        return $created;
    }

    /**
     * Flip every currently-active template at a location to inactive, so a newly
     * imported kit template becomes the single active one at that location.
     */
    private function deactivate_templates_at_location( string $cpt, string $location ): void {
        $existing = get_posts( [
            'post_type'      => $cpt,
            'post_status'    => 'publish',
            'posts_per_page' => 50,
            'no_found_rows'  => true,
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
            'meta_query'     => [
                'relation' => 'AND',
                [ 'key' => '_jalpi_tpl_location', 'value' => $location ],
                [ 'key' => '_jalpi_tpl_status',   'value' => 'active' ],
            ],
        ] );

        foreach ( $existing as $tpl ) {
            update_post_meta( $tpl->ID, '_jalpi_tpl_status', 'inactive' );
        }
    }

    /**
     * Point every Menu / Mega-Menu node in freshly imported kit templates at the
     * menu that was just created.
     *
     * Kit templates are imported BEFORE the nav menu exists, so their nodes ship
     * with menuId 0 and a hardcoded menuLocation. Two things can go wrong:
     * the theme may name its nav slot something other than the kit's guess, or
     * register no slot at all. Either way Jalpi_Element_Menu resolves no items
     * and returns '' — the header renders with no navigation and no error.
     *
     * Writing the real id (and the location actually assigned) removes both
     * failure modes. menuLocation still wins at render time, so re-pointing the
     * theme slot later keeps working exactly as on a normal WordPress site.
     *
     * @param array<int,array> $templates Result of create_website_templates().
     * @param int              $menu_id   Term id of the nav menu just created.
     * @param string           $location  Nav location assigned, '' if none.
     */
    private function bind_menu_to_templates( array $templates, int $menu_id, string $location ): void {
        if ( $menu_id <= 0 || empty( $templates ) ) {
            return;
        }

        $bind = static function ( array &$nodes ) use ( &$bind, $menu_id, $location ): bool {
            $changed = false;
            foreach ( $nodes as &$node ) {
                $type = $node['type'] ?? '';
                if ( 'menu' === $type || 'adv-menu' === $type ) {
                    // Only fill a blank id — never overwrite a menu the kit
                    // author (or the user) deliberately pinned.
                    if ( empty( $node['content']['menuId'] ) ) {
                        $node['content']['menuId'] = $menu_id;
                        $changed                   = true;
                    }
                    // Correct a location the theme does not actually use.
                    if ( isset( $node['content']['menuLocation'] )
                        && $node['content']['menuLocation'] !== $location ) {
                        $node['content']['menuLocation'] = $location;
                        $changed                         = true;
                    }
                }
                if ( ! empty( $node['children'] ) && is_array( $node['children'] ) ) {
                    $changed = $bind( $node['children'] ) || $changed;
                }
            }
            unset( $node );
            return $changed;
        };

        foreach ( $templates as $tpl ) {
            $post_id = (int) ( $tpl['id'] ?? 0 );
            if ( ! $post_id ) {
                continue;
            }

            $data = $this->load_page_data( $post_id );
            if ( empty( $data['sections'] ) || ! is_array( $data['sections'] ) ) {
                continue;
            }

            if ( $bind( $data['sections'] ) ) {
                $this->write_page_data( $post_id, $data );
            }
        }
    }

    public function write_page_data( int $post_id, array $raw_data ): bool {
        global $wpdb;

        $page_data = Jalpi_Sanitizer::sanitize_page_data( $raw_data );
        $json      = wp_json_encode( $page_data );
        if ( false === $json ) return false;

        $table    = esc_sql( $wpdb->prefix . 'jalpi_pages' );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $existing = $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM `{$table}` WHERE post_id = %d LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $post_id
        ) );

        if ( $existing ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $result = $wpdb->update(
                $table,
                [ 'page_data' => $json, 'version' => 1, 'updated_at' => current_time( 'mysql' ) ],
                [ 'post_id' => $post_id ]
            );
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $result = $wpdb->insert(
                $table,
                [ 'post_id' => $post_id, 'page_data' => $json, 'version' => 1 ],
                [ '%d', '%s', '%d' ]
            );
        }

        if ( false !== $result ) {
            update_post_meta( $post_id, '_jalpi_has_canvas_data', '1' );
        }

        return false !== $result;
    }

    private function save_history_snapshot( int $post_id, array $page_data ): void {
        $history = get_post_meta( $post_id, '_jalpi_history', true );
        if ( ! is_array( $history ) ) {
            $history = [];
        }
        array_unshift( $history, [
            'timestamp' => current_time( 'mysql' ),
            'label'     => 'Auto-save',
            // Store only a compact snapshot — not the full tree — to keep meta lean.
            'sections'  => count( $page_data['sections'] ?? [] ),
        ] );
        update_post_meta( $post_id, '_jalpi_history', array_slice( $history, 0, 20 ) );
    }
}
