<?php
/**
 * Dashboard telemetry service.
 *
 * Collects reliable runtime telemetry for the Dashboard matrix surface.
 *
 * @package Noesia_Smart_Waiter
 */

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

if ( ! class_exists( 'Noesia_SWA_Dashboard_Telemetry_Service' ) ) {
    /**
     * Runtime telemetry provider for the Dashboard.
     */
    class Noesia_SWA_Dashboard_Telemetry_Service {

        /**
         * Session transient key.
         *
         * @var string
         */
        const SESSIONS_TRANSIENT_KEY = 'noesia_swa_dashboard_live_sessions';

        /**
         * Failed login transient key.
         *
         * @var string
         */
        const FAILED_LOGINS_TRANSIENT_KEY = 'noesia_swa_dashboard_failed_login_window';

        /**
         * Event stream transient key.
         *
         * @var string
         */
        const EVENTS_TRANSIENT_KEY = 'noesia_swa_dashboard_telemetry_events';

        /**
         * API latency transient key.
         *
         * @var string
         */
        const API_LATENCY_TRANSIENT_KEY = 'noesia_swa_dashboard_api_latency';

        /**
         * Active session TTL in seconds.
         *
         * @var int
         */
        const SESSION_TTL = 120;

        /**
         * Failed login rolling window in seconds.
         *
         * @var int
         */
        const FAILED_LOGIN_WINDOW = 900;

        /**
         * Maximum retained stream events.
         *
         * @var int
         */
        const MAX_EVENTS = 18;

        /**
         * Record a live session heartbeat for the current request.
         *
         * @return void
         */
        public function record_active_session() {
            $fingerprint = $this->get_request_fingerprint();

            if ( '' === $fingerprint ) {
                return;
            }

            $sessions                = $this->cleanup_sessions( $this->read_transient_array( self::SESSIONS_TRANSIENT_KEY ), time() );
            $sessions[ $fingerprint ] = time();

            set_transient( self::SESSIONS_TRANSIENT_KEY, $sessions, self::SESSION_TTL * 2 );
        }

        /**
         * Record a failed login event.
         *
         * @param string $username Username submitted during the failed login.
         * @return void
         */
        public function record_failed_login( $username = '' ) {
            $timestamps   = $this->cleanup_timestamps( $this->read_transient_array( self::FAILED_LOGINS_TRANSIENT_KEY ), time(), self::FAILED_LOGIN_WINDOW );
            $timestamps[] = time();

            set_transient( self::FAILED_LOGINS_TRANSIENT_KEY, array_values( $timestamps ), self::FAILED_LOGIN_WINDOW * 2 );

            $this->add_event( 'critical', 'AUTH', __( 'Failed login attempt detected.', 'noesia-smart-waiter' ) );
        }

        /**
         * Record a recent 404 or missing asset event.
         *
         * @return void
         */
        public function record_not_found_event() {
            $path = $this->get_request_path();

            if ( '' === $path ) {
                return;
            }

            $dedupe_key = 'noesia_swa_dashboard_telemetry_dedupe_' . md5( $path );

            if ( get_transient( $dedupe_key ) ) {
                return;
            }

            set_transient( $dedupe_key, '1', MINUTE_IN_SECONDS );

            if ( $this->looks_like_asset_path( $path ) ) {
                /* translators: %s: missing asset request path. */
                $this->add_event( 'warning', 'HTTP', sprintf( __( 'Missing asset: %s', 'noesia-smart-waiter' ), $path ) );
                return;
            }

            /* translators: %s: not found request path. */
            $this->add_event( 'warning', 'HTTP', sprintf( __( '404 request: %s', 'noesia-smart-waiter' ), $path ) );
        }

        /**
         * Build the telemetry snapshot payload.
         *
         * @return array<string,mixed>
         */
        public function get_snapshot() {
            $generated_at = time();
            $metrics      = array(
                $this->build_db_latency_metric(),
                $this->build_site_api_latency_metric(),
                $this->build_memory_metric(),
                $this->build_active_sessions_metric(),
                $this->build_failed_login_metric(),
                $this->build_cron_due_metric(),
                $this->build_cron_queue_metric(),
                $this->build_placeholder_metric( 'cache_ratio', __( 'Cache ratio', 'noesia-smart-waiter' ), __( 'Unavailable', 'noesia-smart-waiter' ), 'unavailable', __( 'No cache provider exposed.', 'noesia-smart-waiter' ) ),
                $this->build_placeholder_metric( 'host_cpu', __( 'Host CPU', 'noesia-smart-waiter' ), __( 'Unavailable', 'noesia-smart-waiter' ), 'unavailable', __( 'Host metrics are not exposed at PHP level.', 'noesia-smart-waiter' ) ),
                $this->build_placeholder_metric( 'host_ram', __( 'Host RAM', 'noesia-smart-waiter' ), __( 'Unavailable', 'noesia-smart-waiter' ), 'unavailable', __( 'Host metrics are not exposed at PHP level.', 'noesia-smart-waiter' ) ),
            );

            return array(
                'generated_at'       => $generated_at,
                'generated_at_label' => wp_date( 'H:i:s', $generated_at ),
                'metrics'            => $metrics,
                'events'             => $this->get_recent_events(),
            );
        }

        /**
         * Read the current request path.
         *
         * @return string
         */
        private function get_request_path() {
            $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['REQUEST_URI'] ) ) : '';
            $path        = (string) wp_parse_url( $request_uri, PHP_URL_PATH );

            if ( '' === $path ) {
                return '';
            }

            $path = '/' . ltrim( $path, '/' );
            $path = preg_replace( '#/+#', '/', $path );
            $path = wp_strip_all_tags( (string) $path );

            return $this->limit_text( $path, 120 );
        }

        /**
         * Decide whether the current request should count as a live session.
         *
         * @return string
         */
        private function get_request_fingerprint() {
            $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_key( wp_unslash( (string) $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';

            if ( ! in_array( $request_method, array( 'GET', 'HEAD' ), true ) ) {
                return '';
            }

            if ( wp_doing_ajax() || wp_doing_cron() ) {
                return '';
            }

            if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
                return '';
            }

            if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
                return '';
            }

            if ( is_user_logged_in() ) {
                $user_id = (int) get_current_user_id();
                if ( $user_id > 0 ) {
                    return 'u:' . $user_id;
                }
            }

            $ip         = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ) : '';
            $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) ) : '';

            if ( '' === $ip && '' === $user_agent ) {
                return '';
            }

            return 'v:' . hash( 'sha256', $ip . '|' . $user_agent . '|' . wp_salt( 'auth' ) );
        }

        /**
         * Build the DB latency metric.
         *
         * @return array<string,string>
         */
        private function build_db_latency_metric() {
            global $wpdb;

            if ( ! ( $wpdb instanceof wpdb ) ) {
                return $this->build_placeholder_metric( 'db_latency', __( 'DB latency', 'noesia-smart-waiter' ), __( 'Unavailable', 'noesia-smart-waiter' ), 'unavailable', __( 'wpdb is unavailable.', 'noesia-smart-waiter' ) );
            }

            $start           = microtime( true );
            $previous_errors = $wpdb->suppress_errors( true );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Intentional lightweight telemetry ping; the result is not persistent application data.
            $wpdb->get_var( 'SELECT 1' );
            $wpdb->suppress_errors( $previous_errors );
            $elapsed_ms = (int) round( max( 0, ( microtime( true ) - $start ) * 1000 ) );
            $state      = 'ok';

            if ( $elapsed_ms >= 80 ) {
                $state = 'critical';
            } elseif ( $elapsed_ms >= 35 ) {
                $state = 'warning';
            }

            return $this->build_metric( 'db_latency', __( 'DB latency', 'noesia-smart-waiter' ), $elapsed_ms . 'ms', $state, __( 'Direct MySQL ping.', 'noesia-smart-waiter' ) );
        }

        /**
         * Build the loopback site API latency metric.
         *
         * @return array<string,string>
         */
        private function build_site_api_latency_metric() {
            $cached = get_transient( self::API_LATENCY_TRANSIENT_KEY );

            if ( is_array( $cached ) && isset( $cached['value'], $cached['state'], $cached['meta'] ) ) {
                return $this->build_metric(
                    'site_api_latency',
                    __( 'Site API', 'noesia-smart-waiter' ),
                    (string) $cached['value'],
                    sanitize_key( (string) $cached['state'] ),
                    sanitize_text_field( (string) $cached['meta'] )
                );
            }

            $start    = microtime( true );
            $response = wp_remote_get(
                rest_url(),
                array(
                    'timeout'    => 3,
                    'redirection'=> 1,
                    'headers'    => array(
                        'Cache-Control' => 'no-cache',
                    ),
                    'user-agent' => 'Noesia Smart Waiter Dashboard Telemetry',
                )
            );
            $elapsed_ms = (int) round( max( 0, ( microtime( true ) - $start ) * 1000 ) );

            if ( is_wp_error( $response ) ) {
                $metric = $this->build_placeholder_metric( 'site_api_latency', __( 'Site API', 'noesia-smart-waiter' ), __( 'Unavailable', 'noesia-smart-waiter' ), 'unavailable', __( 'Loopback API request failed.', 'noesia-smart-waiter' ) );
                set_transient(
                    self::API_LATENCY_TRANSIENT_KEY,
                    array(
                        'value' => $metric['value'],
                        'state' => $metric['state'],
                        'meta'  => $metric['meta'],
                    ),
                    45
                );
                return $metric;
            }

            $state = 'ok';
            if ( $elapsed_ms >= 1000 ) {
                $state = 'critical';
            } elseif ( $elapsed_ms >= 450 ) {
                $state = 'warning';
            }

            $metric = $this->build_metric( 'site_api_latency', __( 'Site API', 'noesia-smart-waiter' ), $elapsed_ms . 'ms', $state, __( 'Loopback REST response time.', 'noesia-smart-waiter' ) );
            set_transient(
                self::API_LATENCY_TRANSIENT_KEY,
                array(
                    'value' => $metric['value'],
                    'state' => $metric['state'],
                    'meta'  => $metric['meta'],
                ),
                45
            );

            return $metric;
        }

        /**
         * Build the PHP memory metric.
         *
         * @return array<string,string>
         */
        private function build_memory_metric() {
            $usage_bytes = function_exists( 'memory_get_usage' ) ? (int) memory_get_usage( true ) : 0;
            $limit_bytes = wp_convert_hr_to_bytes( WP_MEMORY_LIMIT );

            if ( $limit_bytes <= 0 ) {
                $ini_limit   = (string) ini_get( 'memory_limit' );
                $limit_bytes = wp_convert_hr_to_bytes( $ini_limit );
            }

            if ( $limit_bytes <= 0 ) {
                return $this->build_placeholder_metric( 'php_memory', __( 'PHP memory', 'noesia-smart-waiter' ), size_format( $usage_bytes ), 'unavailable', __( 'Memory limit is not readable.', 'noesia-smart-waiter' ) );
            }

            $percent = $limit_bytes > 0 ? (int) round( ( $usage_bytes / max( 1, $limit_bytes ) ) * 100 ) : 0;
            $state   = 'ok';

            if ( $percent >= 90 ) {
                $state = 'critical';
            } elseif ( $percent >= 75 ) {
                $state = 'warning';
            }

            return $this->build_metric(
                'php_memory',
                __( 'PHP memory', 'noesia-smart-waiter' ),
                sprintf( '%1$s / %2$s', size_format( $usage_bytes ), size_format( $limit_bytes ) ),
                $state,
                /* translators: %d: percentage of configured PHP memory currently used. */
                sprintf( __( '%d%% of the configured PHP memory limit.', 'noesia-smart-waiter' ), $percent )
            );
        }

        /**
         * Build the active session metric.
         *
         * @return array<string,string>
         */
        private function build_active_sessions_metric() {
            $sessions = $this->cleanup_sessions( $this->read_transient_array( self::SESSIONS_TRANSIENT_KEY ), time() );
            $count    = count( $sessions );
            $state    = 'ok';

            if ( $count >= 120 ) {
                $state = 'critical';
            } elseif ( $count >= 60 ) {
                $state = 'warning';
            }

            set_transient( self::SESSIONS_TRANSIENT_KEY, $sessions, self::SESSION_TTL * 2 );

            return $this->build_metric( 'active_sessions', __( 'Live sessions', 'noesia-smart-waiter' ), (string) $count, $state, __( 'Approximate active visitors in the last two minutes.', 'noesia-smart-waiter' ) );
        }

        /**
         * Build the failed login metric.
         *
         * @return array<string,string>
         */
        private function build_failed_login_metric() {
            $timestamps = $this->cleanup_timestamps( $this->read_transient_array( self::FAILED_LOGINS_TRANSIENT_KEY ), time(), self::FAILED_LOGIN_WINDOW );
            $count      = count( $timestamps );
            $state      = 'ok';

            if ( $count >= 10 ) {
                $state = 'critical';
            } elseif ( $count >= 3 ) {
                $state = 'warning';
            }

            set_transient( self::FAILED_LOGINS_TRANSIENT_KEY, array_values( $timestamps ), self::FAILED_LOGIN_WINDOW * 2 );

            return $this->build_metric( 'failed_logins', __( 'Failed logins · 15m', 'noesia-smart-waiter' ), (string) $count, $state, __( 'Rolling failed login count for the last 15 minutes.', 'noesia-smart-waiter' ) );
        }

        /**
         * Build the due cron job metric.
         *
         * @return array<string,string>
         */
        private function build_cron_due_metric() {
            $counts = $this->get_cron_counts();
            $state  = 'ok';

            if ( $counts['due'] >= 10 ) {
                $state = 'critical';
            } elseif ( $counts['due'] >= 3 ) {
                $state = 'warning';
            }

            return $this->build_metric( 'cron_due', __( 'WP-Cron due', 'noesia-smart-waiter' ), (string) $counts['due'], $state, __( 'Scheduled jobs ready to run right now.', 'noesia-smart-waiter' ) );
        }

        /**
         * Build the total cron queue metric.
         *
         * @return array<string,string>
         */
        private function build_cron_queue_metric() {
            $counts = $this->get_cron_counts();
            $state  = 'ok';

            if ( $counts['total'] >= 80 ) {
                $state = 'warning';
            }

            return $this->build_metric( 'cron_queue', __( 'WP-Cron queue', 'noesia-smart-waiter' ), (string) $counts['total'], $state, __( 'Total scheduled cron callbacks in the queue.', 'noesia-smart-waiter' ) );
        }

        /**
         * Count cron callbacks.
         *
         * @return array<string,int>
         */
        private function get_cron_counts() {
            $cron = function_exists( '_get_cron_array' ) ? _get_cron_array() : array();
            $due  = 0;
            $all  = 0;
            $now  = time();

            if ( ! is_array( $cron ) ) {
                return array(
                    'due'   => 0,
                    'total' => 0,
                );
            }

            foreach ( $cron as $timestamp => $hooks ) {
                if ( ! is_array( $hooks ) ) {
                    continue;
                }

                foreach ( $hooks as $entries ) {
                    if ( ! is_array( $entries ) ) {
                        continue;
                    }

                    $hook_total = count( $entries );
                    $all       += $hook_total;

                    if ( (int) $timestamp <= $now ) {
                        $due += $hook_total;
                    }
                }
            }

            return array(
                'due'   => (int) $due,
                'total' => (int) $all,
            );
        }

        /**
         * Return the recent event stream.
         *
         * @return array<int,array<string,string>>
         */
        private function get_recent_events() {
            $events = $this->read_transient_array( self::EVENTS_TRANSIENT_KEY );

            if ( empty( $events ) ) {
                return array(
                    array(
                        'level'      => 'ok',
                        'level_label'=> 'OK',
                        'channel'    => 'SYSTEM',
                        'message'    => __( 'No recent runtime events.', 'noesia-smart-waiter' ),
                        'time_label' => wp_date( 'H:i:s', time() ),
                    ),
                );
            }

            $events = array_slice( array_values( $events ), 0, 8 );

            return array_map(
                array( $this, 'normalize_event_for_output' ),
                $events
            );
        }

        /**
         * Add a new stream event.
         *
         * @param string $level Event severity.
         * @param string $channel Event channel.
         * @param string $message Event message.
         * @return void
         */
        private function add_event( $level, $channel, $message ) {
            $safe_level   = $this->normalize_state( $level );
            $safe_channel = strtoupper( sanitize_key( (string) $channel ) );
            $safe_message = $this->limit_text( sanitize_text_field( (string) $message ), 160 );
            $timestamp    = time();
            $events       = $this->read_transient_array( self::EVENTS_TRANSIENT_KEY );

            array_unshift(
                $events,
                array(
                    'level'   => $safe_level,
                    'channel' => '' !== $safe_channel ? $safe_channel : 'SYSTEM',
                    'message' => $safe_message,
                    'time'    => $timestamp,
                )
            );

            $events = array_slice( $events, 0, self::MAX_EVENTS );
            set_transient( self::EVENTS_TRANSIENT_KEY, $events, DAY_IN_SECONDS );
        }

        /**
         * Normalize one event for output.
         *
         * @param array<string,mixed> $event Raw event.
         * @return array<string,string>
         */
        private function normalize_event_for_output( $event ) {
            $level = isset( $event['level'] ) ? $this->normalize_state( (string) $event['level'] ) : 'ok';
            $time  = isset( $event['time'] ) ? absint( $event['time'] ) : time();

            return array(
                'level'       => $level,
                'level_label' => $this->get_state_label( $level ),
                'channel'     => isset( $event['channel'] ) ? strtoupper( sanitize_key( (string) $event['channel'] ) ) : 'SYSTEM',
                'message'     => isset( $event['message'] ) ? sanitize_text_field( (string) $event['message'] ) : '',
                'time_label'  => wp_date( 'H:i:s', $time ),
            );
        }

        /**
         * Build a metric payload.
         *
         * @param string $id Metric id.
         * @param string $label Metric label.
         * @param string $value Metric value.
         * @param string $state Metric state.
         * @param string $meta Metric helper text.
         * @return array<string,string>
         */
        private function build_metric( $id, $label, $value, $state, $meta ) {
            $safe_state = $this->normalize_state( $state );

            return array(
                'id'          => sanitize_key( (string) $id ),
                'label'       => sanitize_text_field( (string) $label ),
                'value'       => sanitize_text_field( (string) $value ),
                'state'       => $safe_state,
                'state_label' => $this->get_state_label( $safe_state ),
                'meta'        => sanitize_text_field( (string) $meta ),
            );
        }

        /**
         * Build an unavailable metric.
         *
         * @param string $id Metric id.
         * @param string $label Metric label.
         * @param string $value Metric value.
         * @param string $state Metric state.
         * @param string $meta Metric helper text.
         * @return array<string,string>
         */
        private function build_placeholder_metric( $id, $label, $value, $state, $meta ) {
            return $this->build_metric( $id, $label, $value, $state, $meta );
        }

        /**
         * Normalize a metric state.
         *
         * @param string $state Raw state.
         * @return string
         */
        private function normalize_state( $state ) {
            $safe = sanitize_key( (string) $state );

            if ( ! in_array( $safe, array( 'ok', 'warning', 'critical', 'unavailable' ), true ) ) {
                return 'ok';
            }

            return $safe;
        }

        /**
         * Resolve the user-facing state label.
         *
         * @param string $state Normalized state.
         * @return string
         */
        private function get_state_label( $state ) {
            switch ( $this->normalize_state( $state ) ) {
                case 'warning':
                    return 'WARNING';
                case 'critical':
                    return 'CRITICAL';
                case 'unavailable':
                    return 'UNAVAILABLE';
                case 'ok':
                default:
                    return 'OK';
            }
        }

        /**
         * Remove expired sessions.
         *
         * @param array<string,int> $sessions Session map.
         * @param int               $now Current timestamp.
         * @return array<string,int>
         */
        private function cleanup_sessions( $sessions, $now ) {
            $clean = array();

            if ( ! is_array( $sessions ) ) {
                return $clean;
            }

            foreach ( $sessions as $fingerprint => $timestamp ) {
                $timestamp = absint( $timestamp );
                if ( $timestamp <= 0 || ( $now - $timestamp ) > self::SESSION_TTL ) {
                    continue;
                }
                $clean[ sanitize_key( (string) $fingerprint ) ] = $timestamp;
            }

            return $clean;
        }

        /**
         * Remove expired timestamps.
         *
         * @param array<int,mixed> $timestamps Stored timestamps.
         * @param int              $now Current timestamp.
         * @param int              $window Allowed age.
         * @return array<int,int>
         */
        private function cleanup_timestamps( $timestamps, $now, $window ) {
            $clean = array();

            if ( ! is_array( $timestamps ) ) {
                return $clean;
            }

            foreach ( $timestamps as $timestamp ) {
                $timestamp = absint( $timestamp );
                if ( $timestamp <= 0 || ( $now - $timestamp ) > $window ) {
                    continue;
                }
                $clean[] = $timestamp;
            }

            return $clean;
        }

        /**
         * Read an array transient safely.
         *
         * @param string $key Transient key.
         * @return array
         */
        private function read_transient_array( $key ) {
            $value = get_transient( (string) $key );
            return is_array( $value ) ? $value : array();
        }

        /**
         * Detect whether a path looks like a static asset.
         *
         * @param string $path Request path.
         * @return bool
         */
        private function looks_like_asset_path( $path ) {
            return (bool) preg_match( '/\.(?:css|js|map|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|eot|json|txt|xml|mp3|mp4|webm)$/i', (string) $path );
        }

        /**
         * Limit string length safely.
         *
         * @param string $text Input text.
         * @param int    $limit Maximum characters.
         * @return string
         */
        private function limit_text( $text, $limit ) {
            $text = trim( (string) $text );

            if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) {
                if ( mb_strlen( $text ) <= $limit ) {
                    return $text;
                }

                return rtrim( mb_substr( $text, 0, $limit - 1 ) ) . '…';
            }

            if ( strlen( $text ) <= $limit ) {
                return $text;
            }

            return rtrim( substr( $text, 0, $limit - 1 ) ) . '…';
        }
    }
}
