<?php
/**
 * Trusted Device Repository
 *
 * Repository pattern implementation for managing trusted devices used in 2FA bypass.
 * This class encapsulates all database operations for the trusted_devices table,
 * providing a clean API for device management operations.
 *
 * @package     WPUltimateSecurity\UltimateSecurity\Infrastructure\Persistence\Repositories
 * @since       1.0.0
 * @author      Ultimate Security Team
 * @license     GPL-2.0-or-later
 */

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Infrastructure\Persistence\Repositories;

use WPUltimateSecurity\UltimateSecurity\Contracts\RepositoryInterface;
use WPUltimateSecurity\UltimateSecurity\Support\Result;
use WPUltimateSecurity\UltimateSecurity\Support\Exceptions\DatabaseException;
use WPUltimateSecurity\UltimateSecurity\Support\Helpers\DateHelper;

/**
 * Class TrustedDeviceRepository
 *
 * Manages CRUD operations and domain-specific queries for trusted devices.
 * Trusted devices allow users to bypass 2FA verification on recognized devices
 * for a specified period.
 *
 * Table structure (trusted_devices):
 * - id: Primary key
 * - user_id: WordPress user ID
 * - device_fingerprint: Unique device fingerprint hash
 * - device_token: Secure token stored in cookie
 * - device_name: Human-readable device name
 * - browser, browser_version: Browser information
 * - os, os_version: Operating system information
 * - device_type: desktop, mobile, tablet
 * - ip_address: Last known IP
 * - location: Approximate location
 * - last_used_at: Last successful verification bypass
 * - expires_at: Trust expiration datetime
 * - is_revoked: Manual revocation flag
 * - created_at, updated_at: Timestamps
 *
 * Usage example:
 * ```php
 * $repository = new TrustedDeviceRepository();
 *
 * // Check if device is trusted
 * if ($repository->isDeviceTrusted($userId, $token, $fingerprint)) {
 *     // Skip 2FA verification
 * }
 *
 * // Register a new trusted device
 * $deviceId = $repository->create([
 *     'user_id' => $userId,
 *     'device_fingerprint' => $fingerprint,
 *     'device_token' => $token,
 *     'device_name' => 'Chrome on Windows',
 *     'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')),
 * ]);
 *
 * // Revoke all devices for a user
 * $result = $repository->revokeAllForUser($userId);
 * ```
 *
 * @since 1.0.0
 */
class TrustedDeviceRepository implements RepositoryInterface
{
    /**
     * Full table name with WordPress prefix.
     *
     * @since 1.0.0
     * @var string
     */
    private string $table;

    /**
     * WordPress database object instance.
     *
     * @since 1.0.0
     * @var \wpdb
     */
    private \wpdb $wpdb;

    /**
     * Default number of items per page for pagination.
     *
     * @since 1.0.0
     * @var int
     */
    private const DEFAULT_PER_PAGE = 20;

    /**
     * Maximum number of items per page.
     *
     * @since 1.0.0
     * @var int
     */
    private const MAX_PER_PAGE = 100;

    /**
     * Constructor.
     *
     * Initializes the repository with the WordPress database instance
     * and sets up the table name.
     *
     * @since 1.0.0
     */
    public function __construct()
    {
        global $wpdb;
        $this->wpdb  = $wpdb;
        $this->table = $wpdb->prefix . 'ultimate_security_trusted_devices';
    }

