<?php
/**
 * REST API — read-only health endpoint for external monitoring
 * (UptimeRobot, StatusCake) and a quick-stats endpoint for the dashboard.
 *
 * Namespace: shieldforge/v1
 *   GET /health   — admin-only, returns service status + DB/cron/module checks
 *
 * @package Brainwerk Security Suite
 */

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

class Shieldforge_Rest {

    const NAMESPACE_V1 = 'shieldforge/v1';

    // Machine token the central pull-relay presents on the /relay/* endpoints.
    // Network option (shared across a multisite); empty = relay endpoint disabled.
    const RELAY_SECRET_OPTION = 'shieldforge_relay_secret';

    public static function register( Shieldforge_Loader $loader ) {
        $loader->add_action( 'rest_api_init', __CLASS__, 'register_routes' );
    }

    public static function register_routes() {
        register_rest_route( self::NAMESPACE_V1, '/health', array(
            'methods'             => 'GET',
            'permission_callback' => function () {
                return current_user_can( 'manage_options' ) || current_user_can( 'manage_network_options' );
            },
            'callback'            => array( __CLASS__, 'route_health' ),
        ) );

        // --- Pull-relay endpoint (Pfad B) -----------------------------------
        // Purpose-built replacement for the Claude-Talk bridge: egress-locked
        // sites (no outbound HTTPS from PHP) can't push to the central API, so
        // the backend relay PULLS queued SensorEvents here and pushes the salt +
        // signed intel feed back. Bearer-authenticated, machine-to-machine.
        register_rest_route( self::NAMESPACE_V1, '/relay/queue', array(
            'methods'             => 'GET',
            'permission_callback' => array( __CLASS__, 'relay_auth' ),
            'callback'            => array( __CLASS__, 'route_relay_queue' ),
            'args'                => array(
                'max' => array( 'default' => 200, 'sanitize_callback' => 'absint' ),
            ),
        ) );
        register_rest_route( self::NAMESPACE_V1, '/relay/sync', array(
            'methods'             => 'POST',
            'permission_callback' => array( __CLASS__, 'relay_auth' ),
            'callback'            => array( __CLASS__, 'route_relay_sync' ),
        ) );
    }

