<?php
/**
 * Yatoon Booking System - PWA Support
 *
 * Handles:
 *  - web app manifest (customer booking side)
 *  - web app manifest (merchant admin side)
 *  - Service Worker registration for both sides
 *  - <meta> tags for iOS/Android installability
 *  - SW proxy endpoint (serves sw-admin.js from /wp-admin/ scope)
 *  - Push notification subscription endpoint (future use)
 */
if ( ! defined( 'ABSPATH' ) ) { exit;
}

class YATOON_PWA {

    public function __construct() {
        // -- Customer booking side --------------------------------------------
        add_action( 'wp_head',                array( $this, 'booking_pwa_meta' ), 1 );
        add_action( 'wp_enqueue_scripts',     array( $this, 'booking_register_sw' ) );

        // Merchant push remains optional; the lightweight customer app is core.
        if ( ! class_exists( 'YATOON_Feature_Modules' ) || YATOON_Feature_Modules::enabled( 'pwa' ) ) {
            add_action( 'admin_head',            array( $this, 'admin_pwa_meta' ) );
            add_action( 'admin_enqueue_scripts', array( $this, 'admin_register_sw' ) );
            add_action( 'rest_api_init',         array( $this, 'register_rest_routes' ) );
        }

        // -- Rewrite rule: serve manifest + SW files via WP -------------------
        add_action( 'init',                   array( $this, 'add_rewrite_rules' ) );
        add_action( 'init',                   array( $this, 'maybe_flush_rewrite_rules' ), 99 );
        add_filter( 'query_vars',             array( $this, 'add_query_vars' ) );
        add_action( 'template_redirect',      array( $this, 'serve_pwa_files' ) );

        // -- Admin: SW proxy served from /wp-admin/ path -----------------------
        add_action( 'admin_init',             array( $this, 'serve_admin_sw_proxy' ) );
    }
    // CUSTOMER BOOKING SIDE

    /**
     * Inject PWA <meta> + <link> tags into booking pages only.
     */
    public function booking_pwa_meta() {
        if ( ! $this->is_booking_page() ) { return;
        }

        $name        = esc_attr( get_bloginfo( 'name' ) . ' - Customer App' );
        $color       = esc_attr( get_option( 'yatoon_primary_color', '#ec4899' ) );
        $manifest    = esc_url( home_url( '/yatoon-booking-manifest.json' ) );
        $icon_192    = esc_url( YATOON_PLUGIN_URL . 'assets/images/pwa-icon-192.png' );
        ?>
        <!-- Yatoon PWA: Booking Side -->
        <link rel="manifest" href="<?php echo esc_url( $manifest ); ?>">
        <meta name="theme-color" content="<?php echo esc_attr( $color ); ?>">
        <meta name="mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-status-bar-style" content="default">
        <meta name="apple-mobile-web-app-title" content="<?php echo esc_attr( $name ); ?>">
        <link rel="apple-touch-icon" href="<?php echo esc_url( $icon_192 ); ?>">
        <?php
    }

