<?php
/**
 * Plugin Name: Stream Live HLS + Chat
 * Plugin URI: https://www.proxymis.com/live-streaming-no-server/
 * Description: Admin can stream live video from WP Admin. Auto-create viewer page with HLS player + polling chat. Works without external streaming services.
 * Version: 1.0.0
 * Requires at least: 5.9
 * Requires PHP: 7.4
 * Author: proxymis
 * Author URI: https://proxymis.com/
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: stream-live-hls-chat
 * Domain Path: /languages
 */

if (!defined('ABSPATH')) exit;

// Load invitation modal renderer
if (file_exists(__DIR__ . '/invitation.php')) {
    require_once __DIR__ . '/invitation.php';
}

class WP_Stream_Live {
	const CPT = 'wpsl_stream';
	const CHAT_TABLE = 'wpsl_chat';

	// Access control modes
	const ACCESS_PUBLIC   = 'public';
	const ACCESS_PASSWORD = 'password';
	const ACCESS_PAYWALL  = 'paywall';

	// Helpers for options and cookies
	private function get_option($key, $default = '') {
		$val = get_option($key);
		return ($val === false || $val === null) ? $default : $val;
	}

	private function set_cookie($name, $value, $expire = 0) {
		if (headers_sent()) return;
		$secure = is_ssl();
		$httponly = true;
		$path = COOKIEPATH ? COOKIEPATH : '/';
		$domain = COOKIE_DOMAIN ? COOKIE_DOMAIN : '';
		setcookie($name, $value, [ 'expires' => $expire, 'path' => $path, 'domain' => $domain, 'secure' => $secure, 'httponly' => $httponly, 'samesite' => 'Lax' ]);
	}

	private function pass_cookie_name($stream_id) { return 'wpsl_pass_' . intval($stream_id); }
	private function pay_cookie_name($stream_id) { return 'wpsl_pay_' . intval($stream_id); }

	private function make_pass_cookie_value($stream_id, $stored_hash) {
		return hash_hmac('sha256', $stream_id . '|' . $stored_hash, wp_salt('auth'));
	}

	private function has_password_access($stream_id) {
		$stored = get_post_meta($stream_id, '_wpsl_password_hash', true);
		if (!$stored) return false;
		$cookie = isset($_COOKIE[$this->pass_cookie_name($stream_id)]) ? (string) $_COOKIE[$this->pass_cookie_name($stream_id)] : '';
		if ($cookie === '') return false;
		return hash_equals($this->make_pass_cookie_value($stream_id, $stored), $cookie);
	}

	private function grant_password_access($stream_id) {
		$stored = get_post_meta($stream_id, '_wpsl_password_hash', true);
		if (!$stored) return false;
		$this->set_cookie($this->pass_cookie_name($stream_id), $this->make_pass_cookie_value($stream_id, $stored), time() + DAY_IN_SECONDS);
		return true;
	}

    private function has_pay_access($stream_id) {
        // Grant if cookie present
        $cookie = isset($_COOKIE[$this->pay_cookie_name($stream_id)]) ? (string) $_COOKIE[$this->pay_cookie_name($stream_id)] : '';
        if ($cookie !== '') {
            $expect = hash_hmac('sha256', 'paid|' . $stream_id, wp_salt('auth'));
            if (hash_equals($expect, $cookie)) return true;
        }
        // Grant if user owns this stream (recorded purchase)
        if (is_user_logged_in()) {
            $data = get_user_meta(get_current_user_id(), 'wpsl_purchases', true);
            if (is_array($data) && isset($data[(string)intval($stream_id)])) return true;
        }
        return false;
    }

	private function grant_pay_access($stream_id) {
		$this->set_cookie($this->pay_cookie_name($stream_id), hash_hmac('sha256', 'paid|' . $stream_id, wp_salt('auth')), time() + DAY_IN_SECONDS);
	}

