<?php
/**
 * Copyright (C) 2026 Plugixa (https://plugixa.com)
 * License: GNU General Public License v2 or later
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 */

namespace PlugixaRecipe\Frontend;

// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.SlowDBQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom-table data layer: table identifiers are built with $wpdb->prefix and are never user input; all request values are bound via $wpdb->prepare(); ORDER BY / LIMIT fragments come out of internal allow-list maps; content reads go through PlugixaRecipe\Cache (the static sniff cannot trace caching through the wrapper); meta_key/meta_value are dedicated plugin columns, not WP_Query.

// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- $pxr_single_recipe_data is the documented hand-off global read by templates/single-recipe.php. "pxr" is the plugin's established public prefix (CSS classes, --pxr-* custom properties, pxr_* transients) and is unique to it; the sniff only objects because it is under its 4-character minimum.

defined( 'ABSPATH' ) || exit;

use PlugixaRecipe\Cache;

/**
 * RecipeSingle Class - Handles the single recipe frontend page.
 *
 * Registers a rewrite rule for /recipe/{id}/{slug} that renders a full
 * single recipe page using the theme's header/footer and templates/single-recipe.php.
 */
class RecipeSingle {

    /**
     * Initialize single recipe page hooks.
     */
    public function init(): void {
        add_action('init', [$this, 'register_rewrite_rules']);
        add_filter('query_vars', [$this, 'register_query_vars']);
        add_filter('template_include', [$this, 'handle_single_recipe']);
    }

    /**
     * Register the single recipe rewrite rule.
     */
    public function register_rewrite_rules(): void {
        add_rewrite_rule(
            '^recipe/(\d+)(?:/([^/]+))?/?$',
            'index.php?pxr_recipe_id=$matches[1]',
            'top'
        );
    }

    /**
     * Register custom query variables.
     */
    public function register_query_vars(array $vars): array {
        $vars[] = 'pxr_recipe_id';
        return $vars;
    }

    /**
     * Handle the single recipe page via template_include.
     */
    public function handle_single_recipe(string $template): string {
        $recipe_id = get_query_var('pxr_recipe_id');
        if (empty($recipe_id)) {
            return $template;
        }

        $recipe_id = (int) $recipe_id;
        $recipe = $this->build_recipe_object($recipe_id);

        if (!$recipe) {
            return $template;
        }

        // Prevent 404
        global $wp_query;
        $wp_query->is_404 = false;
        status_header(200);

        // Set page title
        add_filter('document_title_parts', function ($parts) use ($recipe) {
            $parts['title'] = $recipe->title;
            return $parts;
        });

        // Fix body classes for block theme compatibility.
        add_filter( 'body_class', function ( $classes ) {
            $classes = array_diff( $classes, [ 'home', 'blog' ] );
            $classes[] = 'wp-singular';
            $classes[] = 'pxr-single-recipe';
            return $classes;
        } );

        // Enqueue frontend CSS
        $css_path = PLUGIXARECIPE_PATH . 'assets/frontend/build/css/frontend.css';
        if (file_exists($css_path)) {
            wp_enqueue_style(
                'plugixarecipe-frontend',
                PLUGIXARECIPE_URL . 'assets/frontend/build/css/frontend.css',
                [],
                PLUGIXARECIPE_VERSION
            );
        }

        // Enqueue frontend JS
        $js_path = PLUGIXARECIPE_PATH . 'assets/frontend/js/recipes.js';
        if (file_exists($js_path)) {
            wp_enqueue_script(
                'plugixarecipe-frontend',
                PLUGIXARECIPE_URL . 'assets/frontend/js/recipes.js',
                [],
                PLUGIXARECIPE_VERSION,
                true
            );

            wp_localize_script('plugixarecipe-frontend', 'pxrConfig', [
                'ajaxUrl'    => admin_url('admin-ajax.php'),
                'restUrl'    => esc_url_raw(rest_url('plugixarecipe/v1/')),
                'nonce'      => wp_create_nonce('pxr_frontend'),
                'restNonce'  => wp_create_nonce('wp_rest'),
                'isLoggedIn'     => is_user_logged_in(),
                'loginUrl'       => wp_login_url(get_permalink()),
                'reviewsPerPage'      => (int) ( ( (array) get_option( 'plugixarecipe_settings', [] ) )['reviews_per_page'] ?? 10 ),
                'reviewHelpfulEnabled' => ! empty( ( (array) get_option( 'plugixarecipe_settings', [] ) )['review_enable_helpful_votes'] ),
                'i18n'       => [
                    'tooltip' => [
                        'like'       => __('Like this recipe', 'plugixarecipe'),
                        'likeActive' => __('Unlike this recipe', 'plugixarecipe'),
                        'save'       => __('Save to favorites', 'plugixarecipe'),
                        'saveActive' => __('Remove from saved', 'plugixarecipe'),
                        'loginLike'  => __('Log in to like this recipe', 'plugixarecipe'),
                        'loginSave'  => __('Log in to save this recipe', 'plugixarecipe'),
                        'loginLink'  => __('Log in', 'plugixarecipe'),
                    ],
                ],
            ]);
        }

        // GLightbox for gallery lightbox (conditional).
        $recipe_arr = (array) $recipe;
        if ( ! empty( $recipe_arr['gallery_urls'] ) ) {
            wp_enqueue_style(
                'glightbox',
                PLUGIXARECIPE_URL . 'assets/frontend/vendor/glightbox/glightbox.min.css',
                [],
                '3.3.0'
            );
            wp_enqueue_script(
                'glightbox',
                PLUGIXARECIPE_URL . 'assets/frontend/vendor/glightbox/glightbox.min.js',
                [],
                '3.3.0',
                true
            );
        }

        // Build template data
        $shortcodes = new Shortcodes();
        $view_config = $this->get_default_view_config();
        $css_vars_style = $shortcodes->build_css_vars($view_config);
        $unique_id = 'pxr-single-' . $recipe_id;

        // Use a wrapper template that calls get_header/get_footer
        $GLOBALS['pxr_single_recipe_data'] = [
            'recipe'        => $recipe,
            'view_config'   => $view_config,
            'css_vars_style' => $css_vars_style,
            'unique_id'     => $unique_id,
        ];

        // Return our wrapper template
        return PLUGIXARECIPE_PATH . 'templates/single-recipe-wrapper.php';
    }

