<?php
/**
 * LoginAttemptsTable — Per-attempt log (IP, username, result).
 *
 * @package UltraguardPro\modules\loginlimiter\db
 * @since   6.5.0
 */

namespace UltraguardPro\modules\loginlimiter\db;

use UltraguardPro\db\BaseTable;
use UltraguardPro\core\DatabaseCrud;
use UltraguardPro\core\Utils;

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

class LoginAttemptsTable extends BaseTable {

    protected $table_name  = 'ultraguard_login_attempts';
    protected $cache_group = 'ultraguard_login_attempts';

    // ── BaseTable contract ─────────────────────────────────────────────────────

    public function get_defaults(): array {
        return [
            'id'           => 0,
            'ip_address'   => '',
            'username'     => '',
            'status'       => 'failed',
            'user_agent'   => null,
            'country_code' => null,
            'created_at'   => '',
        ];
    }

    protected function validate_data( $data ) {
        if ( empty( $data['ip_address'] ) ) { return false; }

        $valid_statuses = [ 'success', 'failed', 'blocked' ];
        if ( isset( $data['status'] ) && ! in_array( $data['status'], $valid_statuses, true ) ) {
            $data['status'] = 'failed';
        }

        return $data;
    }

    protected function sanitize_data( $data ): array {
        $out = [];
        foreach ( $data as $k => $v ) {
            $out[ $k ] = match ( $k ) {
                'user_agent'   => is_null( $v ) ? null : sanitize_textarea_field( (string) $v ),
                'country_code' => is_null( $v ) ? null : strtoupper( substr( sanitize_key( (string) $v ), 0, 5 ) ),
                default        => is_string( $v ) ? sanitize_text_field( $v ) : $v,
            };
        }
        return $out;
    }

    protected function get_searchable_fields(): array {
        return [ 'ip_address', 'username', 'status' ];
    }

    // ── Domain methods ─────────────────────────────────────────────────────────

    /**
     * Log a single login attempt.
     *
     * @param  string $ip
     * @param  string $username
     * @param  string $status    'success' | 'failed' | 'blocked'
     * @return int|false  New row ID or false.
     */
    public function log_attempt( string $ip, string $username, string $status = 'failed' ): int|false {
        $country_code = strtoupper( (string) Utils::get_country_from_ip( $ip ) );

        return $this->insert( [
            'ip_address'   => $ip,
            'username'     => substr( $username, 0, 60 ),
            'status'       => $status,
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- User agent is unslashed, length-limited, and sanitized immediately.
            'user_agent'   => sanitize_text_field( substr( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ), 0, 500 ) ),
            'country_code' => ( 'XX' !== $country_code && preg_match( '/^[A-Z]{2}$/', $country_code ) ) ? $country_code : null,
        ] );
    }

    /**
     * Count failed attempts from an IP within a rolling time window.
     * Uses a direct DB query for speed (called on every failed login).
     *
     * @param  string $ip
     * @param  int    $window_minutes
     * @return int
     */
    public function count_failures( string $ip, int $window_minutes = 60 ): int {
        $cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $window_minutes * 60 ) );

        return DatabaseCrud::count_since(
            $this->table_name,
            [ 'ip_address' => $ip, 'status' => 'failed' ],
            'created_at',
            $cutoff
        );
    }

    /**
     * Top-N attacker IPs by failure count within a time window.
     *
     * @param  int $limit
     * @param  int $window_hours
     * @return array  [ ['ip_address' => '...', 'count' => N], ... ]
     */
    public function get_top_attackers( int $limit = 10, int $window_hours = 24 ): array {
        $cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $window_hours * 3600 ) );

        return DatabaseCrud::get_grouped_count_since(
            $this->table_name,
            'ip_address',
            [ 'status' => 'failed' ],
            'created_at',
            $cutoff,
            $limit
        );
    }

    /**
     * Stat snapshot for dashboard cards.
     *
     * @return array{total:int, failed:int, success:int, failed_today:int, failed_24h:int}
     */
    public function get_attempt_summary(): array {
        $today = gmdate( 'Y-m-d 00:00:00' );
        $h24   = gmdate( 'Y-m-d H:i:s', strtotime( '-24 hours' ) );

        return [
            'total'       => $this->count(),
            'failed'      => $this->count( [ 'status' => 'failed' ] ),
            'success'     => $this->count( [ 'status' => 'success' ] ),
            'failed_today'=> $this->count( [ 'status' => 'failed', 'created_at >=' => $today ] ),
            'failed_24h'  => $this->count( [ 'status' => 'failed', 'created_at >=' => $h24   ] ),
        ];
    }
}