    private function record_purchase($user_id, $stream_id) {
        $user_id = (int) $user_id; $stream_id = (int) $stream_id; if ($user_id <= 0 || $stream_id <= 0) return;
        $key = 'wpsl_purchases';
        $data = get_user_meta($user_id, $key, true);
        if (!is_array($data)) $data = [];
        $is_new = !isset($data[(string)$stream_id]);
        $now = time();
        // store as id => timestamp
        $data[(string)$stream_id] = $now;
        update_user_meta($user_id, $key, $data);
        // store details (ts, price at purchase time, currency)
        $detail_key = 'wpsl_purchases_detail';
        $det = get_user_meta($user_id, $detail_key, true);
        if (!is_array($det)) $det = [];
        $price_cents = (int) get_post_meta($stream_id, '_wpsl_price_cents', true);
        $currency = get_post_meta($stream_id, '_wpsl_currency', true) ?: $this->get_option('wpsl_default_currency', 'usd');
        $det[(string)$stream_id] = [ 'ts' => $now, 'price_cents' => $price_cents, 'currency' => $currency ];
        update_user_meta($user_id, $detail_key, $det);
        if ($is_new) {
            $page_id = (int) get_option('wpsl_purchases_page_id', 0);
            $link = $page_id ? get_permalink($page_id) : home_url('/my-purchase/');
            $u = get_user_by('id', $user_id);
            $to = ($u && $u->user_email) ? $u->user_email : '';
            if ($to) {
                $subj = sprintf(__('Your purchase on %s', 'stream-live-hls-chat'), wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES));
                $body = sprintf(__('Thanks for your purchase. You can access your videos anytime here: %s', 'stream-live-hls-chat'), esc_url($link));
                wp_mail($to, $subj, $body);
            }
        }
    }

	// Simple anti-flood settings (server-side)
	const CHAT_MIN_INTERVAL_SECONDS = 1.0;  // minimum delay between messages per IP per stream
	const CHAT_BURST_WINDOW_SECONDS = 10;   // burst window
	const CHAT_BURST_MAX_MESSAGES   = 6;    // max messages per window per IP per stream

    public function __construct() {
		add_action('init', [$this, 'register_cpt']);
		register_activation_hook(__FILE__, [$this, 'on_activate']);

		add_action('admin_menu', [$this, 'admin_menu']);
		add_action('admin_enqueue_scripts', [$this, 'admin_assets']);
		add_action('wp_enqueue_scripts', [$this, 'front_assets']);

		add_action('wp_ajax_wpsl_create_stream', [$this, 'ajax_create_stream']);
        add_action('wp_ajax_wpsl_create_viewer_page', [$this, 'ajax_create_viewer_page']);
        add_action('wp_ajax_wpsl_rename_stream', [$this, 'ajax_rename_stream']);
        add_action('wp_ajax_wpsl_rec_rename', [$this, 'ajax_rec_rename']);
        add_action('wp_ajax_wpsl_save_access', [$this, 'ajax_save_access']);
        add_action('wp_ajax_wpsl_get_access', [$this, 'ajax_get_access']);
        add_action('wp_ajax_wpsl_save_description', [$this, 'ajax_save_description']);
        add_action('wp_ajax_wpsl_get_description', [$this, 'ajax_get_description']);
        // Invitations
        add_action('wp_ajax_wpsl_list_users', [$this, 'ajax_list_users']);
        add_action('wp_ajax_wpsl_send_invite_preview', [$this, 'ajax_send_invite_preview']);
        add_action('wp_ajax_wpsl_send_invites', [$this, 'ajax_send_invites']);
        add_action('admin_post_wpsl_delete_recording', [$this, 'handle_delete_recording']);
        add_action('admin_post_wpsl_bulk_delete', [$this, 'handle_bulk_delete']);

        add_action('rest_api_init', [$this, 'register_rest_routes']);
        add_action('template_redirect', [$this, 'maybe_viewer_template']);
        // Ensure slugs follow stream_{ID}
        add_action('save_post_' . self::CPT, [$this, 'enforce_stream_slug'], 10, 3);
		add_filter('the_content', [$this, 'filter_stream_post_content'], 20);
        // Clean up video folder when a stream post is trashed or permanently deleted
        add_action('before_delete_post', [$this, 'maybe_delete_stream_files']);
        add_action('wp_trash_post',      [$this, 'maybe_delete_stream_files']);

		add_shortcode('wpsl_viewer', [$this, 'shortcode_viewer']);
		add_shortcode('wpsl_streams', [$this, 'shortcode_streams']);
        add_shortcode('wpsl_my_videos', [$this, 'shortcode_my_videos']);

        // Post editor: Video Access meta box for streams
        add_action('add_meta_boxes_' . self::CPT, [$this, 'add_access_metabox']);
        add_action('save_post_' . self::CPT,       [$this, 'save_access_metabox']);
	}

	public function register_cpt() {
		register_post_type(self::CPT, [
			'label'           => 'Streams',
			'public'          => true,
			'publicly_queryable' => true,
			'show_ui'         => true,
			'show_in_rest'    => true, // Gutenberg / API
			'has_archive'     => true,
			'rewrite'         => ['slug' => 'streams'],
			'supports'        => ['title', 'editor'],
			'menu_icon'       => 'dashicons-video-alt3',
		]);

	}

    public function on_activate() {
		global $wpdb;
		$table = $wpdb->prefix . self::CHAT_TABLE;
		$charset = $wpdb->get_charset_collate();
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta("CREATE TABLE {$table} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			stream_id BIGINT UNSIGNED NOT NULL,
			created_at DATETIME NOT NULL,
			user_name VARCHAR(80) NOT NULL,
			message TEXT NOT NULL,
			PRIMARY KEY (id),
			KEY stream_id (stream_id),
			KEY created_at (created_at)
		) {$charset};");

        // Ensure Videos listing page exists
        try { $this->ensure_streams_page(); } catch (\Throwable $e) {}
        // Ensure Purchases page exists
        try { $this->ensure_purchases_page(); } catch (\Throwable $e) {}
    }

    public function add_access_metabox() {
        add_meta_box(
            'wpsl_access_box',
            __('Video Access', 'stream-live-hls-chat'),
            [$this, 'render_access_metabox'],
            self::CPT,
            'side',
            'high'
        );
    }

    public function render_access_metabox($post) {
        if (!current_user_can('edit_post', $post->ID)) return;
        wp_nonce_field('wpsl_save_access_box', 'wpsl_access_box_nonce');
        $access = get_post_meta($post->ID, '_wpsl_access', true) ?: self::ACCESS_PUBLIC;
        $require_login = get_post_meta($post->ID, '_wpsl_require_login', true) === '1';
        $price_cents = (int) get_post_meta($post->ID, '_wpsl_price_cents', true);
        $currency = get_post_meta($post->ID, '_wpsl_currency', true) ?: $this->get_option('wpsl_default_currency', 'usd');
        ?>
        <p>
            <label><input type="radio" name="wpsl_access" value="public" <?php checked($access, self::ACCESS_PUBLIC); ?>/> <?php esc_html_e('Public', 'stream-live-hls-chat'); ?></label><br/>
            <label><input type="radio" name="wpsl_access" value="password" <?php checked($access, self::ACCESS_PASSWORD); ?>/> <?php esc_html_e('Password', 'stream-live-hls-chat'); ?></label><br/>
            <label><input type="radio" name="wpsl_access" value="paywall" <?php checked($access, self::ACCESS_PAYWALL); ?>/> <?php esc_html_e('Paywall', 'stream-live-hls-chat'); ?></label>
        </p>
        <p>
            <label for="wpsl_password_new"><strong><?php esc_html_e('Password', 'stream-live-hls-chat'); ?></strong></label><br/>
            <input type="password" name="wpsl_password_new" id="wpsl_password_new" class="regular-text" placeholder="<?php esc_attr_e('Set new password (leave blank to keep)', 'stream-live-hls-chat'); ?>" />
        </p>
        <p>
            <label for="wpsl_price_cents"><strong><?php esc_html_e('Price (cents)', 'stream-live-hls-chat'); ?></strong></label><br/>
            <input type="number" name="wpsl_price_cents" id="wpsl_price_cents" class="small-text" min="50" step="50" value="<?php echo (int)$price_cents; ?>" />
            <label for="wpsl_currency" style="margin-left:6px;"><strong><?php esc_html_e('Currency', 'stream-live-hls-chat'); ?></strong></label>
            <input type="text" name="wpsl_currency" id="wpsl_currency" class="small-text" value="<?php echo esc_attr($currency); ?>" />
        </p>
        <p>
            <label><input type="checkbox" name="wpsl_require_login" value="1" <?php checked($require_login, true); ?> /> <?php esc_html_e('Only logged-in users can access video', 'stream-live-hls-chat'); ?></label>
        </p>
        <p class="description"><?php esc_html_e('Choose Public, set a Password, or enable Paywall. Leave password blank to keep existing.', 'stream-live-hls-chat'); ?></p>
        <?php
    }

    public function save_access_metabox($post_id) {
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
        if (!isset($_POST['wpsl_access_box_nonce']) || !wp_verify_nonce((string)$_POST['wpsl_access_box_nonce'], 'wpsl_save_access_box')) return;
        if (!current_user_can('edit_post', $post_id)) return;

        $access = isset($_POST['wpsl_access']) ? sanitize_text_field((string)$_POST['wpsl_access']) : self::ACCESS_PUBLIC;
        if (!in_array($access, [self::ACCESS_PUBLIC, self::ACCESS_PASSWORD, self::ACCESS_PAYWALL], true)) $access = self::ACCESS_PUBLIC;
        update_post_meta($post_id, '_wpsl_access', $access);

        $require_login = isset($_POST['wpsl_require_login']) && $_POST['wpsl_require_login'] === '1' ? '1' : '0';
        update_post_meta($post_id, '_wpsl_require_login', $require_login);

        // Password: update only if provided
        if (!empty($_POST['wpsl_password_new'])) {
            $pw = (string) $_POST['wpsl_password_new'];
            $hash = wp_hash_password($pw);
            update_post_meta($post_id, '_wpsl_password_hash', $hash);
        }

        // Paywall: price + currency
        if (isset($_POST['wpsl_price_cents'])) {
            $price_cents = max(0, (int) $_POST['wpsl_price_cents']);
            update_post_meta($post_id, '_wpsl_price_cents', $price_cents);
        }
        if (isset($_POST['wpsl_currency'])) {
            $cur = sanitize_text_field((string)$_POST['wpsl_currency']);
            if ($cur === '') $cur = $this->get_option('wpsl_default_currency', 'usd');
            update_post_meta($post_id, '_wpsl_currency', $cur);
        }
    }

    private function ensure_streams_page(bool $force = false): int {
        $optPrimary = 'wpsl_videos_page_id';
        $optLegacy  = 'wpsl_streams_page_id';
        $page_id = intval(get_option($optPrimary, 0));
        if ($page_id <= 0) {
            $legacy = intval(get_option($optLegacy, 0));
            if ($legacy > 0) { $page_id = $legacy; }
        }
        $content = '[wpsl_streams per_page="12"]';
        $needs_update = false;
        if ($page_id > 0) {
            $p = get_post($page_id);
            if ($p && $p->post_type === 'page' && $p->post_status !== 'trash') {
                if ($force) { $needs_update = true; }
                elseif (strpos((string)$p->post_content, '[wpsl_streams') === false) { $needs_update = true; }
                if ($needs_update) {
                    wp_update_post([
                        'ID' => $page_id,
                        'post_content' => $content,
                        'post_status'  => 'publish',
                    ]);
                }
                // When forcing, also normalize slug/title to 'videos'
                if ($force && $p->post_name !== 'videos') {
                    wp_update_post([
                        'ID'         => $page_id,
                        'post_name'  => 'videos',
                        'post_title' => 'Videos',
                    ]);
                }
                update_option($optPrimary, $page_id);
                update_option($optLegacy, $page_id);
                return $page_id;
            }
        }

        // Try find by path 'videos' first
        $p = get_page_by_path('videos', OBJECT, 'page');
        if ($p && $p->post_type === 'page' && $p->post_status !== 'trash') {
            $page_id = (int)$p->ID;
            if ($force || strpos((string)$p->post_content, '[wpsl_streams') === false) {
                wp_update_post([
                    'ID' => $page_id,
                    'post_content' => $content,
                    'post_status'  => 'publish',
                ]);
            }
            update_option($optPrimary, $page_id);
            update_option($optLegacy, $page_id);
            return $page_id;
        }

        // If only legacy '/streams/' exists, repurpose it by renaming to 'videos'
        $p = get_page_by_path('streams', OBJECT, 'page');
        if ($p && $p->post_type === 'page' && $p->post_status !== 'trash') {
            $page_id = (int)$p->ID;
            wp_update_post([
                'ID'           => $page_id,
                'post_name'    => 'videos',
                'post_title'   => 'Videos',
                'post_content' => $content,
                'post_status'  => 'publish',
            ]);
            update_option($optPrimary, $page_id);
            update_option($optLegacy, $page_id);
            return $page_id;
        }

        // Create new page
        $page_id = wp_insert_post([
            'post_type'    => 'page',
            'post_status'  => 'publish',
            'post_title'   => 'Videos',
            'post_name'    => 'videos',
            'post_content' => $content,
        ]);
        if (!is_wp_error($page_id) && $page_id) { update_option($optPrimary, (int)$page_id); update_option($optLegacy, (int)$page_id); }
        return (int)(is_wp_error($page_id) ? 0 : $page_id);
    }

    private function ensure_purchases_page(bool $force = false): int {
        $opt = 'wpsl_purchases_page_id';
        $page_id = (int) get_option($opt, 0);
        $content = '[wpsl_my_videos]';
        if ($page_id > 0) {
            $p = get_post($page_id);
            if ($p && $p->post_type === 'page' && $p->post_status !== 'trash') {
                if ($force || strpos((string)$p->post_content, '[wpsl_my_videos') === false) {
                    wp_update_post(['ID'=>$page_id, 'post_content'=>$content, 'post_status'=>'publish']);
                }
                update_option($opt, $page_id);
                return $page_id;
            }
        }
        // try find by path
        $p = get_page_by_path('my-purchase', OBJECT, 'page');
        if ($p && $p->post_type === 'page' && $p->post_status !== 'trash') {
            $page_id = (int)$p->ID;
            if ($force || strpos((string)$p->post_content, '[wpsl_my_videos') === false) {
                wp_update_post(['ID'=>$page_id, 'post_content'=>$content, 'post_status'=>'publish']);
            }
            update_option($opt, $page_id);
            return $page_id;
        }
        // create new
        $page_id = wp_insert_post([
            'post_type'=>'page', 'post_status'=>'publish', 'post_title'=>'My Purchases', 'post_name'=>'my-purchase', 'post_content'=>$content
        ]);
        if (!is_wp_error($page_id) && $page_id) update_option($opt, (int)$page_id);
        return (int)(is_wp_error($page_id) ? 0 : $page_id);
    }

	public function admin_menu() {
		add_menu_page(
			'Stream Live',
			'Stream Live',
			'manage_options',
			'wpsl',
			[$this, 'admin_page'],
			'dashicons-video-alt3'
		);

		add_submenu_page(
			'wpsl',
			'WP Stream Live Settings',
			'Settings',
			'manage_options',
			'wpsl_settings',
			[$this, 'admin_settings_page']
		);


		// Recorded Videos now links to the CPT list table directly
		add_submenu_page(
			'wpsl',
			'Recorded Videos',
			'Recorded Videos',
			'manage_options',
			'edit.php?post_type=' . self::CPT,
			''
		);

		// Sales reports (placed after Recorded Videos)
		add_submenu_page(
			'wpsl',
			'Sale Reports',
			'Sale Reports',
			'manage_options',
			'wpsl_sales',
			[$this, 'admin_sales_page']
		);


		add_submenu_page(
			'wpsl',
			'WP Stream Live FAQ/Help',
			'FAQ/Help',
			'manage_options',
			'wpsl_faq',
			[$this, 'admin_faq_page']
		);
	}

    private function create_stream_post() {
        $stream_id = wp_insert_post([
            'post_type' => self::CPT,
            'post_status' => 'publish',
            'post_title' => 'Temporary',
        ]);
        if (is_wp_error($stream_id) || ! $stream_id) return 0;

        // Set default readable name: stream_{ID}
        wp_update_post([
            'ID' => (int)$stream_id,
            'post_title' => 'stream_' . (int)$stream_id,
            'post_name'  => 'stream_' . (int)$stream_id,
        ]);

		$token = wp_generate_password(32, false, false);
		update_post_meta($stream_id, '_wpsl_token', $token);
		return (int)$stream_id;
	}

	private function ensure_stream_dir($stream_id) {
		$upload = wp_upload_dir();
		$dir = trailingslashit($upload['basedir']) . 'wpsl/' . intval($stream_id);
		if (!is_dir($dir)) wp_mkdir_p($dir);
		return $dir;
	}

	private function stream_url_base($stream_id) {
		$upload = wp_upload_dir();
		return trailingslashit($upload['baseurl']) . 'wpsl/' . intval($stream_id);
	}

	public function admin_page() {
		if (!current_user_can('manage_options')) return;
		?>
		<div class="wrap">

			<p>
				<button class="button button-primary" id="wpsl-new-stream">Create New Live Stream</button>
			</p>

			<div id="wpsl-stream-box" style="display:none;">
				<p style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;">
					<strong>Stream ID:</strong> <span id="wpsl-stream-id"></span>
					<label style="margin-left:8px;">Name: <input type="text" id="wpsl-stream-title" value="" placeholder="stream_" style="min-width:220px;" /></label>
					<button class="button" type="button" id="wpsl-save-title">Save name</button>
					<span id="wpsl-title-status" class="description"></span>
				</p>

				<!-- Removed: Auto-create Viewer Page button -->

				<hr>

				<p><strong>Streamer</strong> (Chrome/Edge desktop, Chrome Android)</p>
				<div class="wpsl-info-panel">
					<div class="wpsl-info-row"><span class="dashicons dashicons-controls-play" aria-hidden="true"></span> Only browsers that support <code>MediaStreamTrackProcessor</code> can stream (Chrome/Edge desktop, Android Chrome). No iOS Safari and no Firefox for streaming.</div>
					<div class="wpsl-info-row"><span class="dashicons dashicons-visibility" aria-hidden="true"></span> Viewing works in all modern browsers.</div>
				</div>
				<div id="wpsl-support-error" class="wpsl-alert wpsl-alert-error" style="display:none;"></div>



				<p style="display:flex;gap:16px;align-items:center;flex-wrap:wrap;">
					<label><input type="checkbox" id="wpsl-allow-chat" checked> Allow Chat</label>
					<span id="wpsl-users-visible-wrap"><label><input type="checkbox" id="wpsl-users-visible" checked> Other users can see user list</label></span>
				</p>

				<fieldset style="margin:12px 0; padding:8px; border:1px solid #ccd0d4;">
					<legend><strong>Video access</strong></legend>
					<label><input type="radio" name="wpsl-access" id="wpsl-access-public" value="public" checked> Public</label>
					<label style="margin-left:15px;"><input type="radio" name="wpsl-access" id="wpsl-access-password" value="password"> Password</label>
					<label style="margin-left:15px;"><input type="radio" name="wpsl-access" id="wpsl-access-paywall" value="paywall"> Paywall</label>
					<p style="margin-top:8px; display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
						<label><input type="checkbox" id="wpsl-require-login"> Only logged-in users can access video</label>
						<?php if (!get_option('users_can_register')): ?>
							<label><input type="checkbox" id="wpsl-allow-register"> Allow new users to register</label>
							<span class="description">(enables WordPress registration site‑wide)</span>
						<?php endif; ?>
					</p>
					<!-- Moved: users-visible checkbox next to Allow Chat -->
					<div id="wpsl-password-row" style="margin-top:8px; display:none;">
						<input type="password" id="wpsl-password" placeholder="Set stream password" />
					</div>
					<div id="wpsl-paywall-row" style="margin-top:8px; display:none;">
						<label>Price (cents): <input type="number" id="wpsl-price-cents" min="50" step="50" value="499" style="width:100px;" /></label>
						<label style="margin-left:8px;">Currency: <input type="text" id="wpsl-price-currency" value="<?php echo esc_attr($this->get_option('wpsl_default_currency', 'usd')); ?>" style="width:70px;" /></label>
					</div>
					<p style="margin-top:8px;"><button class="button" id="wpsl-save-access" type="button">Save Access</button> <span id="wpsl-access-status"></span></p>
				</fieldset>

				<!-- Latency settings moved to Settings page -->

				<fieldset style="margin:12px 0; padding:8px; border:1px solid #ccd0d4;">
					<legend><strong>Invitations</strong></legend>
					<p style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
						<label><input type="checkbox" id="wpsl-send-invites"> Invite by email</label>
						<button type="button" class="button" id="wpsl-config-invites" style="display:none;">Add users to be invited (0)</button>
						<span id="wpsl-invite-result" class="description"></span>
					</p>
			<p class="description" style="margin-top:4px;">
						Use <em>Add users&hellip;</em> to pick recipients or type emails. Edit subject and template under <em>Stream Live &rarr; Settings &rarr; Invitation Email</em>. You can send yourself a preview from the modal. Invitations send automatically when you click <em>Start</em>.
					</p>
				</fieldset>

				<p><label><input type="checkbox" id="wpsl-save-recording"> Save recording (generate replay)</label></p>

				<p>
					<button class="button button-primary" id="start">Start</button>
					<button class="button" id="stopBtn" disabled>Stop</button>
				</p>
				<p id="wpsl-viewer-link-row" style="display:none;"><strong>Preview the live stream:</strong> <a href="#" id="wpsl-viewer-url" target="_blank" rel="noopener"></a> <button class="button" id="wpsl-copy-link" type="button">Copy Link</button></p>

				<div class="wpsl-preview" style="display:inline-block;position:relative;">
					<span id="wpsl-rec-badge" class="wpsl-rec-badge" style="display:none;position:absolute;top:8px;left:8px;background:#d32f2f;color:#fff;border-radius:12px;padding:2px 6px;font-size:11px;line-height:1;font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.2)">RE</span>
					<span id="wpsl-rec-timer" class="wpsl-rec-timer" style="display:none;position:absolute;top:8px;left:56px;background:rgba(17,17,17,.9);color:#fff;border-radius:12px;padding:2px 6px;font-size:11px;line-height:1;font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.2)">00:00</span>
					<video id="p" autoplay muted playsinline style="width:420px;background:#000"></video>
				</div>

				<p style="display:flex;gap:16px;align-items:center;flex-wrap:wrap; margin-top:8px;">
					<label>Video source
						<select id="wpsl-video-source" style="min-width:220px;"></select>
					</label>
					<label>Audio source
						<select id="wpsl-audio-source" style="min-width:220px;"></select>
					</label>
				</p>

				<fieldset style="margin:12px 0; padding:8px; border:1px solid #ccd0d4;">
					<legend><strong>Description</strong></legend>
					<p class="description">This content appears on the video post page.</p>
					<div style="max-width:820px;">
						<?php if (function_exists('wp_editor')): ?>
							<?php wp_editor(
								'',
								'wpsl_desc',
								[
									'textarea_name' => 'wpsl_desc',
									'textarea_rows' => 8,
									'media_buttons' => false,
									'tinymce'       => true,
									'quicktags'     => true,
									'editor_height' => 180,
								]
							); ?>
						<?php else: ?>
							<textarea id="wpsl_desc" rows="8" style="width:100%;"></textarea>
						<?php endif; ?>
					</div>
					<p style="margin-top:8px; display:flex; align-items:center; gap:10px;">
						<button class="button" type="button" id="wpsl-save-desc">Save Description</button>
						<span id="wpsl-desc-status" class="description"></span>
					</p>
				</fieldset>

				<p style="margin-top:10px;">
					<label><input type="checkbox" id="wpsl-debug-toggle"> Debug</label>
				</p>
				<div id="ua" class="wpsl-hidden wpsl-ua"></div>
				<pre id="log" class="wpsl-hidden wpsl-log"></pre>
			</div>
		</div>
		<script>
		(function(){
			function ready(fn){ if(document.readyState!=='loading') fn(); else document.addEventListener('DOMContentLoaded', fn); }
			ready(function(){
				const players = document.querySelectorAll('.wpsl-rec-preview');
				if (!players.length) return;
				function ensureHls(cb){
					if (window.Hls && window.Hls.isSupported && window.Hls.isSupported()) return cb();
					var s=document.createElement('script'); s.src='<?php echo esc_js( plugin_dir_url(__FILE__) . 'assets/js/hls.min.js' ); ?>'; s.onload=cb; document.head.appendChild(s);
				}
				players.forEach(function(box){
					const btn = box.querySelector('.wpsl-preview-btn');
					const video = box.querySelector('video');
					const src = box.getAttribute('data-src') || box.getAttribute('data-vod');
					let started = false; let hls = null;
					btn?.addEventListener('click', function(){
						if (started) { try { video.pause(); } catch(e){} return; }
						started = true; btn.textContent='Loading…';
						if (video.canPlayType('application/vnd.apple.mpegurl')) {
							video.src = src; video.play().catch(function(){}); btn.textContent='Preview';
						} else {
							ensureHls(function(){
								if (window.Hls && window.Hls.isSupported()) {
									hls = new window.Hls({lowLatencyMode:false});
									hls.attachMedia(video);
									hls.on(window.Hls.Events.MEDIA_ATTACHED, function(){ hls.loadSource(src); });
									hls.on(window.Hls.Events.MANIFEST_PARSED, function(){ video.play().catch(function(){}); btn.textContent='Preview'; });
								}
							});
						}
					});
				});
			});
		})();
		</script>
		<?php
		// Invitation modal (pre-fill with saved template)
		$default_tpl = '<p>Hello,</p><p>You are invited to join our live session: <strong>{{stream_title}}</strong>.</p><p>Starts at: {{start_time}}</p><p>Join here: <a href="{{viewer_url}}">{{viewer_url}}</a></p><p>See you there!</p>';
		$invite_tpl = $this->get_option('wpsl_invite_template', $default_tpl);
		if (function_exists('wpsl_render_invitation_modal')) echo wpsl_render_invitation_modal($invite_tpl);
		?>
		<?php
	}

    public function admin_settings_page() {
        if (!current_user_can('manage_options')) return;
        if (isset($_POST['wpsl_save_settings']) && isset($_POST['wpsl_settings_nonce']) && wp_verify_nonce((string)$_POST['wpsl_settings_nonce'], 'wpsl_save_settings')) {
			$pk = isset($_POST['wpsl_stripe_pk']) ? sanitize_text_field(wp_unslash($_POST['wpsl_stripe_pk'])) : '';
			$sk = isset($_POST['wpsl_stripe_sk']) ? sanitize_text_field(wp_unslash($_POST['wpsl_stripe_sk'])) : '';
			$cur = isset($_POST['wpsl_default_currency']) ? sanitize_text_field(wp_unslash($_POST['wpsl_default_currency'])) : 'usd';
			$hls_latency = isset($_POST['wpsl_hls_latency']) ? intval($_POST['wpsl_hls_latency']) : 2;
			$poll_latency = isset($_POST['wpsl_default_poll']) ? (float) $_POST['wpsl_default_poll'] : 1.5;
			$invite_subject  = isset($_POST['wpsl_invite_subject']) ? sanitize_text_field(wp_unslash($_POST['wpsl_invite_subject'])) : '';
			$invite_template = isset($_POST['wpsl_invite_template']) ? wp_kses_post(wp_unslash($_POST['wpsl_invite_template'])) : '';
			update_option('wpsl_stripe_pk', $pk);
			update_option('wpsl_stripe_sk', $sk);
			update_option('wpsl_default_currency', $cur);
			$hls_latency = max(1, min(6, $hls_latency));
			update_option('wpsl_hls_latency', $hls_latency);
			$poll_latency = max(0.5, min(5.0, $poll_latency));
			update_option('wpsl_default_poll', $poll_latency);
			if ($invite_subject !== '') update_option('wpsl_invite_subject', $invite_subject); else delete_option('wpsl_invite_subject');
			if ($invite_template !== '') update_option('wpsl_invite_template', $invite_template); else delete_option('wpsl_invite_template');
            echo '<div class="updated notice"><p>Settings saved.</p></div>';
        }

        // Create Videos Page on demand
        if (isset($_POST['wpsl_create_videos_page']) && isset($_POST['wpsl_settings_nonce']) && wp_verify_nonce((string)$_POST['wpsl_settings_nonce'], 'wpsl_save_settings')) {
            $page_id = $this->ensure_streams_page(true);
            if ($page_id) {
                $link = get_permalink($page_id);
                echo '<div class="updated notice"><p>Videos page ready: <a target="_blank" rel="noopener" href="' . esc_url($link) . '">' . esc_html($link) . '</a></p></div>';
            } else {
                echo '<div class="error notice"><p>Could not create Videos page.</p></div>';
            }
        }

        // Create Purchases Page on demand
        if (isset($_POST['wpsl_create_purchases_page']) && isset($_POST['wpsl_settings_nonce']) && wp_verify_nonce((string)$_POST['wpsl_settings_nonce'], 'wpsl_save_settings')) {
            $page_id = $this->ensure_purchases_page(true);
            if ($page_id) {
                $link = get_permalink($page_id);
                echo '<div class="updated notice"><p>Purchases page ready: <a target="_blank" rel="noopener" href="' . esc_url($link) . '">' . esc_html($link) . '</a></p></div>';
            } else {
                echo '<div class="error notice"><p>Could not create Purchases page.</p></div>';
            }
        }

		$default_tpl = '<p>Hello,</p><p>You are invited to join our live session: <strong>{{stream_title}}</strong>.</p><p>Starts at: {{start_time}}</p><p>Join here: <a href="{{viewer_url}}">{{viewer_url}}</a></p><p>See you there!</p>';
		$invite_subject_val  = $this->get_option('wpsl_invite_subject', 'Live Stream Invitation');
		$invite_template_val = $this->get_option('wpsl_invite_template', $default_tpl);
		?>
		<div class="wrap">
			<h1>WP Stream Live Settings</h1>
			<style>
			.wpsl-help{display:inline-block; margin-left:6px; color:#2271b1; position:relative; cursor:help}
			.wpsl-help::before{font-family:dashicons; content:"\f223"; font-size:16px; line-height:1; vertical-align:middle}
			.wpsl-help:hover::before{color:#135e96}
			.wpsl-help:hover::after{content:attr(data-tip); position:absolute; left:22px; top:-6px; background:#111; color:#fff; padding:8px 10px; border-radius:6px; box-shadow:0 8px 18px rgba(0,0,0,.15); width:300px; max-width:60vw; font-size:12px; line-height:1.45; z-index:9999}
			</style>
			<form method="post">
				<?php wp_nonce_field('wpsl_save_settings', 'wpsl_settings_nonce'); ?>
				<table class="form-table" role="presentation">
					<tr>
						<th scope="row"><label for="wpsl-hls-latency">HLS Latency (seconds)</label> <span class="wpsl-help" data-tip="Lower latency means faster live playback but can increase CPU/network usage and stutter. 2–4s is a good balance."></span></th>
						<td>
							<input type="number" id="wpsl-hls-latency" name="wpsl_hls_latency" class="small-text" min="1" max="6" step="1" value="<?php echo (int) get_option('wpsl_hls_latency', 2); ?>" />
							<span class="description">Lower = faster but more CPU/network and potential stutter. Recommended 2&ndash;4s.</span>
						</td>
					</tr>
					<tr>
						<th scope="row"><label for="wpsl-default-poll">Default Polling Latency (seconds)</label> <span class="wpsl-help" data-tip="How often the viewer polls for new chat messages. Lower is more responsive but increases server load."></span></th>
						<td>
							<input type="number" id="wpsl-default-poll" name="wpsl_default_poll" class="small-text" min="0.5" max="5" step="0.1" value="<?php echo esc_attr( (float) get_option('wpsl_default_poll', 1.5) ); ?>" />
							<span class="description">Chat polling interval for viewers. Lower is more responsive but adds server load.</span>
						</td>
					</tr>
					<tr>
						<th scope="row"><label for="wpsl-stripe-pk">Stripe Publishable Key</label> <span class="wpsl-help" data-tip="Use Stripe test keys while testing (prefix pk_test_) and switch to live keys (pk_live_) before launch. Find in Stripe Dashboard → Developers → API Keys."></span></th>
						<td>
							<input type="text" id="wpsl-stripe-pk" name="wpsl_stripe_pk" class="regular-text" value="<?php echo esc_attr($this->get_option('wpsl_stripe_pk')); ?>" title="Find in Stripe Dashboard &rarr; Developers &rarr; API Keys &rarr; Publishable key (starts with 'pk_')" />
							<p class="description">Stripe Dashboard &rarr; Developers &rarr; API Keys &rarr; Publishable key (starts with <code>pk_</code>).</p>
							<p class="description">Tip: Use <em>test</em> keys while testing and switch to <em>live</em> keys before launch.</p>
						</td>
					</tr>
					<tr>
						<th scope="row"><label for="wpsl-stripe-sk">Stripe Secret Key</label> <span class="wpsl-help" data-tip="Your server-side key (sk_...). No webhooks required; the plugin verifies the Checkout session on return."></span></th>
						<td>
							<input type="password" id="wpsl-stripe-sk" name="wpsl_stripe_sk" class="regular-text" value="<?php echo esc_attr($this->get_option('wpsl_stripe_sk')); ?>" title="Find in Stripe Dashboard &rarr; Developers &rarr; API Keys &rarr; Secret key (starts with 'sk_'). Use a restricted key if possible." />
							<p class="description">Stripe Dashboard &rarr; Developers &rarr; API Keys &rarr; Secret key (starts with <code>sk_</code>). Use a restricted key if possible.</p>
							<p class="description">Tip: No webhooks are required; the plugin verifies the Checkout session on return.</p>
						</td>
					</tr>
					<tr>
						<th scope="row"><label for="wpsl-default-currency">Default Currency</label> <span class="wpsl-help" data-tip="Default currency used for new paywalled streams. You can override per stream under Video access."></span></th>
						<td><input type="text" id="wpsl-default-currency" name="wpsl_default_currency" class="small-text" value="<?php echo esc_attr($this->get_option('wpsl_default_currency', 'usd')); ?>" /> <span class="description">ISO currency (e.g. usd, eur)</span>
							<p class="description">Tip: Each stream can override price and currency under Video access.</p>
						</td>
					</tr>
                <tr>
                    <th scope="row">Videos Page <span class="wpsl-help" data-tip="Public page listing your videos with [wpsl_streams]. Link it in your main menu for easy access. Owned videos show a Purchased badge and Watch now button."></span></th>
                    <td>
                        <?php $videos_page_id = intval(get_option('wpsl_videos_page_id', 0)); if (!$videos_page_id) { $videos_page_id = intval(get_option('wpsl_streams_page_id', 0)); } if ($videos_page_id && ($p = get_post($videos_page_id)) && $p->post_status !== 'trash'): ?>
                            <p>Current: <a href="<?php echo esc_url( get_permalink($videos_page_id) ); ?>" target="_blank" rel="noopener"><?php echo esc_html( get_permalink($videos_page_id) ); ?></a></p>
                        <?php else: ?>
                            <p class="description">No Videos page detected yet.</p>
                        <?php endif; ?>
                        <p><button class="button" name="wpsl_create_videos_page" value="1" type="submit">Create/Update Videos Page</button></p>
                        <p class="description">Creates a public page at <code>/videos/</code> with the shortcode <code>[wpsl_streams per_page="12"]</code>.</p>
                        <p class="description">Tips: Link this page in your main menu; logged-in users will see a “Purchased” badge on streams they own and a “Watch now” button.</p>
                    </td>
                </tr>
                <tr>
                    <th scope="row">User Purchase Page <span class="wpsl-help" data-tip="After purchase, buyers receive an email linking here. The page shows all purchased videos with posters and Watch now buttons."></span></th>
                    <td>
                        <?php $purchases_page_id = (int) get_option('wpsl_purchases_page_id', 0); ?>
                        <?php if ($purchases_page_id && ($p = get_post($purchases_page_id)) && $p->post_status !== 'trash'): ?>
                            <p>Current: <a href="<?php echo esc_url( get_permalink($purchases_page_id) ); ?>" target="_blank" rel="noopener"><?php echo esc_html( get_permalink($purchases_page_id) ); ?></a></p>
                        <?php else: ?>
                            <p class="description">No Purchases page detected yet.</p>
                        <?php endif; ?>
                        <p><button class="button" name="wpsl_create_purchases_page" value="1" type="submit">Create/Update Purchases Page</button></p>
                        <p class="description">Creates a public page at <code>/my-purchase/</code> with the shortcode <code>[wpsl_my_videos]</code>.</p>
                        <p class="description">Tips: Share this link with buyers. After purchase, they receive an email pointing to this page where all purchased videos are listed.</p>
                    </td>
                </tr>
					<tr>
						<th scope="row">Invitation Email</th>
						<td>
							<p style="margin:0 0 6px;"><label for="wpsl-invite-subject">Subject</label><br>
								<input type="text" id="wpsl-invite-subject" name="wpsl_invite_subject" class="regular-text" value="<?php echo esc_attr($invite_subject_val); ?>" />
							</p>
							<p style="margin:6px 0 6px;"><label for="wpsl-invite-template">Template (HTML)</label></p>
							<?php if (function_exists('wp_editor')): ?>
								<?php wp_editor(
									$invite_template_val,
									'wpsl_invite_template',
									[
										'textarea_name' => 'wpsl_invite_template',
										'textarea_rows' => 10,
										'media_buttons' => false,
										'tinymce'       => true,
										'quicktags'     => true,
										'editor_height' => 220,
									]
								); ?>
							<?php else: ?>
								<textarea id="wpsl-invite-template" name="wpsl_invite_template" rows="10" class="large-text code"><?php echo esc_textarea($invite_template_val); ?></textarea>
							<?php endif; ?>
							<p class="description">Placeholders: <code>{{viewer_url}}</code>, <code>{{stream_id}}</code>, <code>{{stream_title}}</code>, <code>{{start_time}}</code></p>
						</td>
					</tr>
				</table>
				<p><button class="button button-primary" name="wpsl_save_settings" value="1">Save Settings</button></p>
			</form>
		</div>
		<?php
	}

	public function admin_faq_page() {
		if (!current_user_can('manage_options')) return;
		?>
		<div class="wrap wpsl-faq">
			<h1>WP Stream Live &ndash; FAQ / Help</h1>
			<div style="margin:10px 0; display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
				<label for="wpsl-faq-search" class="screen-reader-text">Search FAQ</label>
				<input type="search" id="wpsl-faq-search" placeholder="Search FAQ (keywords)" class="regular-text" style="min-width:280px;" />
				<span id="wpsl-faq-count" class="description"></span>
			</div>
			<div class="wpsl-faq-list">
			<details>
				<summary><span style="margin-right:6px">🌐</span><strong>What browsers can stream?</strong></summary>
				<div class=\"wpsl-faq-content\">Streaming requires <code>MediaStreamTrackProcessor</code> and WebCodecs H.264 encoder. Use Chrome/Edge desktop or Android Chrome. iOS Safari and Firefox cannot stream.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">📱</span><strong>Can viewers watch on iOS?</strong></summary>
				<div class=\"wpsl-faq-content\">Yes. Viewing works on all modern browsers including iOS Safari.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">💾</span><strong>Where are stream files stored?</strong></summary>
				<div class=\"wpsl-faq-content\">Under <code>wp-content/uploads/wpsl/{stream_id}</code>. Enable "Save recording" to keep files and auto-generate a replay playlist.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🔒</span><strong>How does password protection work?</strong></summary>
				<div class=\"wpsl-faq-content\">Viewers enter a password; a signed cookie grants access for 24 hours.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">💳</span><strong>How does Stripe paywall work?</strong></summary>
				<div class=\"wpsl-faq-content\">Uses Stripe Checkout for one-time payment, then verifies the session on return to unlock access.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">📊</span><strong>Where are Sale Reports?</strong></summary>
				<div class=\"wpsl-faq-content\">
					Open <em>Stream Live &rarr; Sale Reports</em> to see per‑stream sales.
					<ul style=\"list-style:disc;margin-left:20px;\">
						<li>Filter by date range using Start/End selectors.</li>
						<li>Totals banner shows overall sales count and gross by currency.</li>
						<li>Each stream row shows configured price, sales, gross, buyers (emails), and last sale time.</li>
						<li>Data fetches the latest 100 Stripe Checkout sessions in the range. Click <em>Refresh</em> to update.</li>
					</ul>
					<p class=\"description\">Numbers exclude Stripe fees; use the Stripe Dashboard for fee‑inclusive totals.</p>
				</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🧩</span><strong>Integrate Stripe &ndash; Step by Step</strong></summary>
				<div class=\"wpsl-faq-content\">
					<ol style="margin:8px 0 0 20px;">
						<li>Go to Stripe Dashboard &rarr; Developers &rarr; API keys.</li>
						<li>Copy your <em>Publishable key</em> (starts with <code>pk_</code>) and <em>Secret key</em> (starts with <code>sk_</code>).</li>
						<li>In WordPress, open Stream Live &rarr; Settings and paste keys into the fields.</li>
						<li>Optionally set the default currency (ISO code like <code>usd</code>, <code>eur</code>).</li>
						<li>On the stream admin page, choose Video access &rarr; Paywall and set price/currency.</li>
						<li>Share the viewer URL. Viewers pay via Stripe Checkout and are redirected back with access unlocked.</li>
					</ol>
				</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">✉️</span><strong>How do email invitations work?</strong></summary>
				<div class=\"wpsl-faq-content\">
					<ol style=\"margin:8px 0 0 20px;\">
						<li>On the <em>Stream Live</em> page, check <em>Invite by email</em>.</li>
						<li>Click <em>Add users to be invited</em> to open the picker. Select existing users or add emails manually, then <em>Save Selection</em>.</li>
						<li>Optionally click <em>Send Preview to Me</em> from the modal to test the email.</li>
						<li>Click <em>Start</em> to begin streaming; invites send in the background to selected recipients.</li>
					</ol>
					<p>Customize the subject and HTML template under <em>Stream Live &rarr; Settings &rarr; Invitation Email</em>. Available placeholders: <code>{{viewer_url}}</code>, <code>{{stream_id}}</code>, <code>{{stream_title}}</code>, <code>{{start_time}}</code>.</p>
				</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🛡️</span><strong>How to moderate chat?</strong></summary>
				<div class=\"wpsl-faq-content\">Admins can delete messages and ban users from the sidebar user list.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🎞️</span><strong>Where are Recorded Videos?</strong></summary>
				<div class=\"wpsl-faq-content\">Open <em>Stream Live &rarr; Recorded Videos</em> to browse replays. You can search by name/ID, preview HLS, rename the title inline, and bulk delete old sessions. Replays are stored under <code>uploads/wpsl/{id}</code> as <code>vod.m3u8</code>.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🔗</span><strong>How do I create a Viewer page?</strong></summary>
				<div class=\"wpsl-faq-content\">On the Stream Live page, click <em>Auto-create Viewer Page</em> after creating a stream. Alternatively, embed manually with <code>[wpsl_viewer stream=\"123\"]</code>. Options: <code>chat=\"0|1\"</code>, <code>poll=\"1.5\"</code>. You can also open an on-the-fly viewer at <code>/?wpsl_viewer=1&stream=123</code>.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">👥</span><strong>Online users (Presence)</strong></summary>
				<div class=\"wpsl-faq-content\">The viewer reports presence periodically. Admins can query <code>/wp-json/wpsl/v1/presence/list?stream_id=123</code> and charts in the UI use this data. When <em>Other users can see user list</em> is enabled, a public list of names is exposed via <code>/wp-json/wpsl/v1/presence/public?stream_id=123</code>.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🚦</span><strong>Chat rate limits</strong></summary>
				<div class=\"wpsl-faq-content\">Server-side anti-flood is enabled: minimum interval between messages per IP per stream and a burst window cap. If users report blocked messages, slow the typing rate or reduce burstiness.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🧼</span><strong>Reset chat table</strong></summary>
				<div class=\"wpsl-faq-content\">Use WP-CLI: <code>wp db query \"TRUNCATE TABLE $(wp db prefix)wpsl_chat;\"</code>. Back up first; this permanently deletes all chat history.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">💳</span><strong>Stripe payment issues</strong></summary>
				<div class=\"wpsl-faq-content\">
					<ul style=\"list-style:disc;margin-left:20px;\">
						<li>Ensure <em>Stream Live &rarr; Settings</em> has valid <code>pk_</code> and <code>sk_</code> keys (test vs live).</li>
						<li>Price must be set on the stream when Paywall is selected.</li>
						<li>Viewer returns with <code>?session_id=...</code>; the plugin verifies status and unlocks access. If not redirecting, check site HTTPS and that the return URL loads publicly.</li>
						<li>Use Stripe Dashboard logs to inspect Checkout Session creation and status.</li>
					</ul>
				</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">💡</span><strong>Tips</strong></summary>
				<div class=\"wpsl-faq-content\">
					<ul style="list-style:disc;margin-left:20px;">
						<li>Use 2&ndash;4s latency for a good balance of quality and stability.</li>
						<li>Toggle "Save recording" to publish a VOD playlist when stopping.</li>
						<li>Auto-create a Viewer page for easy sharing and permalinks.</li>
					</ul>
				</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">⏱️</span><strong>Live vs Replay</strong></summary>
				<div class=\"wpsl-faq-content\">Replay is created when "Save recording" is enabled before starting. The recordings page lists both replay (vod.m3u8) and live playlists (index.m3u8). Viewer opens replay automatically when the URL ends with <code>#replay</code>.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🧰</span><strong>Troubleshooting playback</strong></summary>
				<div class=\"wpsl-faq-content\">If video doesn’t start: verify the playlist URL loads, confirm server serves correct MIME types (m3u8: <code>application/vnd.apple.mpegurl</code>, m4s/mp4: <code>video/mp4</code>). For non-Safari browsers, hls.js is loaded automatically for previews.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🧹</span><strong>Storage and cleanup</strong></summary>
				<div class=\"wpsl-faq-content\">Use bulk delete on the recordings page to remove old sessions. Consider using a CDN if footprint grows. Only files under <code>uploads/wpsl/{id}</code> are touched by delete.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🔐</span><strong>Security & privacy</strong></summary>
				<div class=\"wpsl-faq-content\">Admin-only endpoints enforce capability checks and nonces. Password/paywall gate viewer access via signed cookies. Upload writes are restricted to per-stream folders only.</div>
			</details>
			<details>
				<summary><span style="margin-right:6px">🔑</span><strong>Logged-in Access</strong></summary>
				<div class=\"wpsl-faq-content\">Enable <em>Only logged-in users can access video</em> on the Stream Live admin page to require viewers to sign in before watching. If WordPress registration is disabled, you can also check <em>Allow new users to register</em> to enable sign-ups site‑wide so new viewers can create accounts.</div>
			</details>
			</div>
			<p id="wpsl-faq-empty" class="description" style="display:none;">No matches found.</p>
			<script>
			(function(){
				function ready(fn){ if(document.readyState!=='loading') fn(); else document.addEventListener('DOMContentLoaded', fn); }
				ready(function(){
					var input = document.getElementById('wpsl-faq-search');
					var list = document.querySelector('.wpsl-faq-list');
					var empty = document.getElementById('wpsl-faq-empty');
					var count = document.getElementById('wpsl-faq-count');
					if (!input || !list) return;
					var items = Array.prototype.slice.call(list.querySelectorAll('details'));
					function norm(s){ return String(s||'').toLowerCase(); }
					function clearHighlights(root){
						try {
							(root||document).querySelectorAll('mark.wpsl-hl').forEach(function(m){
								var text = document.createTextNode(m.textContent||'');
								m.replaceWith(text);
							});
						} catch(e){}
					}
					function escapeReg(s){ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
					function applyHighlights(root, q){
						if (!q) return;
						var re = new RegExp(escapeReg(q), 'gi');
						var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
							acceptNode: function(n){
								if (!n.nodeValue || !n.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
								var p = n.parentElement; if (!p) return NodeFilter.FILTER_REJECT;
								var tag = p.tagName ? p.tagName.toLowerCase() : '';
								if (tag === 'script' || tag === 'style' || tag === 'mark') return NodeFilter.FILTER_REJECT;
								return NodeFilter.FILTER_ACCEPT;
							}
						}, false);
						var nodes = [];
						while (walker.nextNode()) nodes.push(walker.currentNode);
						nodes.forEach(function(textNode){
							var val = textNode.nodeValue; if (!re.test(val)) return; re.lastIndex = 0;
							var frag = document.createDocumentFragment();
							var last = 0; var m;
							while ((m = re.exec(val)) !== null) {
								if (m.index > last) frag.appendChild(document.createTextNode(val.slice(last, m.index)));
								var mark = document.createElement('mark'); mark.className = 'wpsl-hl'; mark.textContent = m[0]; frag.appendChild(mark);
								last = m.index + m[0].length;
							}
							if (last < val.length) frag.appendChild(document.createTextNode(val.slice(last)));
							textNode.replaceWith(frag);
						});
					}
					function search(){
						var q = norm(input.value.trim());
						var matches = 0;
						// clear previous highlights
						clearHighlights(list);
						items.forEach(function(d){
							var ok = !q || norm(d.textContent).indexOf(q) !== -1;
							d.style.display = ok ? '' : 'none';
							if (ok) { matches++; if (q) d.open = true; }
							else { d.open = false; }
						});
						// apply highlights to visible matches
						if (q) {
							items.forEach(function(d){ if (d.style.display !== 'none') applyHighlights(d, q); });
						}
						if (count) count.textContent = q ? (matches + ' match' + (matches===1?'':'es')) : '';
						if (empty) empty.style.display = (q && matches===0) ? '' : 'none';
					}
					input.addEventListener('input', search);
				});
			})();
			</script>
		<?php
	}

	public function admin_recordings_page() {
		if (!current_user_can('manage_options')) return;
		$upload = wp_upload_dir();
		$base_dir = trailingslashit($upload['basedir']) . 'wpsl/';
		$base_url = trailingslashit($upload['baseurl']) . 'wpsl/';

		// Read current search query early (used in redirects)
		$search = isset($_GET['s']) ? sanitize_text_field((string)$_GET['s']) : '';

		// Handle actions (rename/delete)
		if (isset($_POST['wpsl_rec_nonce']) && wp_verify_nonce((string)$_POST['wpsl_rec_nonce'], 'wpsl_rec_actions')) {
			$action = sanitize_text_field((string)($_POST['rec_action'] ?? ''));
			$stream_id = intval($_POST['stream_id'] ?? 0);
			if ($action && $stream_id) {
				if ($action === 'rename_title') {
					$title = sanitize_text_field((string)($_POST['rec_title'] ?? ''));
					if ($title === '') $title = 'stream_' . $stream_id;
					wp_update_post(['ID' => $stream_id, 'post_title' => $title]);
					wp_safe_redirect( add_query_arg(['page'=>'wpsl_recordings','paged'=>max(1, intval($_GET['paged'] ?? 1)),'s'=>$search], admin_url('admin.php')) );
					exit;
				}
			}
		}
		$per_page = max(1, intval($_GET['per_page'] ?? 5));
		$paged = max(1, intval($_GET['paged'] ?? 1));

		$all = new WP_Query([
			'post_type'      => self::CPT,
			'posts_per_page' => 500,
			'paged'          => 1,
			'post_status'    => 'any',
			'orderby'        => 'date',
			'order'          => 'DESC',
			'fields'         => 'all',
		]);
		$items = [];
		if ($all->have_posts()) {
			while ($all->have_posts()) {
				$all->the_post();
				$p = get_post();
				$sid = (int)$p->ID;
				$vod_path = $base_dir . $sid . '/vod.m3u8';
				$idx_path = $base_dir . $sid . '/index.m3u8';
				if (!file_exists($vod_path) && !file_exists($idx_path)) continue;
				$items[] = [
					'id'      => $sid,
					'title'   => get_the_title($p) ?: ('stream_' . $sid),
					'date'    => get_the_date('', $p),
					'preview' => (file_exists($vod_path) ? ($base_url . $sid . '/vod.m3u8') : ($base_url . $sid . '/index.m3u8')),
					'is_vod'  => file_exists($vod_path) ? 1 : 0,
				];
			}
			wp_reset_postdata();
		}
		if ($search !== '' && !empty($items)) {
			$s = mb_strtolower($search);
			$items = array_values(array_filter($items, function($it) use ($s){
				return (strpos(mb_strtolower($it['title']), $s) !== false) || (strpos((string)$it['id'], $s) !== false);
			}));
		}
		$total = count($items);
		$total_pages = max(1, (int)ceil($total / $per_page));
		if ($paged > $total_pages) $paged = $total_pages;
		$offset = ($paged - 1) * $per_page;
		$rows = array_slice($items, $offset, $per_page);
		?>
		<div class="wrap">
			<h1>Recorded Videos</h1>
			<form method="get" style="margin:10px 0; display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
				<input type="hidden" name="page" value="wpsl_recordings" />
				<input type="hidden" name="per_page" value="<?php echo (int)$per_page; ?>" />
				<input type="search" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Search by name, title, or ID" />
				<button class="button">Search</button>
			</form>
			<?php if ($total_pages > 0): $base = add_query_arg(['page'=>'wpsl_recordings','paged'=>'%#%','s'=>$search,'per_page'=>$per_page], admin_url('admin.php')); $prev = $paged > 1 ? add_query_arg(['paged'=>$paged-1], $base) : ''; $next = $paged < $total_pages ? add_query_arg(['paged'=>$paged+1], $base) : ''; ?>
			<div class="tablenav top"><div class="tablenav-pages">
				<span class="displaying-num">Page <?php echo (int)$paged; ?> of <?php echo (int)$total_pages; ?> (<?php echo (int)$total; ?> recordings)</span>
				<span class="pagination-links" style="display:inline-flex;gap:6px;align-items:center;">
					<?php if ($prev): ?><a class="button" href="<?php echo esc_url($prev); ?>">&laquo; Prev</a><?php else: ?><span class="button disabled">&laquo; Prev</span><?php endif; ?>
					<?php $window=2; $start=max(1,$paged-$window); $end=min($total_pages,$paged+$window); for($i=$start; $i<=$end; $i++){ if($i===$paged) echo '<span class="button button-primary">'.(int)$i.'</span>'; else echo '<a class="button" href="'.esc_url(add_query_arg(['paged'=>$i], $base)).'">'.(int)$i.'</a>'; } ?>
					<?php if ($next): ?><a class="button" href="<?php echo esc_url($next); ?>">Next &raquo;</a><?php else: ?><span class="button disabled">Next &raquo;</span><?php endif; ?>
				</span>
				<form method="get" style="display:inline-flex; gap:6px; align-items:center; margin-left:12px;">
					<input type="hidden" name="page" value="wpsl_recordings" />
					<input type="hidden" name="s" value="<?php echo esc_attr($search); ?>" />
					<label class="description">Per page:
						<select name="per_page" onchange="this.form.submit()">
							<?php foreach ([5,10,20,50] as $opt): ?>
								<option value="<?php echo (int)$opt; ?>" <?php selected($per_page, $opt); ?>><?php echo (int)$opt; ?></option>
							<?php endforeach; ?>
						</select>
					</label>
				</form>
			</div></div>
			<?php endif; ?>
			<form method="post" action="<?php echo esc_url( admin_url('admin-post.php') ); ?>" onsubmit="return confirm('Delete selected videos? This cannot be undone.');">
				<?php wp_nonce_field('wpsl_rec_actions', 'wpsl_rec_nonce'); ?>
				<input type="hidden" name="action" value="wpsl_bulk_delete" />
				<div style="margin:8px 0;"><button class="button button-danger">Delete Selected</button></div>
			<table class="widefat fixed striped">
				<thead><tr><th style="width:30px;"><input type="checkbox" onclick="document.querySelectorAll('.wpsl-bulk').forEach(cb=>cb.checked=this.checked);" /></th><th>Stream</th><th>Title</th><th>Date</th><th>Duration</th><th>Size</th><th>Stats</th><th>Preview</th><th>Actions</th></tr></thead>
				<tbody>
				<?php if (!empty($rows)): foreach ($rows as $row): $sid = (int)$row['id']; $src_url = (string)($row['preview'] ?? ($base_url . $sid . '/vod.m3u8')); $is_vod = !empty($row['is_vod']); $dur = isset($row['dur']) ? $row['dur'] : ''; $size = isset($row['size']) ? $row['size'] : '';
				if ($dur === '' || $size === '') { $dir_path = trailingslashit($base_dir) . $sid; $vod_p = $dir_path . '/vod.m3u8'; $idx_p = $dir_path . '/index.m3u8'; $m3u_p = file_exists($vod_p) ? $vod_p : (file_exists($idx_p) ? $idx_p : ''); if ($m3u_p !== '') { $dur_s = 0.0; $lns = @file($m3u_p, FILE_IGNORE_NEW_LINES); if (is_array($lns)) { foreach ($lns as $l) { if (strpos($l, '#EXTINF:') === 0) { $val = trim(substr($l, 8)); $val = rtrim($val, ','); $dur_s += (float) $val; } } } $h = floor($dur_s / 3600); $m = floor(fmod($dur_s, 3600) / 60); $s = floor(fmod($dur_s, 60)); $dur = ($h > 0 ? $h . ':' : '') . sprintf('%02d:%02d', $m, $s); } $sum = 0; $ents = @scandir($dir_path); if (is_array($ents)) { foreach ($ents as $e) { if ($e === '.' || $e === '..') continue; $p2 = $dir_path . '/' . $e; if (is_file($p2)) { $sz = @filesize($p2); if ($sz) $sum += $sz; } } } $size = $sum > 0 ? size_format($sum, 2) : '&mdash;'; } ?>
					<tr>
						<td><input type="checkbox" class="wpsl-bulk" name="ids[]" value="<?php echo (int)$sid; ?>" /></td>
						<td>#<?php echo (int)$sid; ?></td>
						<td><?php echo esc_html($row['title']); ?></td>
						<td><?php echo esc_html($row['date']); ?></td>
						<td><?php echo esc_html($dur); ?></td>
						<td><?php echo esc_html($size); ?></td>
						<td>
							<?php
							// Stats: views (24h unique), messages total
							$views = 0; $uniq = get_transient( $this->presence_unique_key($sid) ); if (is_array($uniq)) { $views = count($uniq); }
							global $wpdb; $table = $wpdb->prefix . self::CHAT_TABLE; $msgCount = intval($wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE stream_id = %d", $sid) ));
							echo esc_html("Views: {$views} · Messages: {$msgCount}");
							?>
						</td>
						<td>
								<?php $poster_url = $base_url . $sid . '/poster.jpg'; ?>
								<div class="wpsl-rec-preview" data-src="<?php echo esc_attr($src_url); ?>">
									<video playsinline muted width="180" height="101" src="<?php echo esc_attr($src_url); ?>" preload="metadata" poster="<?php echo esc_attr($poster_url); ?>"></video>
								</div>
						</td>
						<td>
							<?php $viewer = add_query_arg(['wpsl_viewer'=>1,'stream'=>$sid,'chat'=>'0'], site_url('/')) . ($is_vod ? '#replay' : ''); ?>
								<a class="button wpsl-btn-compact" href="<?php echo esc_url($viewer); ?>" target="_blank" rel="noopener"><?php echo $is_vod ? 'Open Viewer' : 'Open Viewer'; ?></a>
								<button type="button" class="button wpsl-btn-compact wpsl-copy" data-url="<?php echo esc_attr($viewer); ?>" title="Copy viewer link" onclick="if(navigator.clipboard&&navigator.clipboard.writeText){var b=this;navigator.clipboard.writeText(this.dataset.url).then(function(){var t=b.textContent;b.textContent='Copied';setTimeout(function(){b.textContent=t;},1200);});} return false;">Copy</button>
								<a class="button wpsl-btn-compact" href="<?php echo esc_url($src_url); ?>" target="_blank" rel="noopener"><?php echo $is_vod ? 'Direct VOD' : 'Direct Playlist'; ?></a>
								<button type="button" class="button wpsl-btn-compact wpsl-copy" data-url="<?php echo esc_attr($src_url); ?>" title="Copy direct link" onclick="if(navigator.clipboard&&navigator.clipboard.writeText){var b=this;navigator.clipboard.writeText(this.dataset.url).then(function(){var t=b.textContent;b.textContent='Copied';setTimeout(function(){b.textContent=t;},1200);});} return false;">Copy</button>
							<!-- Per-row delete removed; use bulk delete -->
						</td>
					</tr>
				<?php endforeach; else: ?>
					<tr><td colspan="6"><em>No recordings found.</em></td></tr>
				<?php endif; ?>
				</tbody>
			</table>
				<div style="margin:8px 0;"><button class="button button-danger">Delete Selected</button></div>
			</form>
			<?php if ($total_pages > 0): $base = add_query_arg(['page'=>'wpsl_recordings','paged'=>'%#%','s'=>$search], admin_url('admin.php')); ?>
			<div class="tablenav"><div class="tablenav-pages">
				<?php $prev = $paged > 1 ? add_query_arg(['paged'=>$paged-1], $base) : ''; $next = $paged < $total_pages ? add_query_arg(['paged'=>$paged+1], $base) : ''; ?>
				<span class="displaying-num">Page <?php echo (int)$paged; ?> of <?php echo (int)$total_pages; ?> (<?php echo (int)$total; ?> recordings)</span>
				<span class="pagination-links" style="display:inline-flex;gap:6px;align-items:center;">
					<?php if ($prev): ?><a class="button" href="<?php echo esc_url($prev); ?>">&laquo; Prev</a><?php else: ?><span class="button disabled">&laquo; Prev</span><?php endif; ?>
					<?php $window=2; $start=max(1,$paged-$window); $end=min($total_pages,$paged+$window); for($i=$start; $i<=$end; $i++){ if($i===$paged) echo '<span class="button button-primary">'.(int)$i.'</span>'; else echo '<a class="button" href="'.esc_url(add_query_arg(['paged'=>$i], $base)).'">'.(int)$i.'</a>'; } ?>
					<?php if ($next): ?><a class="button" href="<?php echo esc_url($next); ?>">Next &raquo;</a><?php else: ?><span class="button disabled">Next &raquo;</span><?php endif; ?>
				</span>
			</div></div>
			<?php endif; ?>
		</div>
		<script>
		(function(){
			function ready(fn){ if(document.readyState!=='loading') fn(); else document.addEventListener('DOMContentLoaded', fn); }
			ready(function(){
					document.querySelectorAll('.wpsl-rec-rename-form').forEach(function(f){
					f.addEventListener('submit', function(ev){
						ev.preventDefault();
						var id = f.querySelector('input[name="stream_id"]').value;
						var name = f.querySelector('input[name="rec_title"]').value;
						var status = f.querySelector('.wpsl-inline-status');
						var fd = new FormData();
						fd.append('action','wpsl_rec_rename');
						fd.append('stream_id', String(id));
						fd.append('rec_title', String(name||''));
						fd.append('nonce','<?php echo esc_js( wp_create_nonce('wpsl_admin') ); ?>');
						fetch(ajaxurl, { method:'POST', body: fd }).then(function(r){ return r.json(); }).then(function(){ if(status){ status.style.display='inline'; setTimeout(function(){ status.style.display='none'; },1200);} }).catch(function(){});
					});
				});
					// HLS attach for non-native preview players
					(function(){
						var boxes = document.querySelectorAll('.wpsl-rec-preview');
						if (!boxes.length) return;
						function ensureHls(cb){ if (window.Hls && window.Hls.isSupported && window.Hls.isSupported()) return cb(); var s=document.createElement('script'); s.src='<?php echo esc_js( plugin_dir_url(__FILE__) . 'assets/js/hls.min.js' ); ?>'; s.onload=cb; document.head.appendChild(s); }
						boxes.forEach(function(box){
							var v = box.querySelector('video'); var src = box.getAttribute('data-src'); if (!v || !src) return;
							if (!v.canPlayType('application/vnd.apple.mpegurl')) {
								ensureHls(function(){ if (window.Hls && window.Hls.isSupported()) { try { var hls = new window.Hls({lowLatencyMode:false}); hls.attachMedia(v); hls.on(window.Hls.Events.MEDIA_ATTACHED, function(){ try { hls.loadSource(src); } catch(e){} }); } catch(e){} } });
							}
						});
					})();
			});
		})();
		</script>
		<?php
	}

	public function admin_sales_page() {
		if (!current_user_can('manage_options')) return;
		$sk = $this->get_option('wpsl_stripe_sk');
		// Read filters (date range)
		$start = isset($_GET['start']) ? sanitize_text_field((string)$_GET['start']) : '';
		$end   = isset($_GET['end'])   ? sanitize_text_field((string)$_GET['end'])   : '';
		$start_ts = 0; $end_ts = 0;
		if ($start && preg_match('~^\d{4}-\d{2}-\d{2}$~', $start)) { $start_ts = strtotime($start . ' 00:00:00'); }
		if ($end   && preg_match('~^\d{4}-\d{2}-\d{2}$~', $end))   { $end_ts   = strtotime($end . ' 23:59:59'); }

		$refresh = isset($_GET['refresh']) ? (int) $_GET['refresh'] : 0;
		$nonce_ok = isset($_GET['_wpnonce']) && wp_verify_nonce((string)$_GET['_wpnonce'], 'wpsl_sales_refresh');
		$cache_key = 'wpsl_sales_cache_' . md5((string)$start_ts . '|' . (string)$end_ts);
		if ($refresh && $nonce_ok) {
			delete_transient($cache_key);
		}

		$report = get_transient($cache_key);
		$error = '';
		if (!is_array($report)) {
			$report = [ 'streams' => [], 'fetched_at' => time(), 'totals' => ['sales'=>0,'gross'=>[]] ];
			if ($sk) {
				$url = 'https://api.stripe.com/v1/checkout/sessions?limit=100&expand[]=data.line_items';
				if ($start_ts) { $url .= '&created[gte]=' . rawurlencode((string)$start_ts); }
				if ($end_ts)   { $url   .= '&created[lte]=' . rawurlencode((string)$end_ts); }
				$resp = wp_remote_get($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $sk ], 'timeout' => 12 ]);
				$code = wp_remote_retrieve_response_code($resp);
				$body = json_decode(wp_remote_retrieve_body($resp), true);
				if ($code === 200 && is_array($body) && isset($body['data']) && is_array($body['data'])) {
					$by_stream = [];
					foreach ($body['data'] as $s) {
						$status = isset($s['payment_status']) ? (string)$s['payment_status'] : ((string)($s['status'] ?? ''));
						if (!in_array($status, ['paid','complete'], true)) continue;
						$line = isset($s['line_items']['data'][0]) && is_array($s['line_items']['data'][0]) ? $s['line_items']['data'][0] : [];
						$desc = isset($line['description']) ? (string)$line['description'] : '';
						if ($desc === '' && isset($s['display_items'][0]['custom']['name'])) { $desc = (string)$s['display_items'][0]['custom']['name']; }
						$stream_id = 0;
						if ($desc !== '' && preg_match('~#(\d+)~', $desc, $m)) { $stream_id = (int)$m[1]; }
						if ($stream_id <= 0) continue;
						$currency = strtoupper((string)($s['currency'] ?? ($line['price']['currency'] ?? '')));
						$amount = isset($s['amount_total']) ? (int)$s['amount_total'] : (int)($line['amount_total'] ?? 0);
						$created = isset($s['created']) ? (int)$s['created'] : time();
						// buyer email
						$email = '';
						if (isset($s['customer_details']['email']) && is_string($s['customer_details']['email'])) { $email = trim((string)$s['customer_details']['email']); }
						elseif (isset($s['customer_email']) && is_string($s['customer_email'])) { $email = trim((string)$s['customer_email']); }
						if (!isset($by_stream[$stream_id])) {
							$by_stream[$stream_id] = [ 'sales' => 0, 'gross' => [], 'last' => 0, 'buyers' => [] ];
						}
						$by_stream[$stream_id]['sales'] += 1;
						if ($currency !== '') {
							if (!isset($by_stream[$stream_id]['gross'][$currency])) $by_stream[$stream_id]['gross'][$currency] = 0;
							$by_stream[$stream_id]['gross'][$currency] += max(0, $amount);
						}
						$by_stream[$stream_id]['last'] = max($by_stream[$stream_id]['last'], $created);
						if ($email !== '') {
							$by_stream[$stream_id]['buyers'][$email] = true; // unique set
						}

						// accumulate totals
						$report['totals']['sales'] += 1;
						if ($currency !== '') {
							if (!isset($report['totals']['gross'][$currency])) $report['totals']['gross'][$currency] = 0;
							$report['totals']['gross'][$currency] += max(0, $amount);
						}
					}
					$report['streams'] = $by_stream;
					set_transient($cache_key, $report, 5 * MINUTE_IN_SECONDS);
				} else {
					$error = 'Could not fetch sessions from Stripe.';
				}
			}
		}

		?>
		<div class="wrap">
			<h1>Sale Reports</h1>
			<?php if (!$sk): ?>
				<div class="notice notice-warning"><p>Stripe is not configured. Add your API keys in <a href="<?php echo esc_url( admin_url('admin.php?page=wpsl_settings') ); ?>">Stream Live &rarr; Settings</a> to enable paywall and sales reporting.</p></div>
			<?php endif; ?>
			<?php if ($error): ?>
				<div class="notice notice-error"><p><?php echo esc_html($error); ?></p></div>
			<?php endif; ?>
			<form method="get" style="margin:10px 0; display:flex; gap:10px; align-items:flex-end; flex-wrap:wrap;">
				<input type="hidden" name="page" value="wpsl_sales" />
				<label>Start date<br><input type="date" name="start" value="<?php echo esc_attr($start); ?>" /></label>
				<label>End date<br><input type="date" name="end" value="<?php echo esc_attr($end); ?>" /></label>
				<button class="button">Filter</button>
				<?php $refresh_url = wp_nonce_url( add_query_arg(['page'=>'wpsl_sales','refresh'=>1,'start'=>$start,'end'=>$end], admin_url('admin.php')) , 'wpsl_sales_refresh'); ?>
				<a href="<?php echo esc_url($refresh_url); ?>" class="button">Refresh</a>
				<?php if (!empty($report['fetched_at'])): ?><span class="description">Last fetched: <?php echo esc_html( date_i18n( get_option('date_format') . ' ' . get_option('time_format'), (int)$report['fetched_at'] ) ); ?></span><?php endif; ?>
			</form>

			<?php
			// Totals summary
			$tot_sales = isset($report['totals']['sales']) ? (int)$report['totals']['sales'] : 0;
			$tot_gross = isset($report['totals']['gross']) && is_array($report['totals']['gross']) ? $report['totals']['gross'] : [];
			$gross_parts = [];
			foreach ($tot_gross as $cur => $amt) { $gross_parts[] = esc_html($cur . ' ' . number_format_i18n(((int)$amt)/100, 2)); }
			$gross_str = !empty($gross_parts) ? implode(', ', $gross_parts) : '&mdash;';
			?>
			<div class="notice notice-info" style="padding:8px 12px; margin:0 0 10px 0;">
				<strong>Total Sales:</strong> <?php echo (int)$tot_sales; ?> &nbsp; 
				<strong>Gross:</strong> <?php echo $gross_str; ?>
				<span class="description" style="margin-left:8px;">(Up to latest 100 sessions<?php echo ($start||$end)?', filtered by date':''; ?>)</span>
			</div>

			<table class="widefat fixed striped">
				<thead>
					<tr>
						<th>Stream</th>
						<th>Title</th>
						<th>Price</th>
						<th>Sales</th>
						<th>Gross</th>
						<th>Buyers (emails)</th>
						<th>Last Sale</th>
					</tr>
				</thead>
				<tbody>
				<?php
				$streams = isset($report['streams']) && is_array($report['streams']) ? $report['streams'] : [];
				if (empty($streams)) {
					echo '<tr><td colspan="6">No sales found.</td></tr>';
				} else {
					ksort($streams, SORT_NUMERIC);
					foreach ($streams as $sid => $row) {
						$post = get_post( (int) $sid );
						$title = $post ? ( get_the_title($post) ?: ('stream_' . (int)$sid) ) : ('stream_' . (int)$sid);
						$price_cents = (int) get_post_meta($sid, '_wpsl_price_cents', true);
						$currency = get_post_meta($sid, '_wpsl_currency', true) ?: $this->get_option('wpsl_default_currency', 'usd');
						$price_str = $price_cents > 0 ? ( strtoupper($currency) . ' ' . number_format_i18n($price_cents/100, 2) ) : '&mdash;';
						// Gross across currencies
						$gross_parts = [];
						if (isset($row['gross']) && is_array($row['gross'])) {
							foreach ($row['gross'] as $cur => $amt) {
								$gross_parts[] = esc_html($cur . ' ' . number_format_i18n( ((int)$amt)/100, 2 ));
							}
						}
						$gross_str = !empty($gross_parts) ? implode(', ', $gross_parts) : '&mdash;';
						$last = isset($row['last']) ? (int)$row['last'] : 0;
						$last_str = $last ? date_i18n( get_option('date_format') . ' ' . get_option('time_format'), $last ) : '&mdash;';
						$viewer = add_query_arg(['wpsl_viewer'=>1,'stream'=>(int)$sid], site_url('/'));
						// buyers list (up to 5)
						$buyers = isset($row['buyers']) && is_array($row['buyers']) ? array_keys($row['buyers']) : [];
						$buyers_count = count($buyers);
						$buyers_show = array_slice($buyers, 0, 5);
						$buyers_more = max(0, $buyers_count - count($buyers_show));
						?>
						<tr>
							<td>#<?php echo (int)$sid; ?></td>
							<td><a href="<?php echo esc_url($viewer); ?>" target="_blank" rel="noopener"><?php echo esc_html($title); ?></a></td>
							<td><?php echo esc_html($price_str); ?></td>
							<td><?php echo (int)($row['sales'] ?? 0); ?></td>
							<td><?php echo $gross_str; ?></td>
							<td title="<?php echo esc_attr(implode(', ', $buyers)); ?>">
								<?php echo esc_html(implode(', ', $buyers_show)); ?><?php if ($buyers_more>0): ?>, +<?php echo (int)$buyers_more; ?> more<?php endif; ?>
							</td>
							<td><?php echo esc_html($last_str); ?></td>
						</tr>
						<?php
					}
				}
				?>
				</tbody>
			</table>
			<p class="description">Totals exclude Stripe fees. For full details, use your Stripe Dashboard.</p>
		</div>
		<?php
	}

	public function admin_assets($hook) {
		// Load admin styles on top-level and recordings pages
		if ($hook === 'toplevel_page_wpsl' || $hook === 'stream-live_page_wpsl_recordings') {
			wp_enqueue_style('wpsl-admin', plugin_dir_url(__FILE__) . 'assets/css/admin.css?cache=' . time(), [], '1.0.0');
		}

		// Sales page assets
		if ($hook === 'stream-live_page_wpsl_sales') {
			wp_enqueue_style('wpsl-admin', plugin_dir_url(__FILE__) . 'assets/css/admin.css?cache=' . time(), [], '1.0.0');
		}

		// Top-level page scripts
		if ($hook === 'toplevel_page_wpsl') {
			wp_enqueue_script('wpsl-streamer', plugin_dir_url(__FILE__) . 'assets/js/streamer.js?cache=' . time(), [], '1.0.0', true);
        wp_localize_script('wpsl-streamer', 'WPSL', [
            'ajax'      => admin_url('admin-ajax.php'),
            'rest'      => esc_url_raw(rest_url('wpsl/v1')),
            'nonce'     => wp_create_nonce('wp_rest'),
            'ajaxNonce' => wp_create_nonce('wpsl_admin'),
            'hlsLatency'=> (int) get_option('wpsl_hls_latency', 2),
            'pollLatency'=> (float) get_option('wpsl_default_poll', 1.5),
        ]);
		}
		// FAQ page assets
		if ($hook === 'stream-live_page_wpsl_faq') {
			wp_enqueue_style('wpsl-faq', plugin_dir_url(__FILE__) . 'assets/css/faq.css?cache=' . time(), [], '1.0.0');
		}
	}

	public function front_assets() {
		// viewer assets loaded only when shortcode is present (we enqueue inside shortcode)
	}

    public function filter_stream_post_content($content) {
        if (is_admin() || is_feed()) return $content;
        if (!is_singular(self::CPT)) return $content;
        global $post;
        if (!$post || $post->post_type !== self::CPT) return $content;
        $stream_id = (int)$post->ID;
        // If returning from Stripe Checkout with session_id, verify and grant access
        $session_id = isset($_GET['session_id']) ? sanitize_text_field((string)$_GET['session_id']) : '';
        if ($session_id) {
            $sk = $this->get_option('wpsl_stripe_sk');
            if ($sk) {
                $resp = wp_remote_get('https://api.stripe.com/v1/checkout/sessions/' . rawurlencode($session_id), [
                    'headers' => [ 'Authorization' => 'Bearer ' . $sk ],
                    'timeout' => 10,
                ]);
                $code = wp_remote_retrieve_response_code($resp);
                $body = json_decode(wp_remote_retrieve_body($resp), true);
                if ($code === 200 && is_array($body)) {
                    $status = $body['payment_status'] ?? ($body['status'] ?? '');
                    if ($status === 'paid' || $status === 'complete') {
                        $this->grant_pay_access($stream_id);
                        if (is_user_logged_in()) { $this->record_purchase(get_current_user_id(), $stream_id); }
                        if (!headers_sent()) {
                            $clean = remove_query_arg('session_id');
                            wp_safe_redirect($clean);
                            exit;
                        }
                    }
                }
            }
        }
        // Render viewer without chat; access rules (login/password/paywall) are enforced inside the shortcode.
        $view = do_shortcode('[wpsl_viewer stream="' . $stream_id . '" chat="0" poll="1.5"]');
        // On single stream posts, let the container size with content (avoid full-viewport height)
        if (wp_style_is('wpsl-viewer', 'enqueued')) {
            wp_add_inline_style('wpsl-viewer', 'body.single-wpsl_stream .wpsl-viewer{min-height:auto;height:auto;}');
        } else {
            add_action('wp_enqueue_scripts', function(){
                if (wp_style_is('wpsl-viewer', 'enqueued')) {
                    wp_add_inline_style('wpsl-viewer', 'body.single-wpsl_stream .wpsl-viewer{min-height:auto;height:auto;}');
                }
            }, 20);
        }
        return $view . $content;
    }

	public function maybe_viewer_template() {
		if (isset($_GET['wpsl_viewer'])) {
			$stream_id = intval($_GET['stream'] ?? 0);
			$chat = (isset($_GET['chat']) && $_GET['chat'] === '0') ? '0' : '1';
			$poll = isset($_GET['poll']) ? sanitize_text_field((string)$_GET['poll']) : '';

			// Handle Stripe return with session_id for paywall verification
			$session_id = isset($_GET['session_id']) ? sanitize_text_field((string)$_GET['session_id']) : '';
			if ($stream_id && $session_id) {
				$sk = $this->get_option('wpsl_stripe_sk');
				if ($sk) {
					$resp = wp_remote_get('https://api.stripe.com/v1/checkout/sessions/' . rawurlencode($session_id), [
						'headers' => [ 'Authorization' => 'Bearer ' . $sk ],
						'timeout' => 10,
					]);
					$code = wp_remote_retrieve_response_code($resp);
					$body = json_decode(wp_remote_retrieve_body($resp), true);
                    if ($code === 200 && is_array($body)) {
                        $status = $body['payment_status'] ?? ($body['status'] ?? '');
                        if ($status === 'paid' || $status === 'complete') {
                            $this->grant_pay_access($stream_id);
                            if (is_user_logged_in()) { $this->record_purchase(get_current_user_id(), $stream_id); }
                            // redirect to clean URL (drop session_id)
                            if (!headers_sent()) {
                                $clean = remove_query_arg('session_id');
                                wp_safe_redirect($clean);
                                exit;
							}
						}
					}
				}
			}

			status_header(200);
			nocache_headers();
			?>
			<!DOCTYPE html>
			<html <?php language_attributes(); ?>>
			<head>
				<meta charset="<?php bloginfo('charset'); ?>">
				<meta name="viewport" content="width=device-width, initial-scale=1">
				<title><?php echo esc_html(get_bloginfo('name')); ?> &ndash; Live Stream</title>
				<?php wp_head(); ?>
			</head>
			<body <?php body_class('wpsl-viewer-page'); ?>>
			<main class="site-main">
				<?php echo do_shortcode('[wpsl_viewer stream="' . intval($stream_id) . '" chat="' . esc_attr($chat) . '" poll="' . esc_attr($poll) . '"]'); ?>
			</main>
			<?php wp_footer(); ?>
			</body>
			</html>
			<?php
			exit;
		}
	}

	/**
	 * Accept either POST['_ajax_nonce'] (recommended) or POST['nonce'] (legacy)
	 */
	private function verify_admin_ajax_nonce_or_die() {
		$nonce = '';
		if (isset($_POST['_ajax_nonce'])) $nonce = (string) wp_unslash($_POST['_ajax_nonce']);
		elseif (isset($_POST['nonce']))   $nonce = (string) wp_unslash($_POST['nonce']);

		if (!$nonce || !wp_verify_nonce($nonce, 'wpsl_admin')) {
			wp_send_json_error('bad nonce', 403);
		}
	}

	public function ajax_create_stream() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();

		$stream_id = $this->create_stream_post();
		if (!$stream_id) wp_send_json_error('create failed', 500);

		$token = get_post_meta($stream_id, '_wpsl_token', true);
		wp_send_json_success([
			'stream_id'   => $stream_id,
			'token'       => $token,
			'playlist'    => $this->stream_url_base($stream_id) . '/index.m3u8',
			'viewer_url'  => add_query_arg(['stream' => $stream_id], site_url('/?wpsl_viewer=1')),
		]);
	}

    public function ajax_rename_stream() {
        if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
        $this->verify_admin_ajax_nonce_or_die();
        $stream_id = intval($_POST['stream_id'] ?? 0);
        $title = sanitize_text_field((string)($_POST['title'] ?? ''));
        if (!$stream_id || $title === '') wp_send_json_error('bad params', 400);
        // Keep slug stable as stream_{ID}
        wp_update_post(['ID' => $stream_id, 'post_title' => $title, 'post_name' => 'stream_' . (int)$stream_id]);
        wp_send_json_success(['ok' => true]);
    }

    public function enforce_stream_slug($post_id, $post, $update) {
        if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) return;
        if (!$post || $post->post_type !== self::CPT) return;
        $desired = 'stream_' . (int)$post_id;
        if ($post->post_name !== $desired) {
            // Avoid recursion
            remove_action('save_post_' . self::CPT, [$this, 'enforce_stream_slug'], 10);
            wp_update_post(['ID' => $post_id, 'post_name' => $desired]);
            add_action('save_post_' . self::CPT, [$this, 'enforce_stream_slug'], 10, 3);
        }
    }

	private function build_viewer_url(int $stream_id): string {
		return add_query_arg(['wpsl_viewer' => 1, 'stream' => $stream_id], site_url('/'));
	}

	private function render_invite_content(int $stream_id, string $subject, string $html): array {
		$subject = trim($subject) !== '' ? $subject : $this->get_option('wpsl_invite_subject', 'Live Stream Invitation');
		$viewer = $this->build_viewer_url($stream_id);
		$post = get_post($stream_id);
		$title = $post ? ( get_the_title($post) ?: ('stream_' . $stream_id) ) : ('stream_' . $stream_id);
		$now = time();
		$start = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $now);
		$repl = [
			'{{viewer_url}}'   => esc_url($viewer),
			'{{stream_id}}'    => (string)$stream_id,
			'{{stream_title}}' => esc_html($title),
			'{{start_time}}'   => esc_html($start),
		];
		$body = $html;
		foreach ($repl as $k => $v) { $body = str_replace($k, $v, $body); }
		return [ $subject, $body ];
	}

	public function ajax_list_users() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();
		$users = get_users([ 'number' => 300, 'orderby' => 'display_name', 'order' => 'ASC' ]);
		$out = [];
		foreach ($users as $u) {
			$email = isset($u->user_email) ? (string)$u->user_email : '';
			if ($email === '' || !is_email($email)) continue;
			$out[] = [ 'id' => (int)$u->ID, 'name' => $u->display_name ?: $u->user_login, 'email' => $email ];
		}
		wp_send_json_success([ 'users' => $out ]);
	}

	public function ajax_send_invite_preview() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();
		$stream_id = intval($_POST['stream_id'] ?? 0);
		$subject = sanitize_text_field((string)($_POST['subject'] ?? ''));
		$message = wp_kses_post((string)($_POST['message'] ?? ''));
		$to = sanitize_email((string)($_POST['to'] ?? ''));
		if (!$to) { $u = wp_get_current_user(); $to = ($u && $u->exists()) ? (string)$u->user_email : get_option('admin_email'); }
		if (!$stream_id || !is_email($to)) wp_send_json_error('bad params', 400);
		list($subj, $body) = $this->render_invite_content($stream_id, $subject, $message !== '' ? $message : $this->get_option('wpsl_invite_template', ''));
		$headers = [ 'Content-Type: text/html; charset=UTF-8' ];
		$ok = wp_mail($to, $subj, $body, $headers);
		if (!$ok) wp_send_json_error('send failed', 500);
		wp_send_json_success([ 'to' => $to ]);
	}

	public function ajax_send_invites() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();
		$stream_id = intval($_POST['stream_id'] ?? 0);
		$subject = sanitize_text_field((string)($_POST['subject'] ?? ''));
		$message = wp_kses_post((string)($_POST['message'] ?? ''));
		$emails_raw = (string)($_POST['emails'] ?? '');
		if (!$stream_id) wp_send_json_error('bad params', 400);
		$parts = preg_split('~[\s,;]+~', $emails_raw, -1, PREG_SPLIT_NO_EMPTY);
		$seen = [];
		$targets = [];
		foreach ($parts as $p) {
			$e = sanitize_email($p);
			if ($e && is_email($e) && !isset($seen[$e])) { $seen[$e] = true; $targets[] = $e; }
		}
		$results = [];
		$sent = 0; $failed = 0; $errors = [];
		foreach ($targets as $to) {
			list($subj, $body) = $this->render_invite_content($stream_id, $subject, $message !== '' ? $message : $this->get_option('wpsl_invite_template', ''));
			$headers = [ 'Content-Type: text/html; charset=UTF-8' ];
			$ok = wp_mail($to, $subj, $body, $headers);
			if ($ok) { $sent++; $results[] = ['email'=>$to,'ok'=>true]; }
			else { $failed++; $results[] = ['email'=>$to,'ok'=>false]; $errors[] = 'wp_mail failed'; }
		}
		wp_send_json_success([ 'sent'=>$sent, 'failed'=>$failed, 'results'=>$results, 'errors'=>$errors ]);
	}

	public function ajax_rec_rename() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$nonce = isset($_POST['nonce']) ? (string)$_POST['nonce'] : '';
		if (!$nonce || !wp_verify_nonce($nonce, 'wpsl_admin')) wp_send_json_error('bad nonce', 403);
		$stream_id = intval($_POST['stream_id'] ?? 0);
		$title = sanitize_text_field((string)($_POST['rec_title'] ?? ''));
		if (!$stream_id) wp_send_json_error('bad params', 400);
		if ($title === '') $title = 'stream_' . $stream_id;
		wp_update_post(['ID' => $stream_id, 'post_title' => $title]);
		wp_send_json_success(['ok'=>true, 'title'=>$title]);
	}

    public function ajax_save_description() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();
		$stream_id = intval($_POST['stream_id'] ?? 0);
		$content = isset($_POST['content']) ? wp_kses_post((string)$_POST['content']) : '';
		if (!$stream_id) wp_send_json_error('bad params', 400);
		wp_update_post([
			'ID' => $stream_id,
			'post_content' => $content,
		]);
        wp_send_json_success(['ok' => true]);
    }

	public function ajax_get_description() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();
		$stream_id = intval($_POST['stream_id'] ?? 0);
		if (!$stream_id) wp_send_json_error('bad params', 400);
		$p = get_post($stream_id);
		if (!$p || $p->post_type !== self::CPT) wp_send_json_error('not found', 404);
		wp_send_json_success([ 'content' => $p->post_content ]);
	}

    private function delete_stream_folder(int $stream_id) {
        $upload = wp_upload_dir();
        $base_dir = trailingslashit($upload['basedir']) . 'wpsl/';
        $dir = trailingslashit($base_dir) . intval($stream_id);
        $real_base = @realpath($base_dir);
        $real_dir  = @realpath($dir);
        if (!$real_base || !$real_dir) return;
        if (strpos($real_dir, $real_base) !== 0 || !is_dir($real_dir)) return;
        $stack = [$real_dir];
        while (!empty($stack)) {
            $cur = array_pop($stack);
            $entries = @scandir($cur);
            if ($entries === false) continue;
            foreach ($entries as $e) {
                if ($e === '.' || $e === '..') continue;
                $path = $cur . DIRECTORY_SEPARATOR . $e;
                if (is_dir($path)) $stack[] = $path; else @unlink($path);
            }
            @rmdir($cur);
        }
    }

    public function maybe_delete_stream_files($post_id) {
        $p = get_post($post_id);
        if (!$p || $p->post_type !== self::CPT) return;
        $this->delete_stream_folder((int)$post_id);
    }

	public function handle_delete_recording() {
		if (!current_user_can('manage_options')) wp_die('forbidden');
		check_admin_referer('wpsl_rec_actions', 'wpsl_rec_nonce');
		$stream_id = intval($_POST['stream_id'] ?? 0);
		if ($stream_id) {
			$this->delete_stream_folder($stream_id);
			if (get_post_type($stream_id) === self::CPT) { wp_delete_post($stream_id, true); }
		}
		$redirect = wp_get_referer() ?: admin_url('admin.php?page=wpsl_recordings');
		wp_safe_redirect($redirect);
		exit;
	}

	public function handle_bulk_delete() {
		if (!current_user_can('manage_options')) wp_die('forbidden');
		check_admin_referer('wpsl_rec_actions', 'wpsl_rec_nonce');
		$ids = isset($_POST['ids']) && is_array($_POST['ids']) ? array_map('intval', $_POST['ids']) : [];
		foreach ($ids as $sid) {
			if (!$sid) continue;
			$this->delete_stream_folder($sid);
			if (get_post_type($sid) === self::CPT) { wp_delete_post($sid, true); }
		}
		$redirect = wp_get_referer() ?: admin_url('admin.php?page=wpsl_recordings');
		wp_safe_redirect($redirect);
		exit;
	}

	public function ajax_create_viewer_page() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();

		$stream_id = intval($_POST['stream_id'] ?? 0);
		if (!$stream_id) wp_send_json_error('bad stream_id', 400);

		$allow_chat = (isset($_POST['allow_chat']) && $_POST['allow_chat'] === '1') ? '1' : '0';

		$page_id = wp_insert_post([
			'post_type'    => 'page',
			'post_status'  => 'publish',
			'post_title'   => 'Live Stream #' . $stream_id,
			'post_content' => '[wpsl_viewer stream="' . $stream_id . '" chat="' . $allow_chat . '" poll="' . esc_attr(isset($_POST['poll']) ? (string)$_POST['poll'] : '1.5') . '"]',
		]);

		if (is_wp_error($page_id) || !$page_id) wp_send_json_error('page create failed', 500);

		update_post_meta($stream_id, '_wpsl_viewer_page_id', $page_id);

		wp_send_json_success([
			'page_id' => $page_id,
			'url'     => get_permalink($page_id),
		]);
	}

	public function ajax_save_access() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();
		$stream_id = intval($_POST['stream_id'] ?? 0);
		$mode = sanitize_text_field((string)($_POST['access'] ?? 'public'));
		if (!$stream_id) wp_send_json_error('bad stream_id', 400);
		if (!in_array($mode, [self::ACCESS_PUBLIC, self::ACCESS_PASSWORD, self::ACCESS_PAYWALL], true)) $mode = self::ACCESS_PUBLIC;
		update_post_meta($stream_id, '_wpsl_access', $mode);
		// Optional: require WP login to view
		$require_login = isset($_POST['require_login']) && ((string)$_POST['require_login'] === '1');
		update_post_meta($stream_id, '_wpsl_require_login', $require_login ? '1' : '0');
		// Optionally enable site registration if requested alongside login requirement
		if ($require_login && isset($_POST['allow_register']) && ((string)$_POST['allow_register'] === '1')) {
			update_option('users_can_register', 1);
		}
		// Optional: allow non-admin viewers to see user list
		$users_visible = isset($_POST['users_visible']) && ((string)$_POST['users_visible'] === '1');
		update_post_meta($stream_id, '_wpsl_users_visible', $users_visible ? '1' : '0');
		if ($mode === self::ACCESS_PASSWORD) {
			$pw = (string)($_POST['password'] ?? '');
			$pw = wp_unslash($pw);
			if ($pw !== '') {
				$hash = wp_hash_password($pw);
				update_post_meta($stream_id, '_wpsl_password_hash', $hash);
			}
		}
		if ($mode === self::ACCESS_PAYWALL) {
			$pc = intval($_POST['price_cents'] ?? 0);
			$cur = sanitize_text_field((string)($_POST['currency'] ?? $this->get_option('wpsl_default_currency', 'usd')));
			update_post_meta($stream_id, '_wpsl_price_cents', max(0, $pc));
			update_post_meta($stream_id, '_wpsl_currency', $cur ?: 'usd');
		}
		wp_send_json_success(['ok' => true]);
	}

	public function ajax_get_access() {
		if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
		$this->verify_admin_ajax_nonce_or_die();
		$stream_id = intval($_POST['stream_id'] ?? 0);
		if (!$stream_id) wp_send_json_error('bad stream_id', 400);
		$mode = get_post_meta($stream_id, '_wpsl_access', true) ?: self::ACCESS_PUBLIC;
		$require_login = get_post_meta($stream_id, '_wpsl_require_login', true) === '1' ? '1' : '0';
		$users_visible = get_post_meta($stream_id, '_wpsl_users_visible', true);
		$users_visible = ($users_visible === '' ? '1' : ($users_visible === '1' ? '1' : '0'));
		$price_cents = intval(get_post_meta($stream_id, '_wpsl_price_cents', true) ?: 0);
		$currency = get_post_meta($stream_id, '_wpsl_currency', true) ?: $this->get_option('wpsl_default_currency', 'usd');
		$reg = get_option('users_can_register') ? 1 : 0;
		wp_send_json_success([
			'access' => $mode,
			'require_login' => $require_login,
			'users_visible' => $users_visible,
			'price_cents' => $price_cents,
			'currency' => $currency,
			'users_can_register' => $reg,
		]);
	}

	public function rest_access_password(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		$pw = (string)$req->get_param('password');
		if (!$stream_id || $pw === '') return new WP_Error('bad_request', 'missing', ['status' => 400]);
		$mode = get_post_meta($stream_id, '_wpsl_access', true);
		if ($mode !== self::ACCESS_PASSWORD) return new WP_Error('forbidden', 'not password protected', ['status' => 403]);
		$hash = get_post_meta($stream_id, '_wpsl_password_hash', true);
		if (!$hash || !wp_check_password($pw, $hash)) return new WP_Error('forbidden', 'bad password', ['status' => 403]);
		$this->grant_password_access($stream_id);
		return ['ok' => true];
	}

    public function rest_paywall_session(WP_REST_Request $req) {
        $stream_id = intval($req->get_param('stream_id'));
        $return_url = esc_url_raw((string)$req->get_param('return_url'));
        if (!$stream_id || !$return_url) return new WP_Error('bad_request', 'missing', ['status' => 400]);
        if (!is_user_logged_in()) return new WP_Error('forbidden', 'login required', ['status' => 401]);
		$sk = $this->get_option('wpsl_stripe_sk');
		if (!$sk) return new WP_Error('server_error', 'stripe not configured', ['status' => 500]);
		$price_cents = intval(get_post_meta($stream_id, '_wpsl_price_cents', true) ?: 0);
		$currency = get_post_meta($stream_id, '_wpsl_currency', true) ?: $this->get_option('wpsl_default_currency', 'usd');
		if ($price_cents <= 0) return new WP_Error('server_error', 'invalid price', ['status' => 500]);
		$success = add_query_arg(['session_id' => '{CHECKOUT_SESSION_ID}'], $return_url);
		$cancel  = $return_url;
		$body = [
			'mode' => 'payment',
			'success_url' => $success,
			'cancel_url' => $cancel,
			'line_items[0][quantity]' => 1,
			'line_items[0][price_data][currency]' => $currency,
			'line_items[0][price_data][unit_amount]' => $price_cents,
			'line_items[0][price_data][product_data][name]' => 'Stream #' . $stream_id,
		];
		$resp = wp_remote_post('https://api.stripe.com/v1/checkout/sessions', [
			'timeout' => 10,
			'headers' => [ 'Authorization' => 'Bearer ' . $sk ],
			'body'    => $body,
		]);
		$code = wp_remote_retrieve_response_code($resp);
		$data = json_decode(wp_remote_retrieve_body($resp), true);
		if ($code !== 200 || !is_array($data) || empty($data['url'])) {
			return new WP_Error('stripe_error', 'cannot create session', ['status' => 500]);
		}
		return ['url' => esc_url_raw($data['url'])];
	}

	public function register_rest_routes() {
		register_rest_route('wpsl/v1', '/upload', [
			'methods'             => 'POST',
			'permission_callback' => '__return_true',
			'callback'            => [$this, 'rest_upload'],
		]);

		register_rest_route('wpsl/v1', '/cleanup', [
			'methods'             => 'POST',
			'permission_callback' => '__return_true',
			'callback'            => [$this, 'rest_cleanup'],
		]);

		register_rest_route('wpsl/v1', '/chat/send', [
			'methods'             => 'POST',
			'permission_callback' => '__return_true',
			'callback'            => [$this, 'rest_chat_send'],
		]);

		register_rest_route('wpsl/v1', '/chat/poll', [
			'methods'             => 'GET',
			'permission_callback' => '__return_true',
			'callback'            => [$this, 'rest_chat_poll'],
		]);

		// moderation endpoints (admin only)
		register_rest_route('wpsl/v1', '/chat/mod/banned_list', [
			'methods'             => 'GET',
			'permission_callback' => function() { return current_user_can('manage_options'); },
			'callback'            => [$this, 'rest_chat_banned_list'],
		]);

		register_rest_route('wpsl/v1', '/chat/mod/unban', [
			'methods'             => 'POST',
			'permission_callback' => function() { return current_user_can('manage_options'); },
			'callback'            => [$this, 'rest_chat_unban'],
		]);

		register_rest_route('wpsl/v1', '/chat/mod/list', [
			'methods'             => 'GET',
			'permission_callback' => function() { return current_user_can('manage_options'); },
			'callback'            => [$this, 'rest_chat_list'],
		]);

		register_rest_route('wpsl/v1', '/chat/mod/delete', [
			'methods'             => 'POST',
			'permission_callback' => function() { return current_user_can('manage_options'); },
			'callback'            => [$this, 'rest_chat_delete'],
		]);

		register_rest_route('wpsl/v1', '/chat/mod/ban', [
			'methods'             => 'POST',
			'permission_callback' => function() { return current_user_can('manage_options'); },
			'callback'            => [$this, 'rest_chat_ban'],
		]);

		// Access (password) and Paywall (Stripe) endpoints
		register_rest_route('wpsl/v1', '/access/password', [
			'methods'             => 'POST',
			'permission_callback' => '__return_true',
			'callback'            => [$this, 'rest_access_password'],
		]);

		register_rest_route('wpsl/v1', '/paywall/session', [
			'methods'             => 'POST',
			'permission_callback' => '__return_true',
			'callback'            => [$this, 'rest_paywall_session'],
		]);

		// Presence (online users)
		register_rest_route('wpsl/v1', '/presence/ping', [
			'methods'             => 'POST',
			'permission_callback' => '__return_true',
			'callback'            => [$this, 'rest_presence_ping'],
		]);
		register_rest_route('wpsl/v1', '/presence/list', [
			'methods'             => 'GET',
			'permission_callback' => function() { return current_user_can('manage_options'); },
			'callback'            => [$this, 'rest_presence_list'],
		]);
        register_rest_route('wpsl/v1', '/presence/stats', [
            'methods'             => 'GET',
            'permission_callback' => function() { return current_user_can('manage_options'); },
            'callback'            => [$this, 'rest_presence_stats'],
        ]);

        // Public presence list (names only) when enabled on the stream
        register_rest_route('wpsl/v1', '/presence/public', [
            'methods'             => 'GET',
            'permission_callback' => '__return_true',
            'callback'            => [$this, 'rest_presence_public'],
        ]);
	}

	private function verify_token_or_403($stream_id, $token) {
		$real = get_post_meta($stream_id, '_wpsl_token', true);
		if (!$real || !hash_equals($real, $token)) {
			return new WP_Error('forbidden', 'forbidden', ['status' => 403]);
		}
		return true;
	}

	public function rest_upload(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		$token     = (string)$req->get_param('token');
		$name      = (string)$req->get_param('name');

		if (!$stream_id || !$token || !$name) {
			return new WP_Error('bad_request', 'missing params', ['status' => 400]);
		}

		$ok = $this->verify_token_or_403($stream_id, $token);
		if (is_wp_error($ok)) return $ok;

		if (!preg_match('~^(init\.mp4|index\.m3u8|vod\.m3u8|seg_\d{6}\.m4s|latest\.txt|poster\.(?:jpg|jpeg|png))$~', $name)) {
			return new WP_Error('bad_name', 'bad name', ['status' => 400]);
		}

		$dir  = $this->ensure_stream_dir($stream_id);
		$body = file_get_contents('php://input');
		if ($body === false) return new WP_Error('read_error', 'read error', ['status' => 500]);

		$tmp  = $dir . '/.' . $name . '.tmp.' . bin2hex(random_bytes(4));
		$path = $dir . '/' . $name;

		if (file_put_contents($tmp, $body, LOCK_EX) === false) {
			return new WP_Error('write_failed', 'write failed', ['status' => 500]);
		}

		if (!rename($tmp, $path)) {
			@unlink($tmp);
			return new WP_Error('rename_failed', 'rename failed', ['status' => 500]);
		}

		return ['ok' => true];
	}

	public function rest_cleanup(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		$token     = (string)$req->get_param('token');

		if (!$stream_id || !$token) return new WP_Error('bad_request', 'missing', ['status' => 400]);

		$ok = $this->verify_token_or_403($stream_id, $token);
		if (is_wp_error($ok)) return $ok;

		$dir = $this->ensure_stream_dir($stream_id);
		$files = glob($dir . '/*');
		if ($files) foreach ($files as $f) { if (is_file($f)) @unlink($f); }

		return ['ok' => true];
	}

	private function client_ip(): string {
		// Keep it simple and safe: do NOT trust X-Forwarded-For here (can be spoofed).
		$ip = $_SERVER['REMOTE_ADDR'] ?? '';
		$ip = is_string($ip) ? trim($ip) : '';
		return $ip !== '' ? $ip : '0.0.0.0';
	}

	private function presence_key(int $stream_id): string {
		return 'wpsl_presence_' . $stream_id;
	}

	private function presence_unique_key(int $stream_id): string {
		return 'wpsl_presence_uq_' . $stream_id;
	}

	public function rest_presence_ping(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		$uid = substr(preg_replace('~[^a-zA-Z0-9_-]~', '', (string)$req->get_param('uid')), 0, 64);
		$name = trim((string)$req->get_param('name'));
		if (!$stream_id || $uid === '') return new WP_Error('bad_request', 'missing', ['status' => 400]);
		$name = mb_substr($name !== '' ? $name : 'Guest', 0, 80);
		$key = $this->presence_key($stream_id);
		$now = time();
		$ip = $this->client_ip();
		$map = get_transient($key);
		$map = is_array($map) ? $map : [];
		// prune old entries (>45s)
		foreach ($map as $k => $row) {
			$lt = isset($row['t']) ? intval($row['t']) : 0;
			if ($now - $lt > 45) unset($map[$k]);
		}
		$map[$uid] = [ 'n' => $name, 't' => $now, 'ip' => $ip ];
		set_transient($key, $map, 2 * MINUTE_IN_SECONDS);

		// track uniques (24h)
		$ukey = $this->presence_unique_key($stream_id);
		$uniq = get_transient($ukey);
		$uniq = is_array($uniq) ? $uniq : [];
		// prune > 24h
		foreach ($uniq as $k => $ts) {
			if ($now - intval($ts) > DAY_IN_SECONDS) unset($uniq[$k]);
		}
		if (!isset($uniq[$uid])) $uniq[$uid] = $now;
		set_transient($ukey, $uniq, DAY_IN_SECONDS + HOUR_IN_SECONDS);
		return ['ok' => true];
	}

	public function rest_presence_list(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		if (!$stream_id) return new WP_Error('bad_request', 'missing stream_id', ['status' => 400]);
		$key = $this->presence_key($stream_id);
		$map = get_transient($key);
		$map = is_array($map) ? $map : [];
		$now = time();
		$out = [];
		foreach ($map as $uid => $row) {
			$lt = isset($row['t']) ? intval($row['t']) : 0;
			if ($now - $lt > 45) continue;
			$out[] = [
				'uid' => (string)$uid,
				'name' => isset($row['n']) ? (string)$row['n'] : 'Guest',
				'last' => $lt,
			];
		}
		usort($out, function($a, $b){ return ($b['last'] <=> $a['last']); });
		return [ 'count' => count($out), 'users' => $out ];
	}

	public function rest_presence_stats(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		if (!$stream_id) return new WP_Error('bad_request', 'missing stream_id', ['status' => 400]);
		$now = time();
		$map = get_transient($this->presence_key($stream_id));
		$map = is_array($map) ? $map : [];
		$online = 0;
		foreach ($map as $row) {
			$lt = isset($row['t']) ? intval($row['t']) : 0;
			if ($now - $lt <= 45) $online++;
		}
		$uniq = get_transient($this->presence_unique_key($stream_id));
		$uniq = is_array($uniq) ? $uniq : [];
		return [ 'online' => $online, 'unique_24h' => count($uniq) ];
	}

	public function rest_presence_public(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		if (!$stream_id) return new WP_Error('bad_request', 'missing stream_id', ['status' => 400]);
		$visible = get_post_meta($stream_id, '_wpsl_users_visible', true);
		if ($visible !== '1') return new WP_Error('forbidden', 'hidden', ['status' => 403]);
		$key = $this->presence_key($stream_id);
		$map = get_transient($key);
		$map = is_array($map) ? $map : [];
		$now = time();
		$out = [];
		foreach ($map as $uid => $row) {
			$lt = isset($row['t']) ? intval($row['t']) : 0;
			if ($now - $lt > 45) continue;
			$out[] = [ 'name' => isset($row['n']) ? (string)$row['n'] : 'Guest' ];
		}
		return [ 'count' => count($out), 'users' => $out ];
	}

	private function rate_limit_chat_or_error(int $stream_id): ?WP_Error {
		$ip = $this->client_ip();
		$key_base = 'wpsl_rl_' . $stream_id . '_' . md5($ip);

		// 1) Minimum interval
		$last = get_transient($key_base . '_last');
		$now = microtime(true);
		if (is_numeric($last)) {
			$delta = $now - (float)$last;
			if ($delta < self::CHAT_MIN_INTERVAL_SECONDS) {
				return new WP_Error('rate_limited', 'rate limited', ['status' => 429]);
			}
		}
		set_transient($key_base . '_last', (string)$now, 30);

		// 2) Burst window
		$win_key = $key_base . '_win';
		$win = get_transient($win_key);
		$win = is_array($win) ? $win : ['t' => $now, 'c' => 0];

		if (($now - (float)$win['t']) > self::CHAT_BURST_WINDOW_SECONDS) {
			$win = ['t' => $now, 'c' => 0];
		}

		$win['c'] = (int)$win['c'] + 1;
		set_transient($win_key, $win, self::CHAT_BURST_WINDOW_SECONDS + 5);

		if ((int)$win['c'] > self::CHAT_BURST_MAX_MESSAGES) {
			return new WP_Error('rate_limited', 'rate limited', ['status' => 429]);
		}

		return null;
	}

	public function rest_chat_send(WP_REST_Request $req) {
		global $wpdb;

		$stream_id = intval($req->get_param('stream_id'));
		$name      = trim((string)$req->get_param('name'));
		$msg       = trim((string)$req->get_param('message'));

		if (!$stream_id || $name === '' || $msg === '') {
			return new WP_Error('bad_request', 'missing', ['status' => 400]);
		}

		// Server-side anti-flood (bots bypass JS)
		$rl = $this->rate_limit_chat_or_error($stream_id);
		if ($rl) return $rl;

		// If logged in, prefer WP username
		if (is_user_logged_in()) {
			$u = wp_get_current_user();
			$name = $u && $u->exists() ? ( $u->display_name ?: $u->user_login ) : $name;
		}

		$name = mb_substr($name, 0, 80);

		// moderation checks (ban by name or IP)
		$ip = $this->client_ip();
		$banned_names = array_map('strval', (array) get_post_meta($stream_id, '_wpsl_banned_names', true));
		$banned_ips   = array_map('strval', (array) get_post_meta($stream_id, '_wpsl_banned_ips', true));

		$ln = mb_strtolower($name);
		$bn = array_map('mb_strtolower', $banned_names);

		if (in_array($ln, $bn, true) || ($ip && in_array($ip, $banned_ips, true))) {
			return new WP_Error('forbidden', 'banned', ['status' => 403]);
		}

		$msg = mb_substr($msg, 0, 1000);

		$table = $wpdb->prefix . self::CHAT_TABLE;
		$wpdb->insert($table, [
			'stream_id'   => $stream_id,
			'created_at'  => current_time('mysql'),
			'user_name'   => $name,
			'message'     => $msg,
		], ['%d','%s','%s','%s']);

		return ['ok' => true, 'id' => (int)$wpdb->insert_id];
	}

	public function rest_chat_poll(WP_REST_Request $req) {
		global $wpdb;

		$stream_id = intval($req->get_param('stream_id'));
		$after_id  = intval($req->get_param('after_id'));

		if (!$stream_id) return new WP_Error('bad_request', 'missing stream_id', ['status' => 400]);

		$table = $wpdb->prefix . self::CHAT_TABLE;
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, created_at, user_name, message
				 FROM {$table}
				 WHERE stream_id = %d AND id > %d
				 ORDER BY id ASC
				 LIMIT 50",
				$stream_id, $after_id
			),
			ARRAY_A
		);

		// filter banned names for everyone
		$banned_names = array_map('mb_strtolower', array_map('strval', (array) get_post_meta($stream_id, '_wpsl_banned_names', true)));
		if (!empty($banned_names) && !empty($rows)) {
			$rows = array_values(array_filter($rows, function($r) use ($banned_names) {
				$n = isset($r['user_name']) ? mb_strtolower((string)$r['user_name']) : '';
				return $n === '' || !in_array($n, $banned_names, true);
			}));
		}

		return ['messages' => $rows ?: []];
	}

	public function rest_chat_banned_list(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		if (!$stream_id) return new WP_Error('bad_request', 'missing stream_id', ['status' => 400]);

		$names = (array) get_post_meta($stream_id, '_wpsl_banned_names', true);
		$ips   = (array) get_post_meta($stream_id, '_wpsl_banned_ips', true);

		return [
			'names' => array_values(array_unique(array_map('strval', $names))),
			'ips'   => array_values(array_unique(array_map('strval', $ips))),
		];
	}

	public function rest_chat_unban(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		$name      = trim((string)$req->get_param('name'));
		$ip        = trim((string)$req->get_param('ip'));

		if (!$stream_id || ($name === '' && $ip === '')) return new WP_Error('bad_request', 'missing', ['status' => 400]);

		$names = (array) get_post_meta($stream_id, '_wpsl_banned_names', true);
		$ips   = (array) get_post_meta($stream_id, '_wpsl_banned_ips', true);

		if ($name !== '') {
			$names = array_values(array_filter($names, function($n) use ($name){
				return mb_strtolower((string)$n) !== mb_strtolower($name);
			}));
		}
		if ($ip !== '') {
			$ips = array_values(array_filter($ips, function($x) use ($ip){
				return (string)$x !== (string)$ip;
			}));
		}

		update_post_meta($stream_id, '_wpsl_banned_names', $names);
		update_post_meta($stream_id, '_wpsl_banned_ips', $ips);

		return ['ok' => true];
	}

	public function rest_chat_list(WP_REST_Request $req) {
		global $wpdb;

		$stream_id = intval($req->get_param('stream_id'));
		$limit = min(200, max(1, intval($req->get_param('limit') ?: 100)));

		if (!$stream_id) return new WP_Error('bad_request', 'missing stream_id', ['status' => 400]);

		$table = $wpdb->prefix . self::CHAT_TABLE;
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, created_at, user_name, message
				 FROM {$table}
				 WHERE stream_id = %d
				 ORDER BY id DESC
				 LIMIT %d",
				$stream_id, $limit
			),
			ARRAY_A
		);

		return ['messages' => $rows ?: []];
	}

	public function rest_chat_delete(WP_REST_Request $req) {
		global $wpdb;

		$stream_id = intval($req->get_param('stream_id'));
		$id = intval($req->get_param('id'));

		if (!$stream_id || !$id) return new WP_Error('bad_request', 'missing', ['status' => 400]);

		$table = $wpdb->prefix . self::CHAT_TABLE;
		$wpdb->delete($table, ['id' => $id, 'stream_id' => $stream_id], ['%d','%d']);

		return ['ok' => true];
	}

	public function rest_chat_ban(WP_REST_Request $req) {
		$stream_id = intval($req->get_param('stream_id'));
		$name      = trim((string)$req->get_param('name'));
		$ip        = trim((string)$req->get_param('ip'));

		if (!$stream_id || ($name === '' && $ip === '')) return new WP_Error('bad_request', 'missing', ['status' => 400]);

		$names = (array) get_post_meta($stream_id, '_wpsl_banned_names', true);
		$ips   = (array) get_post_meta($stream_id, '_wpsl_banned_ips', true);

		if ($name !== '') $names[] = $name;
		if ($ip !== '')   $ips[]   = $ip;

		update_post_meta($stream_id, '_wpsl_banned_names', array_values(array_unique(array_map('strval', $names))));
		update_post_meta($stream_id, '_wpsl_banned_ips', array_values(array_unique(array_map('strval', $ips))));

		return ['ok' => true];
	}

	public function shortcode_viewer($atts) {
		$default_poll = (string) get_option('wpsl_default_poll', '1.5');
		$atts = shortcode_atts(['stream' => '0', 'chat' => '1', 'poll' => $default_poll], $atts);
		$stream_id = intval($atts['stream']);
		if (!$stream_id) return '<p>Missing stream id.</p>';

		$chat_enabled = ($atts['chat'] !== '0');
		$base = $this->stream_url_base($stream_id);
		$playlist = esc_url($base . '/index.m3u8');
		$vod = $base . '/vod.m3u8';
		$vod_exists = wp_remote_retrieve_response_code(wp_remote_head($vod, ['timeout' => 2])) === 200;

		$poll_s = (string)$atts['poll'];
		$poll_f = floatval($poll_s);
		if ($poll_f <= 0) $poll_f = 1.5;
		$poll_ms = (int)round(min(5.0, max(0.5, $poll_f)) * 1000);



		$current = null;
		if (is_user_logged_in()) {
			$u = wp_get_current_user();
			$current = $u && $u->exists() ? ( $u->display_name ?: $u->user_login ) : null;
		}

		// Enforce access
		$access = get_post_meta($stream_id, '_wpsl_access', true) ?: self::ACCESS_PUBLIC;
		$require_login = get_post_meta($stream_id, '_wpsl_require_login', true) === '1';
		if ($require_login && !is_user_logged_in()) {
			ob_start(); ?>
			<div class="wpsl-viewer wpsl-login-required">
				<div class="wpsl-login-wrap">
					<div class="wpsl-login-card">
						<h3 class="wpsl-login-title">Login Required</h3>
						<p class="wpsl-login-sub">Please log in to view this stream.</p>
						<p>
							<a class="button button-primary" href="<?php echo esc_url( wp_login_url( get_permalink() ) ); ?>">Log In</a>
							<?php if (get_option('users_can_register')): ?>
								<a class="button" href="<?php echo esc_url( wp_registration_url() ); ?>">Register</a>
							<?php endif; ?>
						</p>
					</div>
				</div>
			</div>
			<?php return ob_get_clean();
		}
		if ($access === self::ACCESS_PASSWORD && !$this->has_password_access($stream_id)) {
			ob_start(); ?>
			<div class="wpsl-viewer">
				<div class="wpsl-pass-wrap">
					<div class="wpsl-pass-card">
						<div class="wpsl-pass-icon" aria-hidden="true">&#x1F512;</div>
						<h3 class="wpsl-pass-title">Protected Stream</h3>
						<p class="wpsl-pass-sub">Enter password to view</p>
						<div class="wpsl-pass-field"><input type="password" id="wpsl-pass" placeholder="Password" /></div>
						<button id="wpsl-pass-btn" class="wpsl-pass-btn" type="button">Unlock</button>
						<div id="wpsl-pass-err" class="wpsl-pass-err"></div>
					</div>
				</div>
				<script>
				(function(){
					const btn = document.getElementById('wpsl-pass-btn');
					btn?.addEventListener('click', async () => {
						const pw = (document.getElementById('wpsl-pass')?.value||'').trim();
						if (!pw) return;
						try {
							const res = await fetch('<?php echo esc_js(esc_url_raw(rest_url('wpsl/v1/access/password'))); ?>', {
								method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},
								body:new URLSearchParams({stream_id:'<?php echo (int)$stream_id; ?>', password: pw})
							});
							if (!res.ok) throw new Error('Bad password');
							location.reload();
						} catch(e){
							document.getElementById('wpsl-pass-err').textContent = 'Incorrect password';
						}
					});
				})();
				</script>
			</div>
			<?php return ob_get_clean();
		}
        if ($access === self::ACCESS_PAYWALL && !$this->has_pay_access($stream_id)) {
            // Require login before purchase
            if (!is_user_logged_in()) {
                ob_start(); ?>
                <div class="wpsl-viewer wpsl-login-required" style="text-align:center;max-width:520px;margin:24px auto;">
                    <h3>Login Required</h3>
                    <p>Please log in to purchase access to this video.</p>
                    <p>
                        <a class="button button-primary" href="<?php echo esc_url( wp_login_url( get_permalink() ) ); ?>">Log In</a>
                        <?php if (get_option('users_can_register')): ?>
                            <a class="button" href="<?php echo esc_url( wp_registration_url() ); ?>">Register</a>
                        <?php endif; ?>
                    </p>
                </div>
                <?php return ob_get_clean();
            }

            $price_cents = intval(get_post_meta($stream_id, '_wpsl_price_cents', true) ?: 0);
            $currency = get_post_meta($stream_id, '_wpsl_currency', true) ?: $this->get_option('wpsl_default_currency', 'usd');
            $login_url = wp_login_url( get_permalink() );
            ob_start(); ?>
                <div class="wpsl-viewer" style="text-align:center;max-width:520px;margin:24px auto;">
                    <h3>Unlock stream</h3>
                    <p>Price: <strong><?php echo esc_html(strtoupper($currency)); ?> <?php echo esc_html(number_format_i18n($price_cents/100, 2)); ?></strong></p>
                    <button id="wpsl-pay-btn" class="button button-primary">Pay with Stripe</button>
                    <div id="wpsl-pay-err" style="color:#b00020;margin-top:6px;"></div>
                    <script>
                    (function(){
                        document.getElementById('wpsl-pay-btn')?.addEventListener('click', async () => {
                            try {
                        const res = await fetch('<?php echo esc_js(esc_url_raw(rest_url('wpsl/v1/paywall/session'))); ?>', { method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8','X-WP-Nonce':'<?php echo esc_js( wp_create_nonce('wp_rest') ); ?>'}, body:new URLSearchParams({ stream_id:'<?php echo (int)$stream_id; ?>', return_url: window.location.href.split('#')[0] }) });
                                const json = await res.json();
                                if (res.status === 401) {
                                    const msg = 'Tu purchase this video, you need to be loggedon';
                                    document.getElementById('wpsl-pay-err').textContent = msg;
                                    setTimeout(function(){ window.location.href = '<?php echo esc_js( esc_url( $login_url ) ); ?>'; }, 3000);
                                    return;
                                }
                                if (!res.ok || !json.url) throw new Error('Failed');
                                window.location.href = json.url;
                            } catch(e){ document.getElementById('wpsl-pay-err').textContent = 'Payment init failed'; }
                        });
                    })();
                    </script>
                </div>
			<?php return ob_get_clean();
		}

        wp_enqueue_style('wpsl-viewer', plugin_dir_url(__FILE__) . 'assets/css/viewer.css?cache=' . time(), [], '1.0.0');
        wp_enqueue_script('wpsl-hls', plugin_dir_url(__FILE__) . 'assets/js/hls.min.js', [], '1.0.0', true);
        wp_enqueue_script('wpsl-viewer', plugin_dir_url(__FILE__) . 'assets/js/viewer.js?cache=' . time(), ['wpsl-hls'], '1.0.0', true);

        wp_localize_script('wpsl-viewer', 'WPSL_VIEW', [
            'rest'         => esc_url_raw(rest_url('wpsl/v1')),
            'stream_id'    => $stream_id,
            'playlist'     => $playlist,
            'vod_playlist' => $vod_exists ? esc_url($vod) : '',
            'chat_enabled' => $chat_enabled ? 1 : 0,
            'user_name'    => $current,
            'can_moderate' => current_user_can('manage_options') ? 1 : 0,
            'nonce'        => wp_create_nonce('wp_rest'),
            'hls_url'      => plugin_dir_url(__FILE__) . 'assets/js/hls.min.js',
            'poll_ms'      => $poll_ms,
            'users_visible'=> (get_post_meta($stream_id, '_wpsl_users_visible', true) === '0') ? 0 : 1,
        ]);

		$poster_url = esc_url($base . '/poster.jpg');
		ob_start(); ?>
		<div class="wpsl-viewer">

			<video id="wpsl-video" controls autoplay muted playsinline poster="<?php echo $poster_url; ?>"></video>
			<?php if ($chat_enabled): ?>
				<?php $users_visible = (get_post_meta($stream_id, '_wpsl_users_visible', true) !== '0'); $is_admin = current_user_can('manage_options'); ?>
				<div class="wpsl-chat<?php echo (!$users_visible && !$is_admin) ? ' wpsl-no-users' : ''; ?>">
					<?php if ($is_admin || $users_visible): ?>
						<button type="button" id="wpsl-toggle-users" class="wpsl-toggle-users">Hide Users</button>
					<?php endif; ?>
					<div class="wpsl-chat-messages" id="wpsl-messages"></div>
					<?php if ($is_admin || $users_visible): ?>
						<aside class="wpsl-chat-users" id="wpsl-users-sidebar">
							<div class="wpsl-users-header">
								<div class="wpsl-online-count" id="wpsl-online-count">Online: 0</div>
								<div class="wpsl-stats" id="wpsl-stats">Unique (24h): 0</div>
							</div>
							<div class="wpsl-users-list" id="wpsl-users-list"></div>
						</aside>
					<?php endif; ?>
					<div class="wpsl-chat-form">
						<input id="wpsl-name" placeholder="Name" maxlength="80" />
						<input id="wpsl-msg" placeholder="Message&hellip;" maxlength="1000" />
						<button id="wpsl-emoji-btn" type="button" aria-haspopup="true" aria-expanded="false" title="Insert emoji">&#x1F60A;</button>
						<button id="wpsl-send" type="button">Send</button>
						<div id="wpsl-emoji-popup" class="wpsl-emoji-popup" style="display:none;" role="dialog" aria-label="Choose an emoji">
							<button type="button" data-emoji="&#x1F600;">&#x1F600;</button>
							<button type="button" data-emoji="&#x1F602;">&#x1F602;</button>
							<button type="button" data-emoji="&#x2764;&#xFE0F;">&#x2764;&#xFE0F;</button>
							<button type="button" data-emoji="&#x1F44D;">&#x1F44D;</button>
							<button type="button" data-emoji="&#x1F389;">&#x1F389;</button>
							<button type="button" data-emoji="&#x1F525;">&#x1F525;</button>
							<button type="button" data-emoji="&#x1F64F;">&#x1F64F;</button>
							<button type="button" data-emoji="&#x1F680;">&#x1F680;</button>
						</div>
					</div>
				</div>
			<?php endif; ?>
		</div>
		<?php
		return ob_get_clean();
	}

    public function shortcode_streams($atts) {
		$atts = shortcode_atts(['per_page' => 12], $atts);
		$per_page = max(1, intval($atts['per_page']));
		$paged = max(1, intval(get_query_var('paged') ?: get_query_var('page') ?: 1));

		$q = new WP_Query([
			'post_type'      => self::CPT,
			'post_status'    => 'publish',
			'posts_per_page' => $per_page,
			'paged'          => $paged,
			'orderby'        => 'date',
			'order'          => 'DESC',
		]);

		$ph = plugin_dir_url(__FILE__) . 'res/icon-256x256.png';
		ob_start();
		?>
		<div class="wpsl-streams-archive">
			<style>
			.wpsl-streams-archive{--gap:16px}
			.wpsl-streams-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--gap);margin:0;padding:0;list-style:none}
			.wpsl-stream-card{border:1px solid #e2e4e7;border-radius:8px;overflow:hidden;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.04)}
			.wpsl-stream-thumb{display:block;position:relative;background:#111}
			.wpsl-stream-thumb img{display:block;width:100%;height:auto}
			.wpsl-stream-body{padding:10px}
			.wpsl-stream-title{margin:0 0 6px;font-weight:600;font-size:15px}
            .wpsl-stream-excerpt{margin:0 0 8px; color:#475569; font-size:13px; line-height:1.4; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical; overflow:hidden}
            .wpsl-stream-meta{margin:0 0 8px; color:#64748b; font-size:12px}
			.wpsl-badges{display:flex;flex-wrap:wrap;gap:6px}
			.wpsl-badge{font-size:12px;background:#f1f1f1;border-radius:999px;padding:3px 8px;border:1px solid #e0e0e0}
			.wpsl-badge.pay{background:#e3f2fd;border-color:#bbdefb}
			.wpsl-badge.pw{background:#fff3e0;border-color:#ffe0b2}
			.wpsl-badge.login{background:#e8f5e9;border-color:#c8e6c9}
			.wpsl-badge.purchased{background:#e8f5e9;border-color:#a5d6a7; color:#2e7d32}
            .wpsl-archive-pg{margin-top:14px;display:flex;gap:8px;align-items:center}
            .wpsl-watch{display:inline-block;margin-top:6px;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;color:#111;text-decoration:none}
            .wpsl-watch:hover{background:#eef2f7}
			</style>
			<ul class="wpsl-streams-list">
			<?php if ($q->have_posts()): while ($q->have_posts()): $q->the_post(); $p = get_post(); $sid = (int)$p->ID; $perma = get_permalink($p); ?>
				<li class="wpsl-stream-card">
					<a class="wpsl-stream-thumb" href="<?php echo esc_url($perma); ?>">
						<?php $poster = trailingslashit( wp_upload_dir()['baseurl'] ) . 'wpsl/' . (int)$sid . '/poster.jpg'; ?>
						<img src="<?php echo esc_url($poster); ?>" alt="Stream #<?php echo (int)$sid; ?>" loading="lazy" onerror="this.onerror=null;this.src='<?php echo esc_js($ph); ?>';" />
					</a>
					<div class="wpsl-stream-body">
						<h3 class="wpsl-stream-title"><a href="<?php echo esc_url($perma); ?>"><?php echo esc_html(get_the_title($p)); ?></a></h3>
                        <?php
                        $excerpt = wp_trim_words( wp_strip_all_tags( $p->post_content ), 22, '…' );
                        // Compute duration from playlist if available
                        $upload = wp_upload_dir();
                        $m3u_vod = trailingslashit($upload['basedir']) . 'wpsl/' . $sid . '/vod.m3u8';
                        $m3u_idx = trailingslashit($upload['basedir']) . 'wpsl/' . $sid . '/index.m3u8';
                        $m3u = file_exists($m3u_vod) ? $m3u_vod : (file_exists($m3u_idx) ? $m3u_idx : '');
                        $dur_str = '';
                        if ($m3u !== '') {
                            $dur_s = 0.0;
                            $lns = @file($m3u, FILE_IGNORE_NEW_LINES);
                            if (is_array($lns)) {
                                foreach ($lns as $l) { if (strpos($l, '#EXTINF:') === 0) { $v = trim(substr($l, 8)); $v = rtrim($v, ','); $dur_s += (float)$v; } }
                            }
                            $h = floor($dur_s / 3600); $m = floor(fmod($dur_s, 3600)/60); $s = floor(fmod($dur_s,60));
                            $dur_str = ($h > 0 ? $h . ':' : '') . sprintf('%02d:%02d', $m, $s);
                        }
                        if ($excerpt): ?>
                            <p class="wpsl-stream-excerpt"><?php echo esc_html($excerpt); ?></p>
                        <?php endif; ?>
                        <?php if ($dur_str !== ''): ?>
                            <p class="wpsl-stream-meta">Length: <?php echo esc_html($dur_str); ?></p>
                        <?php endif; ?>
						<div class="wpsl-badges">
						<?php $access = get_post_meta($sid, '_wpsl_access', true) ?: self::ACCESS_PUBLIC; $owned = false; if (is_user_logged_in()) { $pmeta = get_user_meta(get_current_user_id(), 'wpsl_purchases', true); if (is_array($pmeta) && isset($pmeta[(string)$sid])) { $owned = true; } } ?>
						<?php if ($owned): ?>
							<span class="wpsl-badge purchased">Purchased</span>
						<?php endif; ?>
                        <?php if ($access === self::ACCESS_PAYWALL && !$owned): $price = (int) get_post_meta($sid, '_wpsl_price_cents', true); $cur = get_post_meta($sid, '_wpsl_currency', true) ?: $this->get_option('wpsl_default_currency', 'usd'); ?>
                            <span class="wpsl-badge pay">Paywall: <?php echo esc_html(strtoupper($cur) . ' ' . number_format_i18n($price/100, 2)); ?></span>
                        <?php elseif ($access === self::ACCESS_PASSWORD): ?>
                            <span class="wpsl-badge pw">Password</span>
                        <?php else: ?>
                            <span class="wpsl-badge">Public</span>
                        <?php endif; ?>
							<?php if (get_post_meta($sid, '_wpsl_require_login', true) === '1'): ?>
								<span class="wpsl-badge login">Logged-in only</span>
							<?php endif; ?>
                        </div>
                        <?php if ($owned): ?>
                            <a class="wpsl-watch" href="<?php echo esc_url($perma); ?>">Watch now</a>
                        <?php endif; ?>
					</div>
				</li>
			<?php endwhile; wp_reset_postdata(); else: ?>
				<li>No streams found.</li>
			<?php endif; ?>
			</ul>
			<?php if ($q->max_num_pages > 1): $base = get_pagenum_link(1); $current = $paged; ?>
				<div class="wpsl-archive-pg">
					<?php if ($current > 1): ?><a class="button" href="<?php echo esc_url( get_pagenum_link($current-1) ); ?>">&laquo; Prev</a><?php else: ?><span class="button disabled">&laquo; Prev</span><?php endif; ?>
					<span>Page <?php echo (int)$current; ?> of <?php echo (int)$q->max_num_pages; ?></span>
					<?php if ($current < $q->max_num_pages): ?><a class="button" href="<?php echo esc_url( get_pagenum_link($current+1) ); ?>">Next &raquo;</a><?php else: ?><span class="button disabled">Next &raquo;</span><?php endif; ?>
				</div>
			<?php endif; ?>
		</div>
		<?php
        return ob_get_clean();
    }

    public function shortcode_my_videos($atts) {
        if (!is_user_logged_in()) {
            return '<p>Please log in to view your purchased videos.</p>';
        }
        $user_id = get_current_user_id();
        $data = get_user_meta($user_id, 'wpsl_purchases', true);
        $details = get_user_meta($user_id, 'wpsl_purchases_detail', true);
        if (!is_array($data) || empty($data)) return '<p>No purchased videos yet.</p>';
        // sort by timestamp desc
        arsort($data, SORT_NUMERIC);
        $items = [];
        foreach ($data as $sid => $ts) {
            $sid = (int)$sid; if ($sid <= 0) continue;
            $p = get_post($sid); if (!$p || $p->post_type !== self::CPT) continue;
            $title = get_the_title($p) ?: ('stream_' . $sid);
            $url = get_permalink($p);
            $dts = (int)$ts;
            $price_cents = null; $currency = '';
            if (is_array($details) && isset($details[(string)$sid]) && is_array($details[(string)$sid])) {
                $row = $details[(string)$sid];
                $dts = isset($row['ts']) ? (int)$row['ts'] : $dts;
                $price_cents = isset($row['price_cents']) ? (int)$row['price_cents'] : null;
                $currency = isset($row['currency']) ? (string)$row['currency'] : '';
            }
            $items[] = [ 'id'=>$sid, 'title'=>$title, 'url'=>$url, 'ts'=>$dts, 'price_cents'=>$price_cents, 'currency'=>$currency, 'post'=>$p ];
        }
        if (empty($items)) return '<p>No purchased videos yet.</p>';
        $upload = wp_upload_dir(); $baseurl = trailingslashit($upload['baseurl']) . 'wpsl/';
        ob_start(); ?>
        <style>
        .wpsl-my-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
        .wpsl-card{border:1px solid #e5e7eb;border-radius:10px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.05);overflow:hidden;display:flex;flex-direction:column}
        .wpsl-card img{display:block;width:100%;height:auto;background:#111}
        .wpsl-card .b{padding:10px 12px}
        .wpsl-card .t{margin:0 0 6px;font-weight:600}
        .wpsl-card .m{margin:0 0 8px;color:#64748b;font-size:12px}
        .wpsl-card .x{margin:0 0 10px;color:#475569;font-size:13px;line-height:1.5;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
        .wpsl-card .a{align-self:flex-start;margin-top:auto;margin:0 0 12px 12px;display:inline-block;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;color:#111;text-decoration:none}
        .wpsl-card .a:hover{background:#eef2f7}
        </style>
        <div class="wpsl-my-videos">
            <div class="wpsl-my-grid">
                <?php foreach ($items as $it): $sid=$it['id']; $poster=$baseurl.$sid.'/poster.jpg'; $excerpt = wp_trim_words( wp_strip_all_tags( $it['post']->post_content ), 26, '…'); $price_line=''; if (is_int($it['price_cents'])) { $cur = strtoupper($it['currency'] ?: $this->get_option('wpsl_default_currency','usd')); $price_line = $cur . ' ' . number_format_i18n($it['price_cents']/100, 2); } ?>
                <div class="wpsl-card">
                    <img src="<?php echo esc_url($poster); ?>" alt="" onerror="this.style.display='none'" />
                    <div class="b">
                        <h4 class="t"><a href="<?php echo esc_url($it['url']); ?>"><?php echo esc_html($it['title']); ?></a></h4>
                        <p class="m">Purchased on <?php echo esc_html( date_i18n( get_option('date_format') . ' ' . get_option('time_format'), $it['ts'] ) ); ?><?php echo $price_line ? ' • ' . esc_html($price_line) : ''; ?></p>
                        <?php if ($excerpt): ?><p class="x"><?php echo esc_html($excerpt); ?></p><?php endif; ?>
                    </div>
                    <a class="a" href="<?php echo esc_url($it['url']); ?>">Watch now</a>
                </div>
                <?php endforeach; ?>
            </div>
        </div>
        <?php return ob_get_clean();
    }
}

add_filter('script_loader_tag', function ($tag, $handle, $src) {
	if ($handle === 'wpsl-streamer') {
		return '<script type="module" src="' . esc_url($src) . '"></script>';
	}
	return $tag;
}, 10, 3);

new WP_Stream_Live();