    /**
     * Build the full recipe object that the single-recipe.php template expects.
     */
    private function build_recipe_object(int $id): ?object {
        global $wpdb;
        $prefix = $wpdb->prefix . 'plugixarecipe_';

        // Read-through object cache; key = recipe id.
        $recipe = Cache::remember('recipes', 'single|' . $id, function () use ($wpdb, $prefix, $id) {
            return $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$prefix}recipes WHERE id = %d AND status = 'published'",
                $id
            ), ARRAY_A);
        });

        if (!$recipe) {
            return null;
        }

        $recipe_id = (int) $recipe['id'];

        // Image
        if (!empty($recipe['image_id'])) {
            $recipe['image_url'] = wp_get_attachment_image_url((int) $recipe['image_id'], 'large') ?: '';
            $recipe['image_alt'] = get_post_meta((int) $recipe['image_id'], '_wp_attachment_image_alt', true) ?: $recipe['title'];
        } else {
            $recipe['image_url'] = '';
            $recipe['image_alt'] = $recipe['title'];
        }

        // Author
        if (!empty($recipe['author_id'])) {
            $user = get_userdata((int) $recipe['author_id']);
            $recipe['author_name'] = $user ? $user->display_name : '';
        } else {
            $recipe['author_name'] = '';
        }

        // Published date
        $recipe['published_date'] = !empty($recipe['published_at'])
            ? wp_date(get_option('date_format'), strtotime($recipe['published_at']))
            : (!empty($recipe['created_at']) ? wp_date(get_option('date_format'), strtotime($recipe['created_at'])) : '');

        // Rating
        $recipe['rating'] = (float) ($recipe['rating_average'] ?? 0);
        $recipe['rating_count'] = (int) ($recipe['rating_count'] ?? 0);

        // Categories — child rows cached under the owning recipe.
        $recipe['categories'] = Cache::remember('recipes', 'single_categories|' . $recipe_id, function () use ($wpdb, $prefix, $recipe_id) {
            return $wpdb->get_results($wpdb->prepare(
                "SELECT c.id, c.name, c.slug FROM {$prefix}categories c
                 INNER JOIN {$prefix}recipe_categories rc ON c.id = rc.category_id
                 WHERE rc.recipe_id = %d ORDER BY rc.display_order ASC",
                $recipe_id
            ), ARRAY_A) ?: [];
        });

        // Cuisines — child rows cached under the owning recipe.
        $cuisines = Cache::remember('recipes', 'single_cuisines|' . $recipe_id, function () use ($wpdb, $prefix, $recipe_id) {
            return $wpdb->get_results($wpdb->prepare(
                "SELECT cu.id, cu.name, cu.slug FROM {$prefix}cuisines cu
                 INNER JOIN {$prefix}recipe_cuisines rcu ON cu.id = rcu.cuisine_id
                 WHERE rcu.recipe_id = %d",
                $recipe_id
            ), ARRAY_A) ?: [];
        });
        $recipe['cuisines'] = $cuisines;
        $recipe['cuisine'] = !empty($cuisines) ? $cuisines[0]['name'] : '';

        // Ingredient groups (grouped by group_name/group_order) — child rows cached under the recipe.
        $raw_ingredients = Cache::remember('recipes', 'single_ingredients|' . $recipe_id, function () use ($wpdb, $prefix, $recipe_id) {
            return $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$prefix}ingredients WHERE recipe_id = %d ORDER BY group_order ASC, sort_order ASC",
                $recipe_id
            ), ARRAY_A) ?: [];
        });
        $recipe['ingredient_groups'] = $this->group_ingredients($raw_ingredients);

        // Instruction groups (grouped by group_name/group_order) — child rows cached under the recipe.
        $raw_instructions = Cache::remember('recipes', 'single_instructions|' . $recipe_id, function () use ($wpdb, $prefix, $recipe_id) {
            return $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$prefix}instructions WHERE recipe_id = %d ORDER BY group_order ASC, sort_order ASC",
                $recipe_id
            ), ARRAY_A) ?: [];
        });
        $recipe['instruction_groups'] = $this->group_instructions($raw_instructions);

        // Nutrition — child row cached under the owning recipe.
        $nutrition_row = Cache::remember('recipes', 'single_nutrition|' . $recipe_id, function () use ($wpdb, $prefix, $recipe_id) {
            return $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$prefix}nutrition WHERE recipe_id = %d",
                $recipe_id
            ), ARRAY_A);
        });
        $recipe['nutrition'] = $nutrition_row ? (object) $nutrition_row : null;

        // Reviews (approved only, sorted by settings).
        $pxr_settings  = (array) get_option( 'plugixarecipe_settings', [] );
        $sort_key      = $pxr_settings['reviews_default_sort'] ?? 'newest';
        $sort_order_map = [
            'newest'  => 'created_at DESC',
            'oldest'  => 'created_at ASC',
            'highest' => 'rating DESC, created_at DESC',
            'lowest'  => 'rating ASC, created_at DESC',
            'helpful' => 'helpful_count DESC, created_at DESC',
        ];
        $order_clause = $sort_order_map[ $sort_key ] ?? 'created_at DESC';

        // Read-through object cache; key = recipe id + sort order (the only inputs).
        $recipe['reviews'] = Cache::remember('reviews', 'single|' . $recipe_id . '|' . $order_clause, function () use ($wpdb, $prefix, $recipe_id, $order_clause) {
            return $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$prefix}reviews WHERE recipe_id = %d AND status = 'approved' ORDER BY {$order_clause}",
                $recipe_id
            )) ?: [];
        });

        /** This filter is documented in Api\FrontendApi::get_recipe(). */
        $recipe = apply_filters('plugixarecipe_prepare_recipe_frontend', $recipe, $recipe_id);

        // Diet badges
        $recipe['diet_badges'] = [];
        if (!empty($recipe['is_vegetarian'])) $recipe['diet_badges'][] = __('Vegetarian', 'plugixarecipe');
        if (!empty($recipe['is_vegan'])) $recipe['diet_badges'][] = __('Vegan', 'plugixarecipe');
        if (!empty($recipe['is_gluten_free'])) $recipe['diet_badges'][] = __('Gluten-Free', 'plugixarecipe');
        if (!empty($recipe['is_dairy_free'])) $recipe['diet_badges'][] = __('Dairy-Free', 'plugixarecipe');
        if (!empty($recipe['is_keto'])) $recipe['diet_badges'][] = __('Keto', 'plugixarecipe');
        if (!empty($recipe['is_low_carb'])) $recipe['diet_badges'][] = __('Low-Carb', 'plugixarecipe');

        // Gallery images (decode JSON IDs → URL objects).
        $recipe['gallery_urls'] = [];
        if ( ! empty( $recipe['gallery'] ) ) {
            $gallery_ids = json_decode( $recipe['gallery'], true );
            if ( is_array( $gallery_ids ) ) {
                foreach ( $gallery_ids as $gid ) {
                    $gid       = (int) $gid;
                    $large_url = wp_get_attachment_image_url( $gid, 'large' );
                    if ( ! $large_url ) {
                        continue;
                    }
                    $recipe['gallery_urls'][] = (object) [
                        'id'        => $gid,
                        'url'       => $large_url,
                        'full'      => wp_get_attachment_image_url( $gid, 'full' ) ?: $large_url,
                        'thumbnail' => wp_get_attachment_image_url( $gid, 'thumbnail' ) ?: $large_url,
                        'alt'       => get_post_meta( $gid, '_wp_attachment_image_alt', true ) ?: ( $recipe['title'] ?? '' ),
                    ];
                }
            }
        }

        // Video embed (oEmbed for YouTube/Vimeo, <video> fallback for self-hosted).
        $recipe['video_embed'] = '';
        if ( ! empty( $recipe['video_url'] ) ) {
            $video_ext = strtolower( pathinfo( wp_parse_url( $recipe['video_url'], PHP_URL_PATH ) ?: '', PATHINFO_EXTENSION ) );
            if ( in_array( $video_ext, [ 'mp4', 'webm', 'ogg' ], true ) ) {
                $recipe['video_embed'] = sprintf(
                    '<video controls preload="metadata" playsinline><source src="%s" type="video/%s">%s</video>',
                    esc_url( $recipe['video_url'] ),
                    esc_attr( $video_ext ),
                    esc_html__( 'Your browser does not support the video tag.', 'plugixarecipe' )
                );
            } else {
                $oembed = wp_oembed_get( $recipe['video_url'], [ 'width' => 1200 ] );
                if ( $oembed ) {
                    $recipe['video_embed'] = $oembed;
                }
            }
        }

        // URLs
        $slug = sanitize_title($recipe['title'] ?? '');
        $recipe['permalink'] = home_url("/recipe/{$recipe_id}/{$slug}/");
        $recipe['print_url'] = home_url("/recipe-print/{$recipe_id}/?auto=1");

        // Favorite status (per-user state: not cached).
        $recipe['is_favorited'] = false;
        if (is_user_logged_in()) {
            $recipe['is_favorited'] = (bool) $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM {$prefix}favorites WHERE user_id = %d AND recipe_id = %d",
                get_current_user_id(), $recipe_id
            ));
        }

        // Increment view count.
        // Counter increment only: deliberately does NOT flush the cache — this runs on every
        // render, so flushing here would invalidate the whole cache on every page view. Cached
        // lists therefore carry eventually-consistent view counts.
        $wpdb->query($wpdb->prepare(
            "UPDATE {$prefix}recipes SET view_count = view_count + 1 WHERE id = %d",
            $recipe_id
        ));

        return (object) $recipe;
    }

    /**
     * Group flat ingredient rows into grouped structure with objects.
     *
     * Returns array of objects: { name: string, items: [ { amount, unit, name, text, notes } ] }
     */
    private function group_ingredients(array $rows): array {
        $groups = [];
        $current_group = null;

        foreach ($rows as $row) {
            $group_name = $row['group_name'] ?? '';

            if ($current_group === null || $current_group->name !== $group_name) {
                $current_group = (object) [
                    'name'  => $group_name,
                    'items' => [],
                ];
                $groups[] = $current_group;
            }

            $current_group->items[] = (object) [
                'amount' => $row['quantity'] ?? '',
                'unit'   => $row['unit'] ?? '',
                'name'   => $row['name'] ?? '',
                'text'   => trim(($row['quantity'] ?? '') . ' ' . ($row['unit'] ?? '') . ' ' . ($row['name'] ?? '')),
                'notes'  => $row['notes'] ?? '',
            ];
        }

        return $groups;
    }

    /**
     * Group flat instruction rows into grouped structure with objects.
     *
     * Returns array of objects: { name: string, steps: [ { text, image_url, tip } ] }
     */
    private function group_instructions(array $rows): array {
        $groups = [];
        $current_group = null;

        foreach ($rows as $row) {
            $group_name = $row['group_name'] ?? '';

            if ($current_group === null || $current_group->name !== $group_name) {
                $current_group = (object) [
                    'name'  => $group_name,
                    'steps' => [],
                ];
                $groups[] = $current_group;
            }

            $image_url = '';
            if (!empty($row['image_id'])) {
                $image_url = wp_get_attachment_image_url((int) $row['image_id'], 'medium') ?: '';
            }

            $current_group->steps[] = (object) [
                'text'      => $row['instruction_text'] ?? '',
                'image_url' => $image_url,
                'tip'       => $row['tip'] ?? '',
            ];
        }

        return $groups;
    }

    /**
     * Get default view config from the database.
     */
    private function get_default_view_config(): array {
        global $wpdb;
        $table = $wpdb->prefix . 'plugixarecipe_views';

        $row = $wpdb->get_row("SELECT config FROM {$table} WHERE is_default = 1 LIMIT 1");
        if (!$row) {
            $row = $wpdb->get_row("SELECT config FROM {$table} ORDER BY id ASC LIMIT 1");
        }

        if ($row && !empty($row->config)) {
            $config = json_decode($row->config, true);
            return is_array($config) ? $config : [];
        }

        return [];
    }

}
