<?php
/**
 * Test Mode Simulation Repository
 *
 * Repository pattern implementation for managing test mode simulation logs.
 * This class encapsulates all database operations for the testmode_simulations table,
 * providing a clean API for simulation log management operations.
 *
 * @package     WPUltimateSecurity\UltimateSecurity\Infrastructure\Persistence\Repositories
 * @since       2.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\Exceptions\DatabaseException;
use WPUltimateSecurity\UltimateSecurity\Support\Helpers\DateHelper;

/**
 * Class TestModeSimulationRepository
 *
 * Manages CRUD operations and domain-specific queries for test mode simulations.
 * Test mode allows administrators to preview what actions would be blocked
 * without actually enforcing the rules.
 *
 * Table structure (testmode_simulations):
 * - id: Primary key
 * - module: Security module that triggered the simulation
 * - action: Specific action that was simulated
 * - user_id: WordPress user ID (if authenticated)
 * - user_role: User role at time of simulation
 * - ip_address: Client IP address
 * - request_uri: The requested URI
 * - request_method: HTTP method (GET, POST, etc.)
 * - reason: Explanation of why the action would have been blocked
 * - would_have_blocked: Whether the action would have been blocked
 * - created_at: When the simulation was logged
 * - updated_at: Last update timestamp
 *
 * @since 2.0.0
 */
class TestModeSimulationRepository implements RepositoryInterface
{
    /**
     * Table name (without prefix)
     */
    private const TABLE_NAME = 'ultimate_security_testmode_simulations';

    /**
     * Allowed columns for filtering/ordering (prevents SQL injection)
     */
    private const ALLOWED_COLUMNS = [
        'id',
        'module',
        'action',
        'user_id',
        'user_role',
        'ip_address',
        'request_uri',
        'request_method',
        'reason',
        'would_have_blocked',
        'created_at',
        'updated_at',
    ];

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

    /**
     * Full table name with WordPress prefix.
     *
     * @var string
     */
    private string $table;

    /**
     * Constructor.
     *
     * Initializes the repository with the WordPress database instance.
     */
    public function __construct()
    {
        global $wpdb;
        $this->wpdb = $wpdb;
        $this->table = $wpdb->prefix . self::TABLE_NAME;
    }

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

    /**
     * Find a single record by ID
     *
     * @param int $id Record ID
     * @return object|null Record data 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 record by ID or throw an exception.
     *
     * @since 2.0.0
     *
     * @param int $id Record ID.
     *
     * @return object Record data.
     *
     * @throws \RuntimeException If record not found.
     */
    public function findOrFail(int $id): object
    {
        $result = $this->find($id);

        if ($result === null) {
            throw new \RuntimeException(
                sprintf('Test mode simulation record not found: %d', $id)
            );
        }

        return $result;
    }

    /**
     * Find all records matching criteria
     *
     * @param array $criteria {
     *     @type array  $where    Array of [column => value] for WHERE clause
     *     @type string $orderby  Column name to order by
     *     @type string $order    ASC or DESC
     *     @type int    $limit    Number of records to return
     *     @type int    $offset   Number of records to skip
     * }
     * @return array Array of records
     */
    public function findAll(array $criteria = []): array
    {
        $sql = "SELECT * FROM `{$this->table}`";
        $values = [];

        // Build WHERE clause
        if (!empty($criteria['where']) && is_array($criteria['where'])) {
            $conditions = [];
            foreach ($criteria['where'] as $column => $value) {
                if (in_array($column, self::ALLOWED_COLUMNS, true)) {
                    $conditions[] = "`{$column}` = %s";
                    $values[] = $value;
                }
            }
            if (!empty($conditions)) {
                $sql .= ' WHERE ' . implode(' AND ', $conditions);
            }
        }

        // Build ORDER BY clause
        if (!empty($criteria['orderby']) && in_array($criteria['orderby'], self::ALLOWED_COLUMNS, true)) {
            $order = (!empty($criteria['order']) && strtoupper($criteria['order']) === 'DESC') ? 'DESC' : 'ASC';
            $sql .= " ORDER BY `{$criteria['orderby']}` {$order}";
        } else {
            $sql .= ' ORDER BY `created_at` DESC';
        }

        // Build LIMIT clause
        if (!empty($criteria['limit'])) {
            $offset = $criteria['offset'] ?? 0;
            $sql .= $this->wpdb->prepare(' LIMIT %d OFFSET %d', $criteria['limit'], $offset);
        }

        // Prepare and execute
        if (!empty($values)) {
            $sql = $this->wpdb->prepare($sql, $values);
        }

        return $this->wpdb->get_results($sql, ARRAY_A) ?: [];
    }

