<?php

namespace UltraguardPro\modules\traffic_monitor;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Traffic capture intentionally writes and summarizes live request data directly for security telemetry.

use UltraguardPro\core\BaseController;
use UltraguardPro\core\DatabaseCrud;
use UltraguardPro\core\Utils;
use UltraguardPro\Plugin;

class TrafficCaptureEngine extends BaseController {

    private const HOURLY_TRANSIENT_PREFIX = 'ultraguard_tm_auto_block_';

    public function __construct( ?Plugin $plugin = null ) {
        parent::__construct( $plugin );
    }

    public function maybe_log_request(): void {
        if ( ! $this->plugin || ! $this->plugin->settings->get_option( 'traffic_monitor_enabled', true ) ) {
            return;
        }

        $ip = $this->resolve_ip_address();
        if ( ! $ip ) {
            return;
        }

        $skip_localhost = (bool) $this->plugin->settings->get_option( 'traffic_monitor_skip_localhost', true );
        if ( $skip_localhost && in_array( $ip, [ '127.0.0.1', '::1', 'localhost' ], true ) && ! defined( 'WP_DEBUG' ) ) {
            return;
        }

        if ( (bool) $this->plugin->settings->get_option( 'traffic_monitor_ignore_logged_in', true ) && is_user_logged_in() && \UltraguardPro\ultraguard_current_user_can_manage() ) {
            return;
        }

        $user_agent = sanitize_text_field( substr( wp_unslash( (string) ( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), 0, 500 ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- unslashed and sanitized before use.
        $method     = sanitize_text_field( strtoupper( wp_unslash( (string) ( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- unslashed and sanitized before use.
        $uri        = sanitize_textarea_field( substr( wp_unslash( (string) ( $_SERVER['REQUEST_URI'] ?? '/' ) ), 0, 2000 ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- unslashed and sanitized before use.
        $host       = sanitize_text_field( substr( wp_unslash( (string) ( $_SERVER['HTTP_HOST'] ?? '' ) ), 0, 255 ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- unslashed and sanitized before use.
        $referrer   = sanitize_textarea_field( substr( wp_unslash( (string) ( $_SERVER['HTTP_REFERER'] ?? '' ) ), 0, 1000 ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- unslashed and sanitized before use.

        if ( $this->should_ignore_request( $user_agent, $uri ) ) {
            return;
        }

        $classification = $this->classify_request( $uri, $user_agent );
        $geo            = $this->resolve_geo_data( $ip, true );
        $payload        = [
            'ip_address'      => $ip,
            'ip_country'      => $geo['country_code'],
            'ip_country_name' => $geo['country_name'],
            'ip_city'         => $geo['city'],
            'user_agent'      => $user_agent,
            'request_method'  => $method,
            'request_uri'     => $uri,
            'request_host'    => $host,
            'referrer'        => $referrer,
            'response_code'   => max( 0, (int) http_response_code() ),
            'user_id'         => get_current_user_id() ?: 0,
            'threat_type'     => $classification['threat_type'],
            'threat_level'    => $classification['threat_level'],
            'was_blocked'     => 0,
            'block_reason'    => '',
            'is_bot'          => $classification['is_bot'] ? 1 : 0,
        ];

        $this->insert_log( $payload );

        if ( $classification['threat_level'] === 'high' || $classification['threat_level'] === 'critical' ) {
            $this->maybe_auto_block_ip( $ip, $classification );
        }
    }

    public function insert_log( array $payload ): bool {
        $table = $this->getTable( 'traffic_logs' );
        if ( ! $table ) {
            return false;
        }

        return (bool) $table->insert( $payload );
    }

    public function get_live_traffic( array $filters, int $limit = 50 ): array {
        global $wpdb;

        $table   = $wpdb->prefix . 'ultraguard_traffic_logs';
        $limit   = max( 1, min( 500, $limit ) );
        $page    = max( 1, absint( $filters['page'] ?? 1 ) );
        $offset  = ( $page - 1 ) * $limit;
        $where   = [ '1=1' ];
        $values  = [];

        if ( ! empty( $filters['ip'] ) ) {
            $where[]  = 'ip_address LIKE %s';
            $values[] = '%' . $wpdb->esc_like( sanitize_text_field( $filters['ip'] ) ) . '%';
        }

        if ( ! empty( $filters['threat'] ) ) {
            $where[]  = 'threat_type = %s';
            $values[] = sanitize_key( $filters['threat'] );
        }

        if ( ! empty( $filters['blocked_only'] ) ) {
            $where[] = 'was_blocked = 1';
        }

        if ( ! empty( $filters['date_from'] ) ) {
            $where[]  = 'created_at >= %s';
            $values[] = sanitize_text_field( $filters['date_from'] . ' 00:00:00' );
        }

        if ( ! empty( $filters['date_to'] ) ) {
            $where[]  = 'created_at <= %s';
            $values[] = sanitize_text_field( $filters['date_to'] . ' 23:59:59' );
        }

        $safe_table = esc_sql( $table );
        $order_by   = in_array( $filters['orderby'] ?? '', [ 'created_at', 'ip_address', 'threat_type', 'response_code' ], true ) ? $filters['orderby'] : 'created_at';
        $order      = strtoupper( $filters['order'] ?? 'DESC' ) === 'ASC' ? 'ASC' : 'DESC';
        $where_sql  = implode( ' AND ', $where );

        $row_query_values = array_merge( $values, [ $limit, $offset ] );
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- internal WHERE/ORDER fragments are assembled only from validated allowlists and placeholders.
        $rows = $wpdb->get_results(
            $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- placeholder count is satisfied by runtime spread values.
                "SELECT * FROM `{$safe_table}` WHERE {$where_sql} ORDER BY {$order_by} {$order} LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- validated table, where, and order fragments are assembled from allowlists/placeholders.
                ...$row_query_values
            ),
            ARRAY_A
        ) ?: [];

        foreach ( $rows as &$row ) {
            $row = $this->backfill_geo_for_row( $row, $table );
        }

        $count_query = ! empty( $values )
            ? $wpdb->prepare( "SELECT COUNT(*) FROM `{$safe_table}` WHERE {$where_sql}", ...$values ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- validated table/WHERE fragments with runtime placeholder list.
            : "SELECT COUNT(*) FROM `{$safe_table}` WHERE {$where_sql}";

        return [
            'rows'  => $rows,
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- internal WHERE fragment is assembled only from validated allowlists and placeholders.
            'total' => (int) $wpdb->get_var( $count_query ),
            'page'  => $page,
            'limit' => $limit,
        ];
    }

    public function get_traffic_stats( int $hours = 24 ): array {
        global $wpdb;

        $hours = max( 1, min( 168, $hours ) );
        $table = $wpdb->prefix . 'ultraguard_traffic_logs';
        $since = gmdate( 'Y-m-d H:i:s', time() - $hours * HOUR_IN_SECONDS );

        $this->backfill_missing_geo_rows( $table, $since );

        $timeline = $wpdb->get_results(  // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- internal table identifier is resolved by plugin table registry.
            $wpdb->prepare(
                "SELECT DATE_FORMAT(created_at, '%%Y-%%m-%%d %%H:00:00') AS bucket,
                        SUM(CASE WHEN threat_level = 'none' THEN 1 ELSE 0 END) AS clean_count,
                        SUM(CASE WHEN threat_level <> 'none' THEN 1 ELSE 0 END) AS threat_count
                 " .
                 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- validated internal table identifier.
                 "FROM `{$safe_table}`
                 WHERE created_at >= %s
                 GROUP BY bucket
                 ORDER BY bucket ASC",
                $since
            ),
            ARRAY_A
        );

        $threats = DatabaseCrud::get_grouped_count_since( 'ultraguard_traffic_logs', 'threat_type', [], 'created_at', $since, 8 );
        $threats = array_values(
            array_filter(
                array_map(
                    static function ( $row ): ?array {
                        $type = is_object( $row ) ? (string) ( $row->threat_type ?? '' ) : (string) ( $row['threat_type'] ?? '' );
                        if ( '' === $type || 'clean' === $type ) {
                            return null;
                        }

                        return [
                            'threat_type' => $type,
                            'total'       => (int) ( is_object( $row ) ? ( $row->count ?? 0 ) : ( $row['count'] ?? 0 ) ),
                        ];
                    },
                    (array) $threats
                )
            )
        );

        $countries = $wpdb->get_results(  // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- internal table identifier is resolved by plugin table registry.
            $wpdb->prepare(
                "SELECT ip_country, ip_country_name, COUNT(*) AS total
                 " .
                 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- validated internal table identifier.
                 "FROM `{$safe_table}`
                 WHERE created_at >= %s AND ip_country IS NOT NULL AND ip_country <> ''
                 GROUP BY ip_country, ip_country_name
                 ORDER BY total DESC
                 LIMIT 10",
                $since
            ),
            ARRAY_A
        );

        return [
            'timeline'  => $timeline,
            'threats'   => $threats,
            'countries' => $countries,
        ];
    }

    public function get_overview_stats( int $hours = 24 ): array {
        $since = gmdate( 'Y-m-d H:i:s', time() - max( 1, $hours ) * HOUR_IN_SECONDS );
        $total = DatabaseCrud::count_since( 'ultraguard_traffic_logs', [], 'created_at', $since );
        $clean = DatabaseCrud::count_since( 'ultraguard_traffic_logs', [ 'threat_level' => 'none' ], 'created_at', $since );

        return [
            'total_requests' => $total,
            'threats'        => max( 0, $total - $clean ),
            'blocked'        => DatabaseCrud::count_since( 'ultraguard_traffic_logs', [ 'was_blocked' => 1 ], 'created_at', $since ),
            'unique_ips'     => DatabaseCrud::count_distinct_since( 'ultraguard_traffic_logs', 'ip_address', [], 'created_at', $since ),
        ];
    }

    public function prune_old_logs(): void {
        $days = max( 1, min( 90, (int) $this->plugin->settings->get_option( 'traffic_monitor_retention_days', 7 ) ) );
        DatabaseCrud::delete_old_records( 'ultraguard_traffic_logs', 'created_at', $days, true );
    }

    private function resolve_ip_address(): string {
        return Utils::get_client_ip();
    }

    private function classify_request( string $uri, string $user_agent ): array {
        $uri_lower = strtolower( $uri );
        $ua_lower  = strtolower( $user_agent );
        $query     = strtolower( (string) wp_parse_url( $uri, PHP_URL_QUERY ) );

        $is_bot = (bool) preg_match( '/bot|crawler|spider|bingpreview|googlebot|slurp|duckduckbot|yandex/i', $user_agent );
        $type   = 'clean';
        $level  = 'none';

        if ( strpos( $uri_lower, 'wp-login.php' ) !== false && strtoupper( wp_unslash( (string) ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) ) === 'POST' ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- unslashed server method is compared to a fixed literal only.
            $type  = 'brute_force';
            $level = 'medium';
        }

        if ( preg_match( '/union(\s+all)?\s+select|select\s+.*from|sleep\(|benchmark\(|information_schema|or\s+1=1/i', $uri_lower . ' ' . $query ) ) {
            $type  = 'sqli';
            $level = 'high';
        }

        if ( preg_match( '/<script|%3Cscript|onerror=|onload=|javascript:/i', $uri_lower . ' ' . $query ) ) {
            $type  = 'xss';
            $level = 'high';
        }

        if ( preg_match( '/sqlmap|nikto|masscan|zgrab|nmap|nuclei|gobuster|dirbuster|acunetix/i', $ua_lower ) ) {
            $type   = 'scanner';
            $level  = 'critical';
            $is_bot = true;
        }

        if ( preg_match( '/comment=|message=|contact=|\/xmlrpc\.php/i', $uri_lower ) && preg_match( '/viagra|casino|seo|backlinks/i', $query ) ) {
            $type  = 'spam';
            $level = 'low';
        }

        return [
            'threat_type'  => $type,
            'threat_level' => $level,
            'is_bot'       => $is_bot,
        ];
    }

    private function should_ignore_request( string $user_agent, string $uri ): bool {
        $ua_patterns  = preg_split( '/\r\n|\r|\n/', (string) $this->plugin->settings->get_option( 'traffic_monitor_ignore_user_agents', '' ) );
        $uri_patterns = preg_split( '/\r\n|\r|\n/', (string) $this->plugin->settings->get_option( 'traffic_monitor_ignore_uri_patterns', '' ) );

        foreach ( $ua_patterns as $pattern ) {
            $pattern = trim( (string) $pattern );
            if ( $pattern !== '' && stripos( $user_agent, $pattern ) !== false ) {
                return true;
            }
        }

        foreach ( $uri_patterns as $pattern ) {
            $pattern = trim( (string) $pattern );
            if ( $pattern !== '' && stripos( $uri, $pattern ) !== false ) {
                return true;
            }
        }

        return false;
    }

    private function maybe_auto_block_ip( string $ip, array $classification ): void {
        if ( ! $this->plugin->settings->get_option( 'traffic_monitor_auto_block_enabled', false ) ) {
            return;
        }

        $allowed_types = (array) $this->plugin->settings->get_option( 'traffic_monitor_auto_block_types', [ 'sqli', 'xss', 'scanner', 'brute_force' ] );
        if ( ! in_array( $classification['threat_type'], $allowed_types, true ) ) {
            return;
        }

        $threshold = max( 1, (int) $this->plugin->settings->get_option( 'traffic_monitor_auto_block_threshold', 5 ) );
        $key       = self::HOURLY_TRANSIENT_PREFIX . md5( $ip . gmdate( 'YmdH' ) );
        $hits      = (int) get_transient( $key ) + 1;
        set_transient( $key, $hits, HOUR_IN_SECONDS );

        if ( $hits < $threshold ) {
            return;
        }

        do_action( 'ultraguard_pro/traffic_monitor/block_ip', $ip, 'Auto-blocked by traffic monitor', $classification['threat_level'] );
    }

    private function resolve_geo_data( string $ip, bool $allow_external = false ): array {
        $result = [
            'country_code' => '',
            'country_name' => '',
            'city'         => '',
        ];

        $candidates = [
            WP_CONTENT_DIR . '/uploads/geolite2/GeoLite2-City.mmdb',
            WP_CONTENT_DIR . '/uploads/GeoLite2-City.mmdb',
            ULTRAGUARD_PRO_PATH . 'assets/data/GeoLite2-City.mmdb',
        ];

        $db_path = '';
        foreach ( $candidates as $candidate ) {
            if ( file_exists( $candidate ) ) {
                $db_path = $candidate;
                break;
            }
        }

        if ( $db_path && class_exists( '\\MaxMind\\Db\\Reader' ) ) {
            try {
                $reader = new \MaxMind\Db\Reader( $db_path );
                $geo    = $reader->get( $ip );
                $reader->close();

                $result['country_code'] = sanitize_text_field( (string) ( $geo['country']['iso_code'] ?? '' ) );
                $result['country_name'] = sanitize_text_field( (string) ( $geo['country']['names']['en'] ?? '' ) );
                $result['city']         = sanitize_text_field( (string) ( $geo['city']['names']['en'] ?? '' ) );
            } catch ( \Throwable $e ) {
                $result = [
                    'country_code' => '',
                    'country_name' => '',
                    'city'         => '',
                ];
            }
        }

        if ( '' === $result['country_code'] ) {
            $country_code = strtoupper( (string) Utils::get_country_from_ip( $ip, [ 'skip_external' => ! $allow_external ] ) );
            if ( 'XX' !== $country_code && preg_match( '/^[A-Z]{2}$/', $country_code ) ) {
                $result['country_code'] = $country_code;
                $result['country_name'] = $result['country_name'] ?: Utils::get_country_name_from_code( $country_code );
            }
        }

        return $result;
    }

    private function backfill_missing_geo_rows( string $table, string $since, int $limit = 250 ): void {
        global $wpdb;

        $safe_table = esc_sql( $table );
        $rows = $wpdb->get_results(  // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- validated table identifier and prepared scalar values.
            $wpdb->prepare(
                "SELECT id, ip_address, ip_country, ip_country_name, ip_city
                 " .
                 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- validated internal table identifier.
                 "FROM `{$safe_table}`
                 WHERE created_at >= %s
                   AND ip_address <> ''
                   AND ( ip_country IS NULL OR ip_country = '' )
                 ORDER BY created_at DESC
                 LIMIT %d",
                $since,
                max( 1, $limit )
            ),
            ARRAY_A
        );

        foreach ( $rows as $row ) {
            $this->backfill_geo_for_row( $row, $table );
        }
    }

    private function backfill_geo_for_row( array $row, string $table ): array {
        global $wpdb;

        if ( ! empty( $row['ip_country'] ) || empty( $row['ip_address'] ) ) {
            return $row;
        }

        $geo = $this->resolve_geo_data( (string) $row['ip_address'], false );
        if ( '' === (string) ( $geo['country_code'] ?? '' ) && '' === (string) ( $geo['country_name'] ?? '' ) ) {
            return $row;
        }

        $row['ip_country']      = (string) ( $geo['country_code'] ?? '' );
        $row['ip_country_name'] = (string) ( $geo['country_name'] ?? '' );
        $row['ip_city']         = (string) ( $geo['city'] ?? '' );

        if ( ! empty( $row['id'] ) ) {
            $wpdb->update(
                $table,
                [
                    'ip_country'      => $row['ip_country'],
                    'ip_country_name' => $row['ip_country_name'],
                    'ip_city'         => $row['ip_city'],
                ],
                [ 'id' => (int) $row['id'] ],
                [ '%s', '%s', '%s' ],
                [ '%d' ]
            );
        }

        return $row;
    }
}
