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

/**
 * Bundled template seeder for Jalpi Page Builder.
 *
 * Does NOT auto-seed on page load (avoids PHP-FPM crashes from wp_insert_post
 * side-effects on admin_init in some server configurations).
 *
 * Seeding is triggered explicitly via:
 *   POST /jalpi-page-builder/v1/templates/seed-bundled          (seeds once)
 *   POST /jalpi-page-builder/v1/templates/seed-bundled?force=1  (deletes and re-seeds)
 *
 * The TemplatePicker shows a "Load Default Templates" button in its empty state
 * which calls this endpoint.
 */
class Jalpi_Bundled_Templates {

    private static ?self $instance = null;
    private string $dir;

    /** Thumbnails attached to already-seeded templates during the current seed(). */
    private int $backfilled = 0;

    private const CATEGORY_ALIASES = [
        'call-to-action' => 'cta-blocks',
        'cta'            => 'cta-blocks',
        'hero'           => 'hero-sections',
        'landing'        => 'landing-pages',
    ];

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

    private function __construct() {
        $this->dir = JALPI_DIR . 'templates/bundled/';
        add_action( 'rest_api_init', [ $this, 'register_route' ] );
        add_action( 'admin_init',    [ $this, 'delete_orphaned_bundled' ] );
    }

    /**
     * Deletes any bundled-template DB entries whose JSON source file no longer
     * exists on disk. Runs once on admin_init via a version-keyed option so it
     * does not repeat on every page load after the first pass.
     */
    public function delete_orphaned_bundled(): void {
        $done_key = '_jalpi_orphan_cleanup_v1';
        if ( get_option( $done_key ) ) {
            return;
        }

        $files          = glob( $this->dir . '*.json' ) ?: [];
        $slugs_on_disk  = array_map( static fn( $f ) => basename( $f, '.json' ), $files );

        $all_bundled = get_posts( [
            'post_type'      => Jalpi_Templates::CPT,
            'post_status'    => 'any',
            'posts_per_page' => -1,
            'fields'         => 'ids',
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
            'meta_key'       => '_jalpi_is_bundled',
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
            'meta_value'     => '1',
        ] );

        foreach ( $all_bundled as $pid ) {
            $slug = get_post_meta( (int) $pid, '_jalpi_bundled_slug', true );
            if ( ! in_array( $slug, $slugs_on_disk, true ) ) {
                wp_delete_post( (int) $pid, true );
            }
        }

        update_option( $done_key, true );
    }

    // =========================================================================
    // REST endpoint
    // POST /jalpi-page-builder/v1/templates/seed-bundled
    // POST /jalpi-page-builder/v1/templates/seed-bundled?force=1
    // =========================================================================