    /**
     * Create a new record
     *
     * @param array $data Record data
     * @return int Insert ID or 0 on failure
     *
     * @throws DatabaseException When the insert operation fails.
     */
    public function create(array $data): int
    {
        // Filter to only allowed columns
        $filtered = array_intersect_key($data, array_flip(self::ALLOWED_COLUMNS));

        // Ensure timestamps are set
        if (empty($filtered['created_at'])) {
            $filtered['created_at'] = DateHelper::nowLocal();
        }
        if (empty($filtered['updated_at'])) {
            $filtered['updated_at'] = DateHelper::nowLocal();
        }

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

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

        return $this->wpdb->insert_id;
    }

    /**
     * Update an existing record
     *
     * @param int   $id   Record ID
     * @param array $data Data 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
    {
        // Filter to only allowed columns
        $filtered = array_intersect_key($data, array_flip(self::ALLOWED_COLUMNS));

        // Update timestamp
        $filtered['updated_at'] = DateHelper::nowLocal();

        $result = $this->wpdb->update(
            $this->table,
            $filtered,
            ['id' => $id],
            null,
            ['%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 record
     *
     * @param int $id Record ID
     * @return bool True on success, false on failure
     *
     * @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;
    }

    /**
     * Count records matching criteria
     *
     * @param array $criteria Optional criteria for filtering
     * @return int Number of records
     */
    public function count(array $criteria = []): int
    {
        $sql = "SELECT COUNT(*) FROM `{$this->table}`";
        $values = [];

        if (!empty($criteria['where']) && is_array($criteria['where'])) {
            $conditions = [];
            foreach ($criteria['where'] as $column => $value) {
                if (in_array($column, self::ALLOWED_COLUMNS, true)) {
                    $conditions[] = "`{$column}` = %s";
                    $values[] = $value;
                }
            }
            if (!empty($conditions)) {
                $sql .= ' WHERE ' . implode(' AND ', $conditions);
            }
        }

        if (!empty($values)) {
            $sql = $this->wpdb->prepare($sql, $values);
        }

        return (int) $this->wpdb->get_var($sql);
    }

    // =========================================================================
    // DOMAIN-SPECIFIC METHODS
    // =========================================================================

    /**
     * Find simulations by module
     *
     * @param string $module Module name
     * @param int    $limit  Maximum number of records
     * @return array Array of simulation records
     */
    public function findByModule(string $module, int $limit = 100): array
    {
        return $this->findAll([
            'where' => ['module' => $module],
            'orderby' => 'created_at',
            'order' => 'DESC',
            'limit' => $limit,
        ]);
    }

    /**
     * Find simulations by action
     *
     * @param string $action Action name
     * @param int    $limit  Maximum number of records
     * @return array Array of simulation records
     */
    public function findByAction(string $action, int $limit = 100): array
    {
        return $this->findAll([
            'where' => ['action' => $action],
            'orderby' => 'created_at',
            'order' => 'DESC',
            'limit' => $limit,
        ]);
    }

    /**
     * Find simulations by user ID
     *
     * @param int $userId User ID
     * @param int $limit  Maximum number of records
     * @return array Array of simulation records
     */
    public function findByUser(int $userId, int $limit = 100): array
    {
        return $this->findAll([
            'where' => ['user_id' => $userId],
            'orderby' => 'created_at',
            'order' => 'DESC',
            'limit' => $limit,
        ]);
    }

    /**
     * Find simulations by IP address
     *
     * @param string $ip    IP address
     * @param int    $limit Maximum number of records
     * @return array Array of simulation records
     */
    public function findByIp(string $ip, int $limit = 100): array
    {
        return $this->findAll([
            'where' => ['ip_address' => $ip],
            'orderby' => 'created_at',
            'order' => 'DESC',
            'limit' => $limit,
        ]);
    }

    /**
     * Find simulations that would have blocked
     *
     * @param int $limit Maximum number of records
     * @return array Array of simulation records
     */
    public function findWouldHaveBlocked(int $limit = 100): array
    {
        return $this->findAll([
            'where' => ['would_have_blocked' => 1],
            'orderby' => 'created_at',
            'order' => 'DESC',
            'limit' => $limit,
        ]);
    }

    /**
     * Find simulations within a date range
     *
     * @param string $start Start datetime (Y-m-d H:i:s)
     * @param string $end   End datetime (Y-m-d H:i:s)
     * @param int    $limit Maximum number of records
     * @return array Array of simulation records
     */
    public function findByDateRange(string $start, string $end, int $limit = 1000): array
    {
        return $this->wpdb->get_results(
            $this->wpdb->prepare(
                "SELECT * FROM `{$this->table}`
                WHERE created_at BETWEEN %s AND %s
                ORDER BY created_at DESC
                LIMIT %d",
                $start,
                $end,
                $limit
            ),
            ARRAY_A
        ) ?: [];
    }