    public static function route_health( $request ) {
        global $wpdb;
        $checks = array();

        // DB-Tabellen
        $expected_tables = array(
            'shieldforge_logins'      => 'login_attempts',
            'shieldforge_blocks'      => 'ip_blocks',
            'shieldforge_audit_log'   => 'audit_log',
            'shieldforge_scan_files'  => 'scanner_files',
            'shieldforge_anomalies'   => 'anomalies',
        );
        $tbl_ok = true;
        $tbl_status = array();
        foreach ( $expected_tables as $name => $label ) {
            $full   = $wpdb->base_prefix . $name;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- health-check probe of the plugin's own tables; direct live read by design; not cacheable.
            $exists = (string) $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $full ) ) === $full;
            $tbl_status[ $label ] = $exists ? 'ok' : 'missing';
            if ( ! $exists ) { $tbl_ok = false; }
        }
        $checks['db_tables']        = $tbl_ok ? 'ok' : 'degraded';
        $checks['db_tables_detail'] = $tbl_status;

        // Cron-Hooks (alle die der Activator schedule kann)
        $cron_hooks = array(
            'shieldforge_daily_digest'  => 'digest',
            'shieldforge_cleanup_logs'  => 'cleanup',
            'shieldforge_daily_scan'    => 'scan',
            'shieldforge_daily_vulnscan'=> 'vulnscan',
        );
        $cron_ok = true;
        $cron_status = array();
        foreach ( $cron_hooks as $hook => $label ) {
            $next = wp_next_scheduled( $hook );
            $cron_status[ $label ] = $next ? gmdate( 'c', $next ) : 'missing';
            if ( ! $next ) { $cron_ok = false; }
        }
        $checks['cron']        = $cron_ok ? 'ok' : 'degraded';
        $checks['cron_detail'] = $cron_status;

        // Live-Counters (defensive: NULLable wenn Tabelle fehlt)
        $checks['failed_logins_24h']  = self::safe_count( "SELECT COUNT(*) FROM {$wpdb->base_prefix}shieldforge_logins WHERE successful = 0 AND created_at > UTC_TIMESTAMP() - INTERVAL 24 HOUR" );
        $checks['blocked_ips']        = self::safe_count( "SELECT COUNT(*) FROM {$wpdb->base_prefix}shieldforge_blocks WHERE unblocked_at IS NULL" );
        $checks['audit_log_24h']      = self::safe_count( "SELECT COUNT(*) FROM {$wpdb->base_prefix}shieldforge_audit_log WHERE created_at > UTC_TIMESTAMP() - INTERVAL 24 HOUR" );
        $checks['anomalies_24h']      = self::safe_count( "SELECT COUNT(*) FROM {$wpdb->base_prefix}shieldforge_anomalies WHERE created_at > UTC_TIMESTAMP() - INTERVAL 24 HOUR" );

        // Module-Konfiguration (sind die wichtigsten Schutz-Layer an?)
        $checks['twofactor_enabled']   = (bool) ( class_exists( 'Shieldforge_Settings' ) ? Shieldforge_Settings::get( 'twofactor_enabled', true ) : false );
        $checks['bruteforce_enabled']  = (bool) ( class_exists( 'Shieldforge_Settings' ) ? Shieldforge_Settings::get( 'bruteforce_enabled', true ) : false );
        $checks['scanner_enabled']     = (bool) ( class_exists( 'Shieldforge_Settings' ) ? Shieldforge_Settings::get( 'scanner_enabled', true ) : false );
        $checks['login_url_changed']   = (bool) ( class_exists( 'Shieldforge_Settings' ) ? Shieldforge_Settings::get( 'login_url_slug', '' ) : false );

        // Environment
        $checks['environment'] = function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : 'unknown';
        $checks['is_pro']      = function_exists( 'shieldforge_is_pro' ) ? (bool) shieldforge_is_pro() : false;

        $ok = $tbl_ok && $cron_ok;
        $payload = array(
            'status'  => $ok ? 'ok' : 'degraded',
            'service' => 'brainwerk-security-suite',
            'version' => SHIELDFORGE_VERSION,
            'site'    => get_bloginfo( 'name' ),
            'time'    => gmdate( 'c' ),
            'checks'  => $checks,
            'php'     => PHP_VERSION,
            'wp'      => get_bloginfo( 'version' ),
        );
        return new WP_REST_Response( $payload, $ok ? 200 : 503 );
    }

    /* ------------------------------------------------------------------
     * Pull-relay endpoint (Pfad B)
     * ------------------------------------------------------------------ */

    /**
     * Read the relay secret the backend relay must present. Prefers a wp-config
     * constant (SHIELDFORGE_RELAY_SECRET) — keeps the machine token out of the DB
     * and lets egress-locked sites be provisioned over FTP without any DB write —
     * and falls back to the network option for DB-provisioned installs.
     */
    private static function relay_secret() {
        if ( defined( 'SHIELDFORGE_RELAY_SECRET' ) && '' !== (string) SHIELDFORGE_RELAY_SECRET ) {
            return trim( (string) SHIELDFORGE_RELAY_SECRET );
        }
        $v = is_multisite()
            ? get_site_option( self::RELAY_SECRET_OPTION, '' )
            : get_option( self::RELAY_SECRET_OPTION, '' );
        return is_string( $v ) ? trim( $v ) : '';
    }

    /**
     * Permission gate for /relay/*. Machine-to-machine only: requires
     * "Authorization: Bearer <secret>" matching the stored relay secret,
     * compared in constant time. 503 when no secret is provisioned (disabled),
     * 401 on a missing/wrong token.
     */
    public static function relay_auth( $request ) {
        $secret = self::relay_secret();
        if ( '' === $secret ) {
            return new WP_Error( 'sf_relay_disabled', 'Relay endpoint not provisioned.', array( 'status' => 503 ) );
        }
        $header = (string) $request->get_header( 'authorization' );
        $given  = ( 0 === stripos( $header, 'bearer ' ) ) ? trim( substr( $header, 7 ) ) : '';
        if ( '' === $given || ! hash_equals( $secret, $given ) ) {
            return new WP_Error( 'sf_relay_unauthorized', 'Invalid relay token.', array( 'status' => 401 ) );
        }
        return true;
    }

    /** GET /relay/queue — return up to `max` queued SensorEvents (read-only). */
    public static function route_relay_queue( $request ) {
        if ( ! class_exists( 'Shieldforge_Sensor_Batcher' ) ) {
            return new WP_Error( 'sf_relay_unavailable', 'Sensor subsystem not loaded.', array( 'status' => 503 ) );
        }
        $max   = max( 1, min( 1000, (int) $request->get_param( 'max' ) ) );
        $items = Shieldforge_Sensor_Batcher::peek( $max );

        $events = array();
        foreach ( $items as $it ) {
            if ( empty( $it['id'] ) || empty( $it['event'] ) || ! is_array( $it['event'] ) ) {
                continue;
            }
            $events[] = array(
                'id'              => (string) $it['id'],
                'event'           => $it['event'],
                'share_with_feed' => ! empty( $it['share_with_feed'] ),
                'queued_at'       => isset( $it['queued_at'] ) ? (string) $it['queued_at'] : '',
            );
        }
        return new WP_REST_Response( array(
            'ok'        => true,
            'events'    => $events,
            'queue_len' => Shieldforge_Sensor_Batcher::count(),
            'site'      => home_url(),
            'version'   => defined( 'SHIELDFORGE_VERSION' ) ? SHIELDFORGE_VERSION : '',
        ), 200 );
    }

    /**
     * POST /relay/sync — drain acknowledged ids and apply the return-direction
     * payloads (network salt + signed intel feed). Body (all optional):
     *   { "ack_ids":[...], "netsalt":{epoch,epoch_key,...}, "intel":{signed_payload,signature,feed_version} }
     */
    public static function route_relay_sync( $request ) {
        $body = $request->get_json_params();
        if ( ! is_array( $body ) ) {
            $body = array();
        }

        // 1. Drain ids the backend has durably stored.
        $drained = 0;
        if ( ! empty( $body['ack_ids'] ) && is_array( $body['ack_ids'] ) && class_exists( 'Shieldforge_Sensor_Batcher' ) ) {
            $drained = (int) Shieldforge_Sensor_Batcher::remove( array_map( 'strval', $body['ack_ids'] ) );
        }

        // 2. Apply the pushed network epoch-key bundle (real cross-site correlation).
        $salt_applied = false;
        if ( ! empty( $body['netsalt'] ) && is_array( $body['netsalt'] )
            && ! empty( $body['netsalt']['epoch'] ) && ! empty( $body['netsalt']['epoch_key'] )
            && class_exists( 'Shieldforge_Sensor_Privacy' ) ) {
            Shieldforge_Sensor_Privacy::set_network_salt( $body['netsalt'] );
            $salt_applied = true;
        }

        // 3. Apply the signed intel feed (store_feed builds the envelope itself).
        $feed_applied = false;
        if ( ! empty( $body['intel'] ) && is_array( $body['intel'] )
            && isset( $body['intel']['signed_payload'], $body['intel']['signature'], $body['intel']['feed_version'] )
            && class_exists( 'Shieldforge_Intel_Client' ) ) {
            Shieldforge_Intel_Client::store_feed(
                $body['intel']['signed_payload'],
                $body['intel']['signature'],
                (int) $body['intel']['feed_version']
            );
            $feed_applied = true;
        }

        return new WP_REST_Response( array(
            'ok'           => true,
            'drained'      => $drained,
            'salt_applied' => $salt_applied,
            'feed_applied' => $feed_applied,
            'queue_len'    => class_exists( 'Shieldforge_Sensor_Batcher' ) ? Shieldforge_Sensor_Batcher::count() : 0,
        ), 200 );
    }

    /** Sicheres COUNT — wenn Tabelle nicht existiert, gibt 0 statt Fatal. */
    private static function safe_count( $sql ) {
        global $wpdb;
        $prev_show_errors = $wpdb->hide_errors();
        // $sql is built internally from trusted table names ($wpdb->prefix), never from user input.
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom security table; direct live read by design; not cacheable.
        $v = (int) $wpdb->get_var( $sql );
        if ( $prev_show_errors ) { $wpdb->show_errors(); }
        return $v;
    }
}