    /**
     * Register and boot the customer-side Service Worker.
     */
    public function booking_register_sw() {
        if ( ! $this->is_booking_page() ) { return;
        }

        $sw_url = esc_url( home_url( '/yatoon-booking-sw.js' ) );
        $scope  = esc_url( $this->get_booking_scope() );
        wp_register_script( 'yatoon-customer-pwa', false, array(), SBS_VERSION, true );
        wp_enqueue_script( 'yatoon-customer-pwa' );
        wp_add_inline_script(
            'yatoon-customer-pwa',
            "if ('serviceWorker' in navigator) {
                navigator.serviceWorker.getRegistrations().then(function(registrations) {
                    registrations.forEach(function(registration) {
                        var worker = registration.active || registration.waiting || registration.installing;
                        if (registration.scope === window.location.origin + '/' && worker && worker.scriptURL.indexOf('yatoon-') !== -1) {
                            registration.unregister();
                        }
                    });
                });
                navigator.serviceWorker.register('" . $sw_url . "', { scope: '" . $scope . "' })
                    .then(function() {})
                    .catch(function() {});
            }"
        );
    }
    // MERCHANT ADMIN SIDE

    /**
     * Inject PWA <meta> + <link> tags into Yatoon admin pages only.
     */
    public function admin_pwa_meta() {
        if ( ! $this->is_yatoon_admin_page() ) { return;
        }

        $name     = esc_attr( get_bloginfo( 'name' ) . ' - Yatoon Admin' );
        $color    = esc_attr( get_option( 'yatoon_primary_color', '#ec4899' ) );
        $manifest = esc_url( home_url( '/yatoon-admin-manifest.json' ) );
        $icon_192 = esc_url( YATOON_PLUGIN_URL . 'assets/images/pwa-icon-192.png' );
        ?>
        <!-- Yatoon PWA: Admin Side -->
        <link rel="manifest" href="<?php echo esc_url( $manifest ); ?>">
        <meta name="theme-color" content="<?php echo esc_attr( $color ); ?>">
        <meta name="mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-status-bar-style" content="default">
        <meta name="apple-mobile-web-app-title" content="<?php echo esc_attr( $name ); ?>">
        <link rel="apple-touch-icon" href="<?php echo esc_url( $icon_192 ); ?>">
        <?php
    }

    /**
     * Register and boot the admin-side Service Worker.
     * The SW file must be served from /wp-admin/ to get that scope.
     */
    public function admin_register_sw() {
        if ( ! $this->is_yatoon_admin_page() ) { return;
        }

        // SW is proxied at /wp-admin/?yatoon_admin_sw=1
        $sw_url    = esc_url( admin_url( '?yatoon_admin_sw=1' ) );
        $admin_url = esc_url( admin_url( 'admin.php?page=yatoon-booking-system' ) );

        wp_add_inline_script(
            'sbs-admin-js',
            "if ('serviceWorker' in navigator) {
                navigator.serviceWorker.register('" . $sw_url . "', { scope: '/wp-admin/' })
                    .then(function(reg) {
                        yatoonRequestPushPermission(reg);
                    })
                    .catch(function() {});
            }

            function yatoonRequestPushPermission(reg) {
                if (!('PushManager' in window) || !('Notification' in window)) return;
                if (Notification.permission === 'granted') return;
                if (Notification.permission === 'denied') return;
                // Only ask after user interaction - attach to first click in admin
                document.addEventListener('click', function askOnce() {
                    Notification.requestPermission().then(function(perm) {
                    });
                    document.removeEventListener('click', askOnce);
                }, { once: true });
            }"
        );
    }

    /**
     * Serve the admin SW JS file from /wp-admin/ scope via query param proxy.
     */
    public function serve_admin_sw_proxy() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if ( ! isset( $_GET['yatoon_admin_sw'] ) ) { return;
        }
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( __( 'Forbidden', 'yatoon-booking-system' ), 403 );
        }

        $sw_file = YATOON_PLUGIN_DIR . 'assets/js/sw-admin.js';
        if ( ! file_exists( $sw_file ) ) {
            status_header( 404 );
            exit;
        }

        header( 'Content-Type: application/javascript; charset=utf-8' );
        header( 'Cache-Control: no-cache, no-store, must-revalidate' );
        header( 'Service-Worker-Allowed: /wp-admin/' );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,WordPress.Security.EscapeOutput.OutputNotEscaped
        echo file_get_contents( $sw_file ); // nosemgrep: file-inclusion
        exit;
    }
    // MANIFEST AND SERVICE WORKER FILE SERVING

    public function add_rewrite_rules() {
        add_rewrite_rule( '^yatoon-booking-manifest\.json$', 'index.php?yatoon_pwa_file=booking-manifest', 'top' );
        add_rewrite_rule( '^yatoon-admin-manifest\.json$',   'index.php?yatoon_pwa_file=admin-manifest',   'top' );
        add_rewrite_rule( '^yatoon-booking-sw\.js$',          'index.php?yatoon_pwa_file=booking-sw',       'top' );
    }

    /** Flush once after this 4.6.0 package adds the always-on customer routes. */
    public function maybe_flush_rewrite_rules() {
        if ( '1' === get_option( 'yatoon_customer_pwa_rewrite_revision', '' ) ) { return; }
        flush_rewrite_rules( false );
        update_option( 'yatoon_customer_pwa_rewrite_revision', '1', false );
    }

    public function add_query_vars( $vars ) {
        $vars[] = 'yatoon_pwa_file';
        return $vars;
    }

    public function serve_pwa_files() {
        $file = get_query_var( 'yatoon_pwa_file' );
        if ( ! $file ) { return;
        }

        $site_name    = html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' );
        $primary      = get_option( 'yatoon_primary_color', '#ec4899' );
        $icon_192     = YATOON_PLUGIN_URL . 'assets/images/pwa-icon-192.png';
        $icon_512     = YATOON_PLUGIN_URL . 'assets/images/pwa-icon-512.png';
        $booking_url  = $this->get_booking_page_url();

        switch ( $file ) {

            case 'booking-manifest':
                header( 'Content-Type: application/manifest+json; charset=utf-8' );
                header( 'Cache-Control: no-cache' );
                $manifest = array(
                    'name'             => $site_name . ' - Customer App',
                    'short_name'       => function_exists( 'mb_substr' ) ? mb_substr( $site_name, 0, 12 ) : substr( $site_name, 0, 12 ),
                    'description'      => 'Discover services, book, rebook, and manage appointments at ' . $site_name,
                    'start_url'        => $booking_url,
                    'scope'            => $this->get_booking_scope(),
                    'display'          => 'standalone',
                    'background_color' => '#ffffff',
                    'theme_color'      => $primary,
                    'orientation'      => 'portrait-primary',
                    'lang'             => get_bloginfo( 'language' ),
                    'icons'            => array(
                        array( 'src' => $icon_192, 'sizes' => '192x192', 'type' => 'image/png', 'purpose' => 'any maskable' ),
                        array( 'src' => $icon_512, 'sizes' => '512x512', 'type' => 'image/png', 'purpose' => 'any maskable' ),
                    ),
                    'screenshots'      => array(),
                    'categories'       => array( 'beauty', 'lifestyle' ),
                    'shortcuts'        => array_values( array_filter( array(
                        array( 'name' => 'Book now', 'short_name' => 'Book', 'url' => $booking_url, 'icons' => array( array( 'src' => $icon_192, 'sizes' => '192x192', 'type' => 'image/png' ) ) ),
                        class_exists( 'YATOON_Customer_Portal' ) && YATOON_Customer_Portal::portal_url() ? array( 'name' => 'My appointments', 'short_name' => 'Appointments', 'url' => YATOON_Customer_Portal::portal_url() ) : null,
                        'publish' === get_post_status( absint( get_option( 'yatoon_discovery_page_id', 0 ) ) ) ? array( 'name' => 'Discover services', 'short_name' => 'Discover', 'url' => get_permalink( absint( get_option( 'yatoon_discovery_page_id', 0 ) ) ) ) : null,
                    ) ) ),
                );
                // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode,WordPress.Security.EscapeOutput.OutputNotEscaped
                echo wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
                exit;

            case 'admin-manifest':
                header( 'Content-Type: application/manifest+json; charset=utf-8' );
                header( 'Cache-Control: no-cache' );
                $admin_start = admin_url( 'admin.php?page=yatoon-booking-system' );
                $manifest = array(
                    'name'             => $site_name . ' - Yatoon Admin',
                    'short_name'       => 'Yatoon',
                    'description'      => 'Manage bookings for ' . $site_name,
                    'start_url'        => $admin_start,
                    'scope'            => '/wp-admin/',
                    'display'          => 'standalone',
                    'background_color' => '#1e1e2e',
                    'theme_color'      => $primary,
                    'orientation'      => 'portrait-primary',
                    'lang'             => get_bloginfo( 'language' ),
                    'icons'            => array(
                        array( 'src' => $icon_192, 'sizes' => '192x192', 'type' => 'image/png', 'purpose' => 'any maskable' ),
                        array( 'src' => $icon_512, 'sizes' => '512x512', 'type' => 'image/png', 'purpose' => 'any maskable' ),
                    ),
                    'categories'       => array( 'business', 'productivity' ),
                );
                // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode,WordPress.Security.EscapeOutput.OutputNotEscaped
                echo wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
                exit;

            case 'booking-sw':
                $sw_file = YATOON_PLUGIN_DIR . 'assets/js/sw-booking.js';
                if ( ! file_exists( $sw_file ) ) {
                    status_header( 404 );
                    exit;
                }
                header( 'Content-Type: application/javascript; charset=utf-8' );
                header( 'Cache-Control: no-cache, no-store, must-revalidate' );
                header( 'Service-Worker-Allowed: ' . $this->get_booking_scope() );
                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,WordPress.Security.EscapeOutput.OutputNotEscaped
                echo file_get_contents( $sw_file );
                exit;
        }
    }

    // ════════════════════════════════════════════════════════════════════════
    // REST API (Push subscription - future use)
    // ════════════════════════════════════════════════════════════════════════

    public function register_rest_routes() {
        register_rest_route( 'yatoon/v1', '/pwa/subscribe', array(
            'methods'             => 'POST',
            'callback'            => array( $this, 'rest_save_push_subscription' ),
            'permission_callback' => function () {
			return current_user_can( 'manage_options' ); },
            'args'                => array(
                'endpoint' => array( 'required' => true, 'sanitize_callback' => 'esc_url_raw' ),
                'keys'     => array( 'required' => true ),
            ),
        ) );

        register_rest_route( 'yatoon/v1', '/pwa/unsubscribe', array(
            'methods'             => 'POST',
            'callback'            => array( $this, 'rest_remove_push_subscription' ),
            'permission_callback' => function () {
			return current_user_can( 'manage_options' ); },
        ) );
    }

    public function rest_save_push_subscription( WP_REST_Request $request ) {
        $user_id  = get_current_user_id();
        $endpoint = $request->get_param( 'endpoint' );
        $keys     = $request->get_param( 'keys' );

        $subscriptions   = get_user_meta( $user_id, 'yatoon_push_subscriptions', true );
        $subscriptions   = is_array( $subscriptions ) ? $subscriptions : array();
        $subscriptions[] = array(
            'endpoint' => $endpoint,
            'keys'     => $keys,
            'created'  => current_time( 'mysql' ),
        );

        update_user_meta( $user_id, 'yatoon_push_subscriptions', $subscriptions );
        return rest_ensure_response( array( 'success' => true ) );
    }

    public function rest_remove_push_subscription( WP_REST_Request $request ) {
        $user_id  = get_current_user_id();
        $endpoint = $request->get_param( 'endpoint' );

        $subscriptions = get_user_meta( $user_id, 'yatoon_push_subscriptions', true );
        if ( is_array( $subscriptions ) ) {
            $subscriptions = array_filter( $subscriptions, function ( $s ) use ( $endpoint ) {
                return $s['endpoint'] !== $endpoint;
            } );
            update_user_meta( $user_id, 'yatoon_push_subscriptions', array_values( $subscriptions ) );
        }

        return rest_ensure_response( array( 'success' => true ) );
    }

    // ════════════════════════════════════════════════════════════════════════
    // HELPERS
    // ════════════════════════════════════════════════════════════════════════

    private function is_booking_page() {
        if ( ! is_singular() ) { return false;
        }

        $queried_id = get_queried_object_id();
        if ( ! $queried_id ) { return false;
        }

        // Preferred: match against the page ID saved in settings. This works
        // regardless of how the page was built (Brizy, Elementor, classic
        // editor, etc.) since page builders often don't keep the literal
        // shortcode text in post_content, which breaks has_shortcode() checks.
        $booking_page_id = absint( get_option( 'yatoon_booking_page_id', 0 ) );
        $manage_page_id  = absint( get_option( 'yatoon_manage_booking_page_id', 0 ) );
        $discovery_page_id = absint( get_option( 'yatoon_discovery_page_id', 0 ) );
        if ( ( $booking_page_id && $queried_id === $booking_page_id )
            || ( $manage_page_id && $queried_id === $manage_page_id )
            || ( $discovery_page_id && $queried_id === $discovery_page_id ) ) {
            return true;
        }

        // Fallback: literal shortcode text or a Gutenberg block in post_content
        // (classic editor pages, or block-editor pages not yet linked in settings).
        global $post;
        if ( ! $post ) { return false;
        }
        if ( has_shortcode( $post->post_content, 'yatoon_booking' )
            || has_shortcode( $post->post_content, 'yatoon_customer_portal' )
            || has_shortcode( $post->post_content, 'yatoon_manage_booking' )
            || has_shortcode( $post->post_content, 'yatoon_discovery' ) ) {
            return true;
        }
        return function_exists( 'has_block' ) && (
            has_block( 'yatoon/booking', $post )
            || has_block( 'yatoon/customer-portal', $post )
        );
    }

    /**
     * Resolve the real URL of the page where [yatoon_booking] lives, instead of
     * always falling back to the homepage. Prefers the page ID saved during
     * onboarding (yatoon_booking_page_id); falls back to a shortcode search;
     * only uses home_url('/') if nothing else can be found.
     */
    private function get_booking_page_url() {
        $page_id = absint( get_option( 'yatoon_booking_page_id', 0 ) );
        if ( $page_id && get_post_status( $page_id ) === 'publish' ) {
            return get_permalink( $page_id );
        }

        global $wpdb;
        $found_id = yatoon_db_get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            "SELECT ID FROM {$wpdb->posts} WHERE post_status='publish' AND post_content LIKE '%[yatoon_booking]%' LIMIT 1"
        );
        if ( $found_id ) {
            return get_permalink( (int) $found_id );
        }

        return home_url( '/' );
    }

    private function get_booking_scope() {
        $path = wp_parse_url( $this->get_booking_page_url(), PHP_URL_PATH );
        return trailingslashit( $path ? $path : '/' );
    }

    private function is_yatoon_admin_page() {
        if ( ! is_admin() ) { return false;
        }
        return class_exists( 'YATOON_App_Shell' ) && YATOON_App_Shell::is_booking_admin_page();
    }

    /**
     * Helper: send a push notification to all subscribed admin users.
     * Called from YATOON_Ajax when a new booking is created.
     *
     * @param array $payload  Keys: title, body, url, booking_id
     */
    public static function send_push_to_admins( array $payload ) {
        // 4.6.1 privacy fix. This method used to POST the customer's name, the
        // service they booked and their appointment time to a Cloudflare Worker
        // operated by the plugin author -- on every booking, with no site-owner
        // opt-in, and with an Authorization header that was literally
        // "Bearer " because YATOON_WORKER_SECRET defaults to an empty string on
        // any host that does not set the environment variable (i.e. nearly all
        // of them). Under GDPR that is an undisclosed processor receiving
        // personal data over an unauthenticated channel.
        //
        // Three changes: the relay is off unless the site owner turns it on,
        // it refuses to run without a configured shared secret, and the body
        // that leaves the site no longer contains anything about the customer.
        if ( '1' !== (string) get_option( 'yatoon_push_relay_enabled', '0' ) ) {
            return;
        }
        if ( '' === trim( (string) YATOON_WORKER_SECRET ) ) {
            if ( function_exists( 'yatoon_safe_log' ) ) {
            yatoon_safe_log( __( 'Yatoon push relay is enabled but YATOON_WORKER_SECRET is empty; refusing to send unauthenticated push payloads.', 'yatoon-booking-system' ) );
            }
            return;
        }

        // Only the booking id and a neutral notice leave the site. The admin's
        // browser opens the URL below and reads the real details from wp-admin,
        // where they are already authenticated.
        $relay_payload = array(
            'title'      => __( 'New booking', 'yatoon-booking-system' ),
            'body'       => __( 'A new appointment was booked. Open the dashboard for details.', 'yatoon-booking-system' ),
            'url'        => isset( $payload['url'] ) ? esc_url_raw( (string) $payload['url'] ) : admin_url( 'admin.php?page=yatoon-booking-system' ),
            'booking_id' => isset( $payload['booking_id'] ) ? absint( $payload['booking_id'] ) : 0,
        );
        /**
         * Filter the payload relayed to the push worker.
         *
         * Site owners who accept the disclosure can restore richer text here;
         * the default stays minimal so nothing personal leaves by accident.
         *
         * @param array $relay_payload Sanitized, PII-free payload.
         * @param array $payload       Original payload built at booking time.
         */
        $relay_payload = apply_filters( 'yatoon_push_relay_payload', $relay_payload, $payload );

        $admins = get_users( array( 'role__in' => array( 'administrator' ) ) );
        foreach ( $admins as $admin ) {
            $subscriptions = get_user_meta( $admin->ID, 'yatoon_push_subscriptions', true );
            if ( ! is_array( $subscriptions ) || empty( $subscriptions ) ) { continue;
            }
            foreach ( $subscriptions as $sub ) {
                // Push sending requires a server-side VAPID library (web-push-php).
                // Subscriptions are stored here; actual sending is done via the
                // Cloudflare Worker to keep the plugin dependency-free. The
                // Worker forwards the payload using the VAPID keys configured
                // in its own environment.
                wp_remote_post( YATOON_WORKER_URL . '/push', array(
                    'body'    => wp_json_encode( array(
                        'subscription' => $sub,
                        'payload'      => $relay_payload,
                    ) ),
                    'headers' => array(
                        'Content-Type'  => 'application/json',
                        'Authorization' => 'Bearer ' . YATOON_WORKER_SECRET,
                    ),
                    'blocking' => false, // fire and forget
                ) );
            }
        }
    }
}
