<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * REST API: /vlens/v1/record
 * Receives batched session events from recorder.js.
 */
class VLENS_REST_Record {

    const MAX_EVENTS_PER_BATCH = 200;

    public function register_routes() {
        register_rest_route( 'vlens/v1', '/record', [
            'methods'             => 'POST',
            'callback'            => [ $this, 'handle' ],
            // Intentionally public — anonymous session-recording beacon (no login involved).
            // Guarded against cross-origin forgery and per-IP flooding in VLENS_Helpers::public_write_permission().
            'permission_callback' => function ( WP_REST_Request $request ) {
                return VLENS_Helpers::public_write_permission( $request, 'record' );
            },
        ] );
    }

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

        $data = $request->get_json_params();
        if ( empty( $data ) || empty( $data['session_key'] ) ) {
            return new WP_REST_Response( [ 'error' => 'bad request' ], 400 );
        }

        $t_sessions = VLENS_Helpers::table( 'sessions' );

        // Monthly session limit enforcement (only for new sessions, not updates).
        //
        // Tier-1 perf: the running month count is cached in a transient and
        // incremented in-process after each new session, so the COUNT(*) on the
        // (growing) sessions table runs at most once an hour instead of on every
        // new-session beacon. This is a soft cap — a little drift under heavy
        // concurrency is acceptable, and the hourly refresh self-corrects it.
        $session_id_param = isset( $data['session_id'] ) ? absint( $data['session_id'] ) : 0;
        $count_cache_key  = '';
        if ( ! $session_id_param ) {
            $settings      = VLENS_Settings::get_all();
            $session_limit = (int) $settings['session_limit'];
            if ( $session_limit > 0 ) {
                $month_start     = gmdate( 'Y-m-01' );
                $count_cache_key = 'vlens_month_sessions_' . gmdate( 'Y_m' );
                $count           = get_transient( $count_cache_key );
                if ( false === $count ) {
                    $count = (int) $wpdb->get_var( $wpdb->prepare(
                        "SELECT COUNT(*) FROM %i WHERE date >= %s",
                        $t_sessions, $month_start
                    ) );
                    set_transient( $count_cache_key, $count, HOUR_IN_SECONDS );
                }
                if ( (int) $count >= $session_limit ) {
                    return new WP_REST_Response( [
                        'ok'    => false,
                        'error' => 'limit_reached',
                    ], 429 );
                }
            }
        }
        $t_events   = VLENS_Helpers::table( 'session_events' );

        $key        = substr( preg_replace( '/[^a-f0-9\-]/', '', $data['session_key'] ), 0, 36 );
        $session_id = isset( $data['session_id'] ) ? absint( $data['session_id'] ) : 0;
        $today      = current_time( 'Y-m-d' );
        $is_final   = ! empty( $data['is_final'] );

        $page_uri   = VLENS_Helpers::sanitize_str( $data['page_uri'] ?? '/', 2000 );
        $page_title = VLENS_Helpers::sanitize_str( $data['page_title'] ?? '', 500 );
        $vp_w       = isset( $data['viewport_w'] ) ? absint( $data['viewport_w'] ) : null;
        $vp_h       = isset( $data['viewport_h'] ) ? absint( $data['viewport_h'] ) : null;
        $page_h     = isset( $data['page_h'] ) ? absint( $data['page_h'] ) : null;
        $max_sc_px  = isset( $data['max_scroll_px'] ) ? absint( $data['max_scroll_px'] ) : null;
        $max_sc_pct = isset( $data['max_scroll_pct'] ) ? min( 100, absint( $data['max_scroll_pct'] ) ) : null;
        $duration   = isset( $data['duration_ms'] ) ? absint( $data['duration_ms'] ) : null;
        $events     = array_slice( $data['events'] ?? [], 0, self::MAX_EVENTS_PER_BATCH );
        $visitor_id = isset( $data['visitor_id'] ) && (int) $data['visitor_id'] > 0 ? absint( $data['visitor_id'] ) : null;

        // Count event types
        $clicks = $scrolls = $inputs = $rages = 0;
        foreach ( $events as $e ) {
            $t = $e['type'] ?? '';
            if ( in_array( $t, [ 'click', 'rage_click' ], true ) ) $clicks++;
            if ( $t === 'scroll' ) $scrolls++;
            if ( $t === 'input' ) $inputs++;
            if ( $t === 'rage_click' ) $rages++;
        }