    public function register_route(): void {
        register_rest_route( Jalpi_Templates::NAMESPACE, '/templates/seed-bundled', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [ $this, 'rest_seed' ],
            'permission_callback' => static fn() => is_user_logged_in() && current_user_can( 'edit_posts' ),
            'args'                => [
                'force' => [
                    'type'    => 'boolean',
                    'default' => false,
                ],
            ],
        ] );
    }

    public function rest_seed( WP_REST_Request $request ): WP_REST_Response {
        $force = (bool) $request->get_param( 'force' );
        $result = $this->seed( $force );
        return rest_ensure_response( $result );
    }

    // =========================================================================
    // Seeder
    // =========================================================================

    public function seed( bool $force = false ): array {
        if ( ! is_dir( $this->dir ) ) {
            return [ 'seeded' => 0, 'skipped' => 0, 'error' => 'Templates directory not found.' ];
        }

        $files = glob( $this->dir . '*.json' ) ?: [];
        if ( ! $files ) {
            return [ 'seeded' => 0, 'skipped' => 0 ];
        }

        $seeded = 0; $skipped = 0; $errors = [];
        $this->backfilled = 0;

        foreach ( $files as $file ) {
            $slug   = basename( $file, '.json' );
            $result = $this->seed_one( $slug, $file, $force );
            if ( true === $result )          { $seeded++; }
            elseif ( 'skipped' === $result ) { $skipped++; }
            else                             { $errors[ $slug ] = $result; }
        }

        // The editor caches its template list for an hour. Without this, a
        // freshly seeded template (or a re-seed that only changed thumbnails)
        // stays invisible in the picker until the transient happens to expire.
        if ( $seeded || $this->backfilled ) {
            delete_transient( 'jalpi_editor_tpl_list' );
        }

        // `backfilled` is reported separately from `seeded` so the editor can
        // refresh when an existing card gained a thumbnail. The template list
        // is inlined into the page at load, so without a reload the user would
        // keep seeing the placeholder even though the meta is now correct.
        $out = [
            'seeded'     => $seeded,
            'skipped'    => $skipped,
            'backfilled' => $this->backfilled,
        ];
        if ( $errors ) {
            $out['errors'] = $errors;
        }
        return $out;
    }

    private function seed_one( string $slug, string $file, bool $force ): bool|string {

        if ( $force ) {
            $existing = get_posts( [
                'post_type' => Jalpi_Templates::CPT, 'post_status' => 'any',
                'posts_per_page' => -1, 'fields' => 'ids',
                'meta_query' => [ [ 'key' => '_jalpi_bundled_slug', 'value' => $slug ] ], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
            ] );
            foreach ( $existing as $pid ) {
                wp_delete_post( (int) $pid, true );
            }
        } else {
            $existing = get_posts( [
                'post_type' => Jalpi_Templates::CPT, 'post_status' => 'publish',
                'posts_per_page' => 1, 'fields' => 'ids',
                'meta_query' => [ [ 'key' => '_jalpi_bundled_slug', 'value' => $slug ] ], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
            ] );
            if ( $existing ) {
                // Already seeded, but it may predate thumbnail support (or a
                // thumbnail may have been added to the plugin since). Backfill
                // the meta rather than leaving the card on its placeholder
                // icon — re-seeding with force would otherwise be the only way
                // to pick up an image, and that deletes and recreates the post.
                $this->backfill_thumbnail( (int) $existing[0], $slug, $file );
                return 'skipped';
            }
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading a local plugin file, WP_Filesystem not available in this context
        $raw = file_get_contents( $file );
        if ( false === $raw ) {
            return "Cannot read: {$slug}";
        }

        $json = json_decode( $raw, true );
        if ( ! is_array( $json ) || ! isset( $json['data'] ) ) {
            return "Invalid JSON: {$slug}";
        }

        $type = sanitize_key( $json['type'] ?? 'section' );
        $data = $json['data'];

        if ( 'section' === $type ) {
            if ( isset( $data['sections'][0] ) ) {
                $data = $data['sections'][0];
            } else {
                return "No section node in: {$slug}";
            }
        }

        $post_id = wp_insert_post( [
            'post_type'    => Jalpi_Templates::CPT,
            'post_status'  => 'publish',
            'post_title'   => sanitize_text_field( $json['name'] ?? $slug ),
            'post_excerpt' => sanitize_textarea_field( $json['description'] ?? '' ),
            'post_content' => wp_json_encode( $data ),
        ], true );

        if ( is_wp_error( $post_id ) ) {
            return $post_id->get_error_message();
        }

        $category = $this->resolve_category( $json['category'] ?? '' );
        update_post_meta( $post_id, '_jalpi_tpl_type',     $type );
        update_post_meta( $post_id, '_jalpi_is_bundled',   '1' );
        update_post_meta( $post_id, '_jalpi_bundled_slug', $slug );
        if ( $category ) {
            update_post_meta( $post_id, '_jalpi_tpl_category', $category );
        }

        // Card thumbnail. Without this the picker falls back to a generic
        // placeholder icon, so a bundled template gives no sense of its design.
        $thumb = $this->resolve_thumbnail( $slug, $json['thumbnail'] ?? '' );
        if ( $thumb ) {
            update_post_meta( $post_id, '_jalpi_tpl_thumbnail', $thumb );
        }

        return true;
    }

    /**
     * Attach a card thumbnail to an already-seeded template that has none.
     *
     * Only fills a missing or stale value, so it never overwrites a thumbnail a
     * user set themselves. Clears the editor's cached template list when it
     * changes something, otherwise the picker would keep serving the old row
     * for up to an hour.
     */
    private function backfill_thumbnail( int $post_id, string $slug, string $file ): void {
        if ( $post_id <= 0 ) {
            return;
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin file
        $raw      = file_get_contents( $file );
        $json     = $raw ? json_decode( $raw, true ) : null;
        $declared = is_array( $json ) ? (string) ( $json['thumbnail'] ?? '' ) : '';

        $thumb = $this->resolve_thumbnail( $slug, $declared );
        if ( '' === $thumb ) {
            return;
        }

        $current = (string) get_post_meta( $post_id, '_jalpi_tpl_thumbnail', true );
        if ( $current === $thumb ) {
            return;
        }

        update_post_meta( $post_id, '_jalpi_tpl_thumbnail', $thumb );
        $this->backfilled++;
    }

    /**
     * URL of a bundled template's card thumbnail, or '' when there is none.
     *
     * Accepts an absolute URL in the JSON's `thumbnail` key, otherwise looks for
     * `templates/bundled/thumbs/<slug>.(svg|png|jpg|webp)`. Only the basename of
     * a relative value is used, so a crafted path cannot escape the thumbs
     * directory.
     */
    private function resolve_thumbnail( string $slug, string $declared ): string {
        $declared = trim( $declared );

        if ( $declared && preg_match( '#^https?://#i', $declared ) ) {
            return esc_url_raw( $declared );
        }

        $candidates = [];
        if ( $declared ) {
            $candidates[] = basename( $declared );
        }
        foreach ( [ 'svg', 'png', 'jpg', 'jpeg', 'webp' ] as $ext ) {
            $candidates[] = $slug . '.' . $ext;
        }

        foreach ( $candidates as $file ) {
            // sanitize_file_name() also strips directory separators.
            $file = sanitize_file_name( $file );
            if ( '' === $file ) {
                continue;
            }
            if ( file_exists( $this->dir . 'thumbs/' . $file ) ) {
                return JALPI_URL . 'templates/bundled/thumbs/' . rawurlencode( $file );
            }
        }

        return '';
    }

    private function resolve_category( string $slug ): string {
        $slug = sanitize_key( $slug );
        if ( array_key_exists( $slug, Jalpi_Templates::CATEGORIES ) ) {
            return $slug;
        }
        return self::CATEGORY_ALIASES[ $slug ] ?? '';
    }
}