    /**
     * Get statistics for a date range
     *
     * @param string $start Start datetime
     * @param string $end   End datetime
     * @return array Statistics array
     */
    public function getStatistics(string $start, string $end): array
    {
        // Total count
        $total = $this->wpdb->get_var($this->wpdb->prepare(
            "SELECT COUNT(*) FROM `{$this->table}` WHERE created_at BETWEEN %s AND %s",
            $start,
            $end
        ));

        // Count by module
        $byModule = $this->wpdb->get_results($this->wpdb->prepare(
            "SELECT module, COUNT(*) as count FROM `{$this->table}`
            WHERE created_at BETWEEN %s AND %s
            GROUP BY module
            ORDER BY count DESC",
            $start,
            $end
        ), ARRAY_A);

        // Count by action
        $byAction = $this->wpdb->get_results($this->wpdb->prepare(
            "SELECT action, COUNT(*) as count FROM `{$this->table}`
            WHERE created_at BETWEEN %s AND %s
            GROUP BY action
            ORDER BY count DESC
            LIMIT 20",
            $start,
            $end
        ), ARRAY_A);

        // Would have blocked count
        $wouldHaveBlocked = $this->wpdb->get_var($this->wpdb->prepare(
            "SELECT COUNT(*) FROM `{$this->table}`
            WHERE created_at BETWEEN %s AND %s AND would_have_blocked = 1",
            $start,
            $end
        ));

        // Unique IPs
        $uniqueIps = $this->wpdb->get_var($this->wpdb->prepare(
            "SELECT COUNT(DISTINCT ip_address) FROM `{$this->table}`
            WHERE created_at BETWEEN %s AND %s",
            $start,
            $end
        ));

        // Unique users
        $uniqueUsers = $this->wpdb->get_var($this->wpdb->prepare(
            "SELECT COUNT(DISTINCT user_id) FROM `{$this->table}`
            WHERE created_at BETWEEN %s AND %s AND user_id IS NOT NULL",
            $start,
            $end
        ));

        return [
            'total' => (int) $total,
            'by_module' => $byModule ?: [],
            'by_action' => $byAction ?: [],
            'would_have_blocked' => (int) $wouldHaveBlocked,
            'unique_ips' => (int) $uniqueIps,
            'unique_users' => (int) $uniqueUsers,
        ];
    }

    /**
     * Log a simulation event
     *
     * @param string      $module           Security module
     * @param string      $action           Action performed
     * @param bool        $wouldHaveBlocked Whether it would have been blocked
     * @param string|null $reason           Reason for the action
     * @param array       $context          Additional context data
     * @return int Insert ID
     */
    public function logSimulation(
        string $module,
        string $action,
        bool $wouldHaveBlocked,
        ?string $reason = null,
        array $context = []
    ): int {
        $currentUser = wp_get_current_user();

        return $this->create([
            'module' => $module,
            'action' => $action,
            'user_id' => $currentUser->ID > 0 ? $currentUser->ID : null,
            'user_role' => $currentUser->ID > 0 ? implode(',', $currentUser->roles) : null,
            'ip_address' => $context['ip_address'] ?? $this->getClientIp(),
            'request_uri' => $context['request_uri'] ?? ($_SERVER['REQUEST_URI'] ?? ''),
            'request_method' => $context['request_method'] ?? ($_SERVER['REQUEST_METHOD'] ?? ''),
            'reason' => $reason,
            'would_have_blocked' => $wouldHaveBlocked ? 1 : 0,
        ]);
    }

    /**
     * Get client IP address
     *
     * @return string
     */
    private function getClientIp(): string
    {
        $headers = [
            'HTTP_CF_CONNECTING_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'REMOTE_ADDR',
        ];

        foreach ($headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ip = $_SERVER[$header];
                if (strpos($ip, ',') !== false) {
                    $ip = trim(explode(',', $ip)[0]);
                }
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }

        return '0.0.0.0';
    }

    /**
     * Delete simulations older than a specified date
     *
     * @param string $beforeDate Delete records before this date (Y-m-d H:i:s)
     * @return int Number of deleted records
     */
    public function deleteOlderThan(string $beforeDate): int
    {
        $result = $this->wpdb->query($this->wpdb->prepare(
            "DELETE FROM `{$this->table}` WHERE created_at < %s",
            $beforeDate
        ));

        return $result !== false ? $result : 0;
    }

    /**
     * Truncate the entire table
     *
     * Use with caution!
     *
     * @return bool True on success
     */
    public function truncate(): bool
    {
        return $this->wpdb->query("TRUNCATE TABLE `{$this->table}`") !== false;
    }
}