        // Upsert session
        if ( ! $session_id ) {
            $wpdb->suppress_errors( true );
            $wpdb->insert( $t_sessions, [
                'session_key'   => $key,
                'visitor_id'    => $visitor_id,
                'date'          => $today,
                'page_uri'      => $page_uri,
                'page_title'    => $page_title,
                'viewport_w'    => $vp_w,
                'viewport_h'    => $vp_h,
                'page_h'        => $page_h,
                'max_scroll_px' => $max_sc_px,
                'max_scroll_pct'=> $max_sc_pct,
                'duration_ms'   => $duration,
                'click_count'   => $clicks,
                'scroll_count'  => $scrolls,
                'input_count'   => $inputs,
                'rage_clicks'   => $rages,
                'is_complete'   => $is_final ? 1 : 0,
                'started_at'    => current_time( 'mysql' ),
            ] );
            $wpdb->suppress_errors( false );

            $session_id = $wpdb->insert_id;
            if ( ! $session_id ) {
                $session_id = (int) $wpdb->get_var( $wpdb->prepare(
                    "SELECT id FROM %i WHERE session_key = %s LIMIT 1", $t_sessions, $key
                ) );
            } elseif ( $count_cache_key ) {
                // Keep the cached monthly counter in step with the new insert.
                set_transient( $count_cache_key, (int) get_transient( $count_cache_key ) + 1, HOUR_IN_SECONDS );
            }
        } else {
            $wpdb->query( $wpdb->prepare(
                "UPDATE %i SET
                    visitor_id     = COALESCE(%s, visitor_id),
                    max_scroll_px  = GREATEST(COALESCE(max_scroll_px, 0), %d),
                    max_scroll_pct = GREATEST(COALESCE(max_scroll_pct, 0), %d),
                    page_h         = COALESCE(%s, page_h),
                    duration_ms    = %d,
                    click_count    = click_count + %d,
                    scroll_count   = scroll_count + %d,
                    input_count    = input_count + %d,
                    rage_clicks    = rage_clicks + %d,
                    is_complete    = %d,
                    ended_at       = IF(%d = 1, NOW(), ended_at)
                WHERE id = %d",
                $t_sessions,
                $visitor_id,
                $max_sc_px ?? 0,
                $max_sc_pct ?? 0,
                $page_h,
                $duration,
                $clicks, $scrolls, $inputs, $rages,
                $is_final ? 1 : 0,
                $is_final ? 1 : 0,
                $session_id
            ) );
        }

        // Insert events
        if ( $session_id && count( $events ) ) {
            $allowed_types = [
                'scroll', 'click', 'mousemove', 'input', 'select', 'resize',
                'visibility', 'navigate', 'rage_click', 'copy', 'paste',
                'focus', 'blur', 'error', 'custom', 'pause', 'resume',
                'performance', 'dead_click', 'u_turn',
            ];

            // Allow premium to add more event types
            $allowed_types = apply_filters( 'vlens_allowed_event_types', $allowed_types );

            // Tier-1 perf: instead of one INSERT per event (up to 200 queries per
            // batch), sanitize every event into a row and write them with a single
            // multi-row INSERT. Rows are flushed in chunks so a large batch can't
            // exceed MySQL's max_allowed_packet / placeholder limits.
            $columns = [
                'session_id', 't', 'type', 'x', 'y', 'scroll_x', 'scroll_y',
                'tag', 'el_id', 'el_class', 'el_text', 'href',
                'input_type', 'input_name', 'vp_w', 'vp_h', 'meta',
            ];

            $rows = [];
            foreach ( $events as $e ) {
                $type = $e['type'] ?? '';
                if ( ! in_array( $type, $allowed_types, true ) ) continue;

                $rows[] = [
                    $session_id,
                    absint( $e['t'] ?? 0 ),
                    $type,
                    isset( $e['x'] ) ? intval( $e['x'] ) : null,
                    isset( $e['y'] ) ? intval( $e['y'] ) : null,
                    isset( $e['scroll_x'] ) ? absint( $e['scroll_x'] ) : null,
                    isset( $e['scroll_y'] ) ? absint( $e['scroll_y'] ) : null,
                    isset( $e['tag'] ) ? substr( sanitize_text_field( $e['tag'] ), 0, 30 ) : null,
                    isset( $e['el_id'] ) ? substr( sanitize_text_field( $e['el_id'] ), 0, 200 ) : null,
                    isset( $e['el_class'] ) ? substr( sanitize_text_field( $e['el_class'] ), 0, 300 ) : null,
                    isset( $e['el_text'] ) ? substr( sanitize_text_field( $e['el_text'] ), 0, 200 ) : null,
                    isset( $e['href'] ) ? substr( esc_url_raw( $e['href'] ), 0, 2000 ) : null,
                    isset( $e['input_type'] ) ? substr( sanitize_text_field( $e['input_type'] ), 0, 30 ) : null,
                    isset( $e['input_name'] ) ? substr( sanitize_text_field( $e['input_name'] ), 0, 200 ) : null,
                    isset( $e['vp_w'] ) ? absint( $e['vp_w'] ) : null,
                    isset( $e['vp_h'] ) ? absint( $e['vp_h'] ) : null,
                    isset( $e['meta'] ) ? wp_json_encode( $e['meta'] ) : null,
                ];
            }

            if ( $rows ) {
                $col_list = '`' . implode( '`, `', $columns ) . '`';

                foreach ( array_chunk( $rows, 100 ) as $chunk ) {
                    $row_sql = [];
                    $args    = [ $t_events ]; // first arg feeds the %i table identifier

                    foreach ( $chunk as $vals ) {
                        $placeholders = [];
                        foreach ( $vals as $v ) {
                            // Emit a literal NULL for missing values (preserves the
                            // nullable-column semantics of the old per-row insert);
                            // real values go through prepared %s placeholders.
                            if ( null === $v ) {
                                $placeholders[] = 'NULL';
                            } else {
                                $placeholders[] = '%s';
                                $args[]         = $v;
                            }
                        }
                        $row_sql[] = '(' . implode( ',', $placeholders ) . ')';
                    }

                    $sql = "INSERT INTO %i ($col_list) VALUES " . implode( ',', $row_sql );
                    $wpdb->query( $wpdb->prepare( $sql, $args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
                }
            }
        }

        // Fire hook after session batch saved
        do_action( 'vlens_session_recorded', $session_id, $events );

        return new WP_REST_Response( [
            'ok'         => true,
            'session_id' => $session_id,
        ], 200 );
    }
}