    /**
     * Find a single trusted device by its ID.
     *
     * Retrieves complete device information including all metadata.
     *
     * @since 1.0.0
     *
     * @param int $id The device ID to find.
     *
     * @return object|null The device record as an object, or null if not found.
     */
    public function find(int $id): ?object
    {
        $result = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT * FROM {$this->table} WHERE id = %d",
                $id
            )
        );

        return $result ?: null;
    }

    /**
     * Find a single trusted device by ID or throw an exception.
     *
     * @since 2.0.0
     *
     * @param int $id The device ID to find.
     *
     * @return object The device record.
     *
     * @throws \RuntimeException If device not found.
     */
    public function findOrFail(int $id): object
    {
        $result = $this->find($id);

        if ($result === null) {
            throw new \RuntimeException(
                sprintf('Trusted device not found: %d', $id)
            );
        }

        return $result;
    }

    /**
     * Find all trusted devices matching the given criteria.
     *
     * Supports filtering, sorting, and pagination through the criteria array.
     *
     * Supported criteria:
     * - 'where': Associative array of field => value conditions
     * - 'orderby': Column to sort by (default: 'created_at')
     * - 'order': Sort direction 'ASC' or 'DESC' (default: 'DESC')
     * - 'limit': Max records to return (default: 20, max: 100)
     * - 'offset': Records to skip (default: 0)
     * - 'page': Page number (alternative to offset, 1-based)
     *
     * @since 1.0.0
     *
     * @param array<string, mixed> $criteria Optional filtering and pagination criteria.
     *
     * @return array<int, object> Array of matching device records.
     */
    public function findAll(array $criteria = []): array
    {
        $where_clauses = [];
        $where_values  = [];

        // Build WHERE conditions from criteria.
        if (!empty($criteria['where']) && is_array($criteria['where'])) {
            foreach ($criteria['where'] as $field => $value) {
                $sanitized_field = $this->sanitizeColumnName($field);
                if ($sanitized_field) {
                    $where_clauses[] = "`{$sanitized_field}` = %s";
                    $where_values[]  = $value;
                }
            }
        }

        // Handle common filter shortcuts.
        if (isset($criteria['user_id'])) {
            $where_clauses[] = 'user_id = %d';
            $where_values[]  = (int) $criteria['user_id'];
        }

        if (isset($criteria['is_revoked'])) {
            $where_clauses[] = 'is_revoked = %d';
            $where_values[]  = (int) $criteria['is_revoked'];
        }

        if (isset($criteria['active_only']) && $criteria['active_only']) {
            $where_clauses[] = 'is_revoked = 0';
            $where_clauses[] = 'expires_at > %s';
            $where_values[]  = DateHelper::now();
        }

        // Build WHERE clause.
        $where_sql = '';
        if (!empty($where_clauses)) {
            $where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
        }

        // Handle ordering.
        $orderby = $this->sanitizeColumnName($criteria['orderby'] ?? 'created_at') ?: 'created_at';
        $order   = strtoupper($criteria['order'] ?? 'DESC');
        $order   = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';

        // Handle pagination.
        $limit  = min((int) ($criteria['limit'] ?? self::DEFAULT_PER_PAGE), self::MAX_PER_PAGE);
        $limit  = max($limit, 1);
        $offset = (int) ($criteria['offset'] ?? 0);

        // Support page-based pagination.
        if (isset($criteria['page']) && $criteria['page'] > 0) {
            $page   = (int) $criteria['page'];
            $offset = ($page - 1) * $limit;
        }

        // Build the query.
        $sql = "SELECT * FROM {$this->table} {$where_sql} ORDER BY `{$orderby}` {$order} LIMIT %d OFFSET %d";

        // Add limit and offset to values.
        $where_values[] = $limit;
        $where_values[] = $offset;

        // Execute query.
        if (!empty($where_values)) {
            $prepared_sql = $this->wpdb->prepare($sql, $where_values);
            $results      = $this->wpdb->get_results($prepared_sql);
        } else {
            $results = $this->wpdb->get_results($sql);
        }

        return $results ?: [];
    }

    /**
     * Create a new trusted device record.
     *
     * Required fields:
     * - user_id: The WordPress user ID
     * - device_fingerprint: Unique device identifier hash
     * - device_token: Secure random token for cookie
     * - expires_at: Trust expiration datetime
     *
     * @since 1.0.0
     *
     * @param array<string, mixed> $data Device data to insert.
     *
     * @return int|false The new device ID on success, false on failure.
     *
     * @throws DatabaseException When the insert operation fails.
     */
    public function create(array $data): int|false
    {
        // Validate required fields.
        $required = ['user_id', 'device_fingerprint', 'device_token', 'expires_at'];
        foreach ($required as $field) {
            if (empty($data[$field])) {
                return false;
            }
        }

        // Prepare data with defaults.
        $insert_data = [
            'user_id'            => (int) $data['user_id'],
            'device_fingerprint' => sanitize_text_field($data['device_fingerprint']),
            'device_token'       => sanitize_text_field($data['device_token']),
            'device_name'        => sanitize_text_field($data['device_name'] ?? ''),
            'browser'            => sanitize_text_field($data['browser'] ?? ''),
            'browser_version'    => sanitize_text_field($data['browser_version'] ?? ''),
            'os'                 => sanitize_text_field($data['os'] ?? ''),
            'os_version'         => sanitize_text_field($data['os_version'] ?? ''),
            'device_type'        => sanitize_text_field($data['device_type'] ?? 'desktop'),
            'ip_address'         => sanitize_text_field($data['ip_address'] ?? ''),
            'location'           => sanitize_text_field($data['location'] ?? ''),
            'last_used_at'       => $data['last_used_at'] ?? DateHelper::now(),
            'expires_at'         => $data['expires_at'],
            'is_revoked'         => (int) ($data['is_revoked'] ?? 0),
        ];

        $format = [
            '%d', // user_id
            '%s', // device_fingerprint
            '%s', // device_token
            '%s', // device_name
            '%s', // browser
            '%s', // browser_version
            '%s', // os
            '%s', // os_version
            '%s', // device_type
            '%s', // ip_address
            '%s', // location
            '%s', // last_used_at
            '%s', // expires_at
            '%d', // is_revoked
        ];

        $result = $this->wpdb->insert($this->table, $insert_data, $format);

        if ($result === false) {
            throw DatabaseException::insertFailed(
                $this->table,
                $this->wpdb->last_error,
                $insert_data
            );
        }

        return (int) $this->wpdb->insert_id;
    }

    /**
     * Update an existing trusted device record.
     *
     * Only the fields present in the data array will be updated.
     * The id, user_id, device_token, and device_fingerprint cannot be changed.
     *
     * @since 1.0.0
     *
     * @param int                  $id   The device ID to update.
     * @param array<string, mixed> $data Fields to update.
     *
     * @return bool True on success, false on failure.
     *
     * @throws DatabaseException When the update operation fails due to a database error.
     */
    public function update(int $id, array $data): bool
    {
        // Remove protected fields that shouldn't be updated.
        unset($data['id'], $data['user_id'], $data['device_token'], $data['device_fingerprint'], $data['created_at']);

        if (empty($data)) {
            return false;
        }

        // Build update data with proper sanitization.
        $update_data = [];
        $format      = [];

        $string_fields = [
            'device_name', 'browser', 'browser_version',
            'os', 'os_version', 'device_type',
            'ip_address', 'location', 'last_used_at', 'expires_at',
        ];

        $int_fields = ['is_revoked'];

        foreach ($string_fields as $field) {
            if (array_key_exists($field, $data)) {
                $update_data[$field] = sanitize_text_field((string) $data[$field]);
                $format[]            = '%s';
            }
        }

        foreach ($int_fields as $field) {
            if (array_key_exists($field, $data)) {
                $update_data[$field] = (int) $data[$field];
                $format[]            = '%d';
            }
        }

        if (empty($update_data)) {
            return false;
        }

        $result = $this->wpdb->update(
            $this->table,
            $update_data,
            ['id' => $id],
            $format,
            ['%d']
        );

        if ($result === false && ! empty($this->wpdb->last_error)) {
            throw DatabaseException::updateFailed(
                $this->table,
                $this->wpdb->last_error,
                ['id' => $id]
            );
        }

        return $result !== false;
    }

    /**
     * Delete a trusted device by its ID.
     *
     * Permanently removes the device record. For soft-deletion,
     * use the revoke() method instead.
     *
     * @since 1.0.0
     *
     * @param int $id The device ID to delete.
     *
     * @return bool True if deleted, false otherwise.
     *
     * @throws DatabaseException When the delete operation fails due to a database error.
     */
    public function delete(int $id): bool
    {
        $result = $this->wpdb->delete(
            $this->table,
            ['id' => $id],
            ['%d']
        );

        if ($result === false && ! empty($this->wpdb->last_error)) {
            throw DatabaseException::deleteFailed(
                $this->table,
                $this->wpdb->last_error,
                ['id' => $id]
            );
        }

        return $result !== false && $result > 0;
    }

    /**
     * Count trusted devices matching the given criteria.
     *
     * Uses the same criteria format as findAll() for consistency.
     *
     * @since 1.0.0
     *
     * @param array<string, mixed> $criteria Optional filtering criteria.
     *
     * @return int The count of matching records.
     */
    public function count(array $criteria = []): int
    {
        $where_clauses = [];
        $where_values  = [];

        // Build WHERE conditions (same logic as findAll).
        if (!empty($criteria['where']) && is_array($criteria['where'])) {
            foreach ($criteria['where'] as $field => $value) {
                $sanitized_field = $this->sanitizeColumnName($field);
                if ($sanitized_field) {
                    $where_clauses[] = "`{$sanitized_field}` = %s";
                    $where_values[]  = $value;
                }
            }
        }

        if (isset($criteria['user_id'])) {
            $where_clauses[] = 'user_id = %d';
            $where_values[]  = (int) $criteria['user_id'];
        }

        if (isset($criteria['is_revoked'])) {
            $where_clauses[] = 'is_revoked = %d';
            $where_values[]  = (int) $criteria['is_revoked'];
        }

        if (isset($criteria['active_only']) && $criteria['active_only']) {
            $where_clauses[] = 'is_revoked = 0';
            $where_clauses[] = 'expires_at > %s';
            $where_values[]  = DateHelper::now();
        }

        // Build WHERE clause.
        $where_sql = '';
        if (!empty($where_clauses)) {
            $where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
        }

        $sql = "SELECT COUNT(*) FROM {$this->table} {$where_sql}";

        if (!empty($where_values)) {
            $count = $this->wpdb->get_var($this->wpdb->prepare($sql, $where_values));
        } else {
            $count = $this->wpdb->get_var($sql);
        }

        return (int) $count;
    }

    /**
     * Find all trusted devices for a specific user.
     *
     * @since 1.0.0
     *
     * @param int  $userId         The WordPress user ID.
     * @param bool $includeRevoked Whether to include revoked devices. Default false.
     *
     * @return array<int, object> Array of device records.
     */
    public function findByUserId(int $userId, bool $includeRevoked = false): array
    {
        $criteria = [
            'user_id' => $userId,
            'orderby' => 'last_used_at',
            'order'   => 'DESC',
            'limit'   => self::MAX_PER_PAGE,
        ];

        if (!$includeRevoked) {
            $criteria['is_revoked'] = 0;
        }

        return $this->findAll($criteria);
    }

    /**
     * Find a trusted device by its token.
     *
     * The token is stored in the user's browser cookie and used
     * to identify trusted devices.
     *
     * @since 1.0.0
     *
     * @param string $token The device token to search for.
     *
     * @return object|null The device record or null if not found.
     */
    public function findByToken(string $token): ?object
    {
        if (empty($token)) {
            return null;
        }

        $result = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT * FROM {$this->table} WHERE device_token = %s LIMIT 1",
                sanitize_text_field($token)
            )
        );

        return $result ?: null;
    }

    /**
     * Find all active (non-revoked, non-expired) devices for a user.
     *
     * @since 1.0.0
     *
     * @param int $userId The WordPress user ID.
     *
     * @return array<int, object> Array of active device records.
     */
    public function findActiveByUserId(int $userId): array
    {
        return $this->findAll([
            'user_id'     => $userId,
            'active_only' => true,
            'orderby'     => 'last_used_at',
            'order'       => 'DESC',
        ]);
    }

    /**
     * Revoke a single trusted device.
     *
     * Marks the device as revoked rather than deleting it,
     * preserving the audit trail.
     *
     * @since 1.0.0
     *
     * @param int $deviceId The device ID to revoke.
     *
     * @return Result Success or failure result with context.
     */
    public function revoke(int $deviceId): Result
    {
        $device = $this->find($deviceId);

        if (!$device) {
            return Result::failure(
                __('Device not found.', 'ultimate-security'),
                'device_not_found'
            );
        }

        if ($device->is_revoked) {
            return Result::failure(
                __('Device is already revoked.', 'ultimate-security'),
                'already_revoked'
            );
        }

        $updated = $this->update($deviceId, ['is_revoked' => 1]);

        if (!$updated) {
            return Result::failure(
                __('Failed to revoke device.', 'ultimate-security'),
                'revoke_failed'
            );
        }

        return Result::success([
            'device_id' => $deviceId,
            'user_id'   => $device->user_id,
            'message'   => __('Device successfully revoked.', 'ultimate-security'),
        ]);
    }

    /**
     * Revoke all trusted devices for a user.
     *
     * Useful when a user changes their password or when
     * forcing re-verification on all devices.
     *
     * @since 1.0.0
     *
     * @param int $userId The WordPress user ID.
     *
     * @return Result Success result with count of revoked devices.
     */
    public function revokeAllForUser(int $userId): Result
    {
        // Get count of active devices before revoking.
        $active_count = $this->count([
            'user_id'     => $userId,
            'active_only' => true,
        ]);

        if ($active_count === 0) {
            return Result::success([
                'revoked_count' => 0,
                'message'       => __('No active devices to revoke.', 'ultimate-security'),
            ]);
        }

        $result = $this->wpdb->update(
            $this->table,
            ['is_revoked' => 1],
            [
                'user_id'    => $userId,
                'is_revoked' => 0,
            ],
            ['%d'],
            ['%d', '%d']
        );

        if ($result === false) {
            $exception = DatabaseException::updateFailed(
                $this->table,
                $this->wpdb->last_error,
                ['user_id' => $userId]
            );
            return $exception->toResult();
        }

        return Result::success([
            'revoked_count' => (int) $result,
            'user_id'       => $userId,
            'message'       => sprintf(
                /* translators: %d: number of devices revoked */
                _n(
                    '%d device revoked.',
                    '%d devices revoked.',
                    (int) $result,
                    'ultimate-security'
                ),
                (int) $result
            ),
        ]);
    }

    /**
     * Clean up expired trusted devices.
     *
     * Permanently deletes devices that expired more than the specified
     * number of days ago. This helps maintain database performance
     * and comply with data retention policies.
     *
     * @since 1.0.0
     *
     * @param int $daysOld Number of days after expiration to keep records. Default 30.
     *
     * @return int Number of deleted records.
     */
    public function cleanupExpired(int $daysOld = 30): int
    {
        $cutoff_date = DateHelper::subtract(DateHelper::now(), "{$daysOld} days");

        $result = $this->wpdb->query(
            $this->wpdb->prepare(
                "DELETE FROM {$this->table} WHERE expires_at < %s",
                $cutoff_date
            )
        );

        return $result !== false ? (int) $result : 0;
    }

    /**
     * Get statistics about trusted devices.
     *
     * Returns aggregate information useful for dashboards and reports.
     *
     * @since 1.0.0
     *
     * @return array{
     *     total: int,
     *     active: int,
     *     revoked: int,
     *     expired: int,
     *     unique_users: int,
     *     by_device_type: array<string, int>,
     *     by_browser: array<string, int>,
     *     created_last_7_days: int,
     *     created_last_30_days: int
     * }
     */
    public function getStatistics(): array
    {
        $now = DateHelper::now();

        // Total count.
        $total = $this->count();

        // Active count (not revoked and not expired).
        $active = (int) $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SELECT COUNT(*) FROM {$this->table}
                WHERE is_revoked = 0 AND expires_at > %s",
                $now
            )
        );

        // Revoked count.
        $revoked = (int) $this->wpdb->get_var(
            "SELECT COUNT(*) FROM {$this->table} WHERE is_revoked = 1"
        );

        // Expired (but not revoked) count.
        $expired = (int) $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SELECT COUNT(*) FROM {$this->table}
                WHERE is_revoked = 0 AND expires_at <= %s",
                $now
            )
        );

        // Unique users with trusted devices.
        $unique_users = (int) $this->wpdb->get_var(
            "SELECT COUNT(DISTINCT user_id) FROM {$this->table}"
        );

        // Group by device type.
        $device_type_results = $this->wpdb->get_results(
            "SELECT device_type, COUNT(*) as count
            FROM {$this->table}
            GROUP BY device_type",
            OBJECT
        );
        $by_device_type = [];
        foreach ($device_type_results as $row) {
            $by_device_type[$row->device_type ?: 'unknown'] = (int) $row->count;
        }

        // Group by browser.
        $browser_results = $this->wpdb->get_results(
            "SELECT browser, COUNT(*) as count
            FROM {$this->table}
            GROUP BY browser
            ORDER BY count DESC
            LIMIT 10",
            OBJECT
        );
        $by_browser = [];
        foreach ($browser_results as $row) {
            $by_browser[$row->browser ?: 'unknown'] = (int) $row->count;
        }

        // Created in last 7 days.
        $seven_days_ago = DateHelper::subtract(DateHelper::now(), '7 days');
        $created_7_days = (int) $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SELECT COUNT(*) FROM {$this->table} WHERE created_at > %s",
                $seven_days_ago
            )
        );

        // Created in last 30 days.
        $thirty_days_ago = DateHelper::subtract(DateHelper::now(), '30 days');
        $created_30_days = (int) $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SELECT COUNT(*) FROM {$this->table} WHERE created_at > %s",
                $thirty_days_ago
            )
        );

        return [
            'total'               => $total,
            'active'              => $active,
            'revoked'             => $revoked,
            'expired'             => $expired,
            'unique_users'        => $unique_users,
            'by_device_type'      => $by_device_type,
            'by_browser'          => $by_browser,
            'created_last_7_days' => $created_7_days,
            'created_last_30_days'=> $created_30_days,
        ];
    }

    /**
     * Check if a device is trusted for a specific user.
     *
     * Validates that the device token and fingerprint match,
     * the device is not revoked, and has not expired.
     *
     * @since 1.0.0
     *
     * @param int    $userId      The WordPress user ID.
     * @param string $token       The device token from cookie.
     * @param string $fingerprint The device fingerprint.
     *
     * @return bool True if the device is trusted and valid.
     */
    public function isDeviceTrusted(int $userId, string $token, string $fingerprint): bool
    {
        if (empty($token) || empty($fingerprint)) {
            return false;
        }

        $device = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT * FROM {$this->table}
                WHERE user_id = %d
                AND device_token = %s
                AND device_fingerprint = %s
                AND is_revoked = 0
                AND expires_at > %s
                LIMIT 1",
                $userId,
                sanitize_text_field($token),
                sanitize_text_field($fingerprint),
                DateHelper::now()
            )
        );

        if (!$device) {
            return false;
        }

        // Update last_used_at timestamp.
        $this->update((int) $device->id, [
            'last_used_at' => DateHelper::now(),
        ]);

        return true;
    }

    /**
     * Update the last used timestamp for a device.
     *
     * Called when a trusted device is used to bypass 2FA.
     *
     * @since 1.0.0
     *
     * @param int         $deviceId  The device ID.
     * @param string|null $ipAddress Optional new IP address to record.
     *
     * @return bool True on success.
     */
    public function touchLastUsed(int $deviceId, ?string $ipAddress = null): bool
    {
        $data = [
            'last_used_at' => DateHelper::now(),
        ];

        if ($ipAddress !== null) {
            $data['ip_address'] = sanitize_text_field($ipAddress);
        }

        return $this->update($deviceId, $data);
    }

    /**
     * Extend the expiration date for a trusted device.
     *
     * @since 1.0.0
     *
     * @param int    $deviceId  The device ID.
     * @param string $expiresAt New expiration datetime in MySQL format.
     *
     * @return Result Success or failure result.
     */
    public function extendExpiration(int $deviceId, string $expiresAt): Result
    {
        $device = $this->find($deviceId);

        if (!$device) {
            return Result::failure(
                __('Device not found.', 'ultimate-security'),
                'device_not_found'
            );
        }

        if ($device->is_revoked) {
            return Result::failure(
                __('Cannot extend expiration for a revoked device.', 'ultimate-security'),
                'device_revoked'
            );
        }

        $updated = $this->update($deviceId, ['expires_at' => $expiresAt]);

        if (!$updated) {
            return Result::failure(
                __('Failed to extend device expiration.', 'ultimate-security'),
                'update_failed'
            );
        }

        return Result::success([
            'device_id'  => $deviceId,
            'expires_at' => $expiresAt,
            'message'    => __('Device expiration extended.', 'ultimate-security'),
        ]);
    }

    /**
     * Find devices by IP address.
     *
     * Useful for security analysis to see all devices from a specific IP.
     *
     * @since 1.0.0
     *
     * @param string $ipAddress The IP address to search for.
     *
     * @return array<int, object> Array of matching devices.
     */
    public function findByIpAddress(string $ipAddress): array
    {
        if (empty($ipAddress)) {
            return [];
        }

        $results = $this->wpdb->get_results(
            $this->wpdb->prepare(
                "SELECT * FROM {$this->table}
                WHERE ip_address = %s
                ORDER BY created_at DESC",
                sanitize_text_field($ipAddress)
            )
        );

        return $results ?: [];
    }

    /**
     * Check if a user has any active trusted devices.
     *
     * @since 1.0.0
     *
     * @param int $userId The WordPress user ID.
     *
     * @return bool True if user has at least one active trusted device.
     */
    public function userHasActiveDevices(int $userId): bool
    {
        return $this->count([
            'user_id'     => $userId,
            'active_only' => true,
        ]) > 0;
    }

    /**
     * Get the table name.
     *
     * @since 1.0.0
     *
     * @return string The full table name with prefix.
     */
    public function getTableName(): string
    {
        return $this->table;
    }

    /**
     * Sanitize a column name to prevent SQL injection.
     *
     * Only allows alphanumeric characters and underscores.
     *
     * @since 1.0.0
     *
     * @param string $column The column name to sanitize.
     *
     * @return string|null Sanitized column name or null if invalid.
     */
    private function sanitizeColumnName(string $column): ?string
    {
        // List of valid columns for this table.
        $valid_columns = [
            'id', 'user_id', 'device_fingerprint', 'device_token',
            'device_name', 'browser', 'browser_version', 'os', 'os_version',
            'device_type', 'ip_address', 'location', 'last_used_at',
            'expires_at', 'is_revoked', 'created_at', 'updated_at',
        ];

        $column = preg_replace('/[^a-zA-Z0-9_]/', '', $column);

        if (in_array($column, $valid_columns, true)) {
            return $column;
        }

        return null;
    }
}
