<?php
/**
 * Static helpers: IP detection, anonymization, pseudonymization, CIDR matching.
 *
 * @package Brainwerk Security Suite
 */

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

class Shieldforge_Utils {

    /**
     * Best-effort real client IP.
     *
     * Honors common proxy headers but only when WP_TRUSTED_PROXIES is set
     * (avoids spoofing). Falls back to REMOTE_ADDR.
     */
    public static function get_real_ip() {
        $remote = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
        $remote = filter_var( $remote, FILTER_VALIDATE_IP ) ? $remote : '';

        $trusted = defined( 'SHIELDFORGE_TRUSTED_PROXIES' ) ? (array) SHIELDFORGE_TRUSTED_PROXIES : array();
        if ( ! empty( $trusted ) && self::ip_in_list( $remote, $trusted ) ) {
            $headers = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR' );
            foreach ( $headers as $h ) {
                if ( ! empty( $_SERVER[ $h ] ) ) {
                    $candidate = trim( explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $h ] ) ) )[0] );
                    if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
                        return $candidate;
                    }
                }
            }
        }
        return $remote;
    }

    /**
     * GDPR-friendly IP anonymization.
     *  - IPv4: zero out last octet (1.2.3.4 -> 1.2.3.0)
     *  - IPv6: keep first /64, zero the rest
     */
    public static function anonymize_ip( $ip ) {
        if ( ! $ip || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
            return '';
        }
        if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
            $parts = explode( '.', $ip );
            $parts[3] = '0';
            return implode( '.', $parts );
        }
        // IPv6: collapse to /64
        $packed = inet_pton( $ip );
        if ( false === $packed ) {
            return '';
        }
        $masked = substr( $packed, 0, 8 ) . str_repeat( "\0", 8 );
        return inet_ntop( $masked );
    }

    /**
     * Stable hash of a username (no reversal, no collision in practice).
     * Uses wp_salt() so each install has a different namespace.
     */
    public static function pseudonymize_username( $username ) {
        if ( '' === $username || null === $username ) {
            return '';
        }
        return 'u_' . substr( hash_hmac( 'sha256', strtolower( $username ), wp_salt( 'auth' ) ), 0, 12 );
    }

    /**
     * Check whether an IP is inside a CIDR range (IPv4 + IPv6).
     */
    public static function ip_in_cidr( $ip, $cidr ) {
        if ( strpos( $cidr, '/' ) === false ) {
            return $ip === $cidr;
        }
        list( $subnet, $mask ) = explode( '/', $cidr, 2 );
        $mask = (int) $mask;

        $ip_packed     = @inet_pton( $ip ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- malformed IP triggers a PHP warning; failure yields false, handled by the false === check below.
        $subnet_packed = @inet_pton( $subnet ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- malformed subnet triggers a PHP warning; failure yields false, handled by the false === check below.
        if ( false === $ip_packed || false === $subnet_packed || strlen( $ip_packed ) !== strlen( $subnet_packed ) ) {
            return false;
        }

        $bytes_full   = intdiv( $mask, 8 );
        $bits_partial = $mask % 8;

        if ( substr( $ip_packed, 0, $bytes_full ) !== substr( $subnet_packed, 0, $bytes_full ) ) {
            return false;
        }
        if ( 0 === $bits_partial ) {
            return true;
        }
        $mask_byte = chr( ( 0xFF << ( 8 - $bits_partial ) ) & 0xFF );
        return ( $ip_packed[ $bytes_full ] & $mask_byte ) === ( $subnet_packed[ $bytes_full ] & $mask_byte );
    }

    public static function ip_in_list( $ip, array $list ) {
        foreach ( $list as $entry ) {
            $entry = trim( $entry );
            if ( '' === $entry ) {
                continue;
            }
            if ( self::ip_in_cidr( $ip, $entry ) ) {
                return true;
            }
        }
        return false;
    }

    public static function safe_user_agent() {
        $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
        return mb_substr( sanitize_text_field( $ua ), 0, 255 );
    }

    public static function safe_request_uri() {
        $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
        return mb_substr( esc_url_raw( $uri ), 0, 500 );
    }

    public static function current_blog_id() {
        return is_multisite() ? (int) get_current_blog_id() : 0;
    }

    public static function table( $name ) {
        global $wpdb;
        return $wpdb->base_prefix . 'shieldforge_' . $name;
    }

    /**
     * Returns the IP form that is/will be stored in the log table — i.e. the
     * anonymized form if privacy_anonymize_ip is on, otherwise the raw IP.
     *
     * Use this everywhere we *count*, *block* or *look up* IPs against the
     * log/blocks tables, so the comparison value matches what was actually
     * persisted. Mixing raw and anonymized IPs across these paths produces
     * silent counter-zero / lockup-skipped bugs.
     */
    public static function store_ip( $ip ) {
        if ( ! $ip ) {
            return '';
        }
        if ( Shieldforge_Settings::get( 'privacy_anonymize_ip', true ) ) {
            return self::anonymize_ip( $ip );
        }
        return $ip;
    }

    /**
     * Returns the username form that gets stored in the log table — i.e. the
     * pseudonymized form if privacy_pseudonym_users is on, otherwise raw.
     * Use everywhere we count, lock or look up usernames against the log so
     * the comparison value matches what was actually persisted.
     */
    public static function store_username( $username ) {
        $username = (string) $username;
        if ( '' === $username ) {
            return '';
        }
        if ( Shieldforge_Settings::get( 'privacy_pseudonym_users', false ) ) {
            return self::pseudonymize_username( $username );
        }
        return $username;
    }

    /**
     * Emit a small block of (server-generated) JavaScript the WordPress way.
     *
     * Instead of printing a raw <script> tag from a template — which the
     * WordPress.org guidelines discourage — we register a script handle with no
     * external file (src = false) and attach the code via wp_add_inline_script().
     * WordPress then prints it in the footer through its normal dependency /
     * queue machinery. Safe to call late (during an admin page render), because
     * footer scripts have not been printed yet at that point.
     *
     * @param string $handle Unique handle, e.g. 'shieldforge-inline-scanner'.
     * @param string $js     JavaScript body (no <script> wrapper).
     */
    public static function enqueue_inline_script( $handle, $js ) {
        if ( ! function_exists( 'wp_add_inline_script' ) ) {
            return;
        }
        wp_register_script( $handle, false, array(), defined( 'SHIELDFORGE_VERSION' ) ? SHIELDFORGE_VERSION : false, true );
        wp_enqueue_script( $handle );
        wp_add_inline_script( $handle, $js );
    }
}
