<?php

declare(strict_types=1);

require_once __DIR__ . '/Database.php';
require_once __DIR__ . '/MappingEngine.php';

/**
 * SmartApply
 *
 * Portal Automation Layer
 *
 * IMPORTANT:
 * - Current testing portal = UNILAK
 * - Database = SmartApply
 * - Data source = applications database
 * - Mapping source = application_mappings
 *
 * This class DOES NOT create application data.
 * It only reads existing SmartApply data and prepares it
 * for the external UNILAK application portal.
 */
final class RPPortalAutomation
{
    private PDO $pdo;

    private MappingEngine $mappingEngine;

    /**
     * Current portal used for testing.
     */
    private string $portalName = 'UNILAK';

    /**
     * Current UNILAK admission portal.
     */
    private string $portalUrl =
        'http://www.portal.unilak.ac.rw/admission/PGadmission_student.aspx';

    /**
     * HTTP session information.
     */
    private string $cookieFile;

    /**
     * Last downloaded portal HTML.
     */
    private string $lastHtml = '';

    /**
     * Last HTTP status.
     */
    private int $lastHttpStatus = 0;

    /**
     * Last error.
     */
    private ?string $lastError = null;

    public function __construct(
        ?PDO $pdo = null,
        string $portalName = 'UNILAK'
    ) {
        if ($pdo instanceof PDO) {
            $this->pdo = $pdo;
        } else {
            $this->pdo = Database::connection();
        }

        $this->mappingEngine = new MappingEngine($this->pdo);

        $this->portalName = trim($portalName);

        if ($this->portalName === '') {
            $this->portalName = 'UNILAK';
        }

        /**
         * Temporary cookie storage.
         *
         * One file per PHP process/session.
         */
        $this->cookieFile =
            sys_get_temp_dir()
            . DIRECTORY_SEPARATOR
            . 'smartapply_unilak_' . md5((string)microtime(true))
            . '.cookie';
    }

    /**
     * Get portal name.
     */
    public function getPortalName(): string
    {
        return $this->portalName;
    }

    /**
     * Get portal URL.
     */
    public function getPortalUrl(): string
    {
        return $this->portalUrl;
    }

    /**
     * Set portal URL.
     */
    public function setPortalUrl(string $url): self
    {
        $url = trim($url);

        if ($url !== '') {
            $this->portalUrl = $url;
        }

        return $this;
    }

    /**
     * ------------------------------------------------------------
     * LOAD APPLICATION
     * ------------------------------------------------------------
     *
     * Reads one application directly from SmartApply database.
     */
    public function getApplication(int $applicationId): ?array
    {
        if ($applicationId <= 0) {
            throw new InvalidArgumentException(
                'Invalid application ID.'
            );
        }

        /**
         * We intentionally use SELECT * because the SmartApply
         * application structure may contain additional fields.
         */
        $sql = "
            SELECT *
            FROM applications
            WHERE id = :id
            LIMIT 1
        ";

        $stmt = $this->pdo->prepare($sql);

        $stmt->execute([
            ':id' => $applicationId
        ]);

        $application = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$application) {
            return null;
        }

        return $application;
    }

    /**
     * ------------------------------------------------------------
     * LOAD COMPLETE APPLICATION DATA
     * ------------------------------------------------------------
     *
     * Loads the application and related records when available.
     *
     * The main source remains the SmartApply database.
     */
    public function getCompleteApplication(
        int $applicationId
    ): array {
        $application = $this->getApplication($applicationId);

        if (!$application) {
            throw new RuntimeException(
                "Application {$applicationId} was not found."
            );
        }

        $data = $application;

        /**
         * Load education information if the table exists.
         */
        if ($this->tableExists('student_education')) {
            $education = $this->getRowsByApplicationId(
                'student_education',
                $applicationId
            );

            $data['_student_education'] = $education;
        }

        /**
         * Load documents if the table exists.
         */
        if ($this->tableExists('student_documents')) {
            $documents = $this->getRowsByApplicationId(
                'student_documents',
                $applicationId
            );

            $data['_student_documents'] = $documents;
        }

        /**
         * Load payment information if available.
         */
        if ($this->tableExists('service_payments')) {
            $payments = $this->getRowsByApplicationId(
                'service_payments',
                $applicationId
            );

            $data['_service_payments'] = $payments;
        }

        return $data;
    }

    /**
     * ------------------------------------------------------------
     * BUILD MAPPED DATA
     * ------------------------------------------------------------
     *
     * Uses application_mappings through MappingEngine.
     */
    public function buildMappedData(
        int $applicationId
    ): array {
        $application = $this->getCompleteApplication(
            $applicationId
        );

        return $this->mappingEngine->buildPayload(
            $this->portalName,
            $application
        );
    }

    /**
     * ------------------------------------------------------------
     * VALIDATE APPLICATION
     * ------------------------------------------------------------
     */
    public function validateApplication(
        int $applicationId
    ): array {
        $application = $this->getCompleteApplication(
            $applicationId
        );

        return $this->mappingEngine->validate(
            $this->portalName,
            $application
        );
    }

    /**
     * ------------------------------------------------------------
     * PREPARE APPLICATION
     * ------------------------------------------------------------
     *
     * This is the main method used by the test.
     *
     * It does NOT submit anything to UNILAK.
     *
     * It:
     * 1. Reads application from DB
     * 2. Reads mapping rules
     * 3. Creates portal field/value pairs
     * 4. Reads the UNILAK page
     * 5. Detects ASP.NET WebForms hidden fields
     * 6. Returns everything needed for the next automation step.
     */
    public function prepareApplication(
        int $applicationId
    ): array {
        $this->lastError = null;

        $application = $this->getCompleteApplication(
            $applicationId
        );

        if (!$application) {
            throw new RuntimeException(
                "Application {$applicationId} could not be loaded."
            );
        }

        $validationErrors =
            $this->mappingEngine->validate(
                $this->portalName,
                $application
            );

        $mappedData =
            $this->mappingEngine->buildPayload(
                $this->portalName,
                $application
            );

        /**
         * Fetch current UNILAK page.
         */
        $portal = $this->httpGet(
            $this->portalUrl
        );

        $hiddenFields =
            $this->extractHiddenFields(
                $portal['body']
            );

        return [
            'success' => true,

            'application_id' =>
                $applicationId,

            'portal' => [
                'name' =>
                    $this->portalName,

                'url' =>
                    $this->portalUrl,

                'http_status' =>
                    $portal['status']
            ],

            'application' =>
                $application,

            'validation_errors' =>
                $validationErrors,

            'mapped_fields' =>
                $mappedData,

            'hidden_fields' =>
                $hiddenFields,

            'field_count' =>
                count($mappedData),

            'ready_for_automation' =>
                empty($validationErrors)
        ];
    }

    /**
     * ------------------------------------------------------------
     * BUILD FORM DATA
     * ------------------------------------------------------------
     *
     * Converts MappingEngine payload into:
     *
     * portal_field => value
     */
    public function buildFormData(
        int $applicationId
    ): array {
        $mappedData =
            $this->buildMappedData(
                $applicationId
            );

        $formData = [];

        foreach ($mappedData as $mapping) {
            $portalField =
                trim(
                    (string)(
                        $mapping['portal_field'] ?? ''
                    )
                );

            if ($portalField === '') {
                continue;
            }

            $formData[$portalField] =
                $mapping['value'];
        }

        return $formData;
    }

    /**
     * ------------------------------------------------------------
     * BUILD WEBFORMS POST DATA
     * ------------------------------------------------------------
     *
     * ASP.NET WebForms pages require hidden values such as:
     *
     * __VIEWSTATE
     * __VIEWSTATEGENERATOR
     * __EVENTVALIDATION
     * __EVENTTARGET
     * __EVENTARGUMENT
     */
    public function buildWebFormsPostData(
        int $applicationId,
        array $portalHtml = []
    ): array {
        $formData =
            $this->buildFormData(
                $applicationId
            );

        $hiddenFields = $portalHtml;

        /**
         * Hidden fields already present in the page.
         */
        foreach ($hiddenFields as $name => $value) {
            if (!array_key_exists($name, $formData)) {
                $formData[$name] = $value;
            }
        }

        return $formData;
    }

    /**
     * ------------------------------------------------------------
     * EXTRACT HIDDEN FIELDS
     * ------------------------------------------------------------
     */
    public function extractHiddenFields(
        string $html
    ): array {
        $fields = [];

        if (trim($html) === '') {
            return $fields;
        }

        libxml_use_internal_errors(true);

        $dom = new DOMDocument();

        $loaded = $dom->loadHTML(
            $html,
            LIBXML_NOERROR | LIBXML_NOWARNING
        );

        if (!$loaded) {
            libxml_clear_errors();

            return $fields;
        }

        $xpath = new DOMXPath($dom);

        $nodes = $xpath->query(
            '//input[@type="hidden"]'
        );

        if ($nodes !== false) {
            foreach ($nodes as $node) {
                $name = $node->getAttribute('name');

                if ($name === '') {
                    $name = $node->getAttribute('id');
                }

                if ($name === '') {
                    continue;
                }

                $fields[$name] =
                    $node->getAttribute('value');
            }
        }

        libxml_clear_errors();

        return $fields;
    }

    /**
     * ------------------------------------------------------------
     * GET SELECT OPTIONS FROM PORTAL
     * ------------------------------------------------------------
     *
     * This is especially important for the UNILAK fields such as:
     *
     * Country
     * Campus
     * Faculty
     * Programme
     * Option
     * Time Study
     */
    public function extractSelectOptions(
        string $html
    ): array {
        $result = [];

        if (trim($html) === '') {
            return $result;
        }

        libxml_use_internal_errors(true);

        $dom = new DOMDocument();

        $loaded = $dom->loadHTML(
            $html,
            LIBXML_NOERROR | LIBXML_NOWARNING
        );

        if (!$loaded) {
            libxml_clear_errors();

            return $result;
        }

        $xpath = new DOMXPath($dom);

        $selects = $xpath->query('//select');

        if ($selects !== false) {
            foreach ($selects as $select) {

                $name = $select->getAttribute('name');

                if ($name === '') {
                    $name = $select->getAttribute('id');
                }

                if ($name === '') {
                    continue;
                }

                $options = [];

                foreach (
                    $xpath->query(
                        './option',
                        $select
                    ) as $option
                ) {
                    $options[] = [
                        'value' =>
                            $option->getAttribute('value'),

                        'text' =>
                            trim(
                                $option->textContent
                            ),

                        'selected' =>
                            $option->hasAttribute(
                                'selected'
                            )
                    ];
                }

                $result[$name] = $options;
            }
        }

        libxml_clear_errors();

        return $result;
    }

    /**
     * ------------------------------------------------------------
     * FIND PORTAL FIELD
     * ------------------------------------------------------------
     *
     * Searches both name and id.
     */
    public function findPortalField(
        string $html,
        string $fieldName
    ): ?array {
        $fieldName = trim($fieldName);

        if ($fieldName === '') {
            return null;
        }

        libxml_use_internal_errors(true);

        $dom = new DOMDocument();

        $loaded = $dom->loadHTML(
            $html,
            LIBXML_NOERROR | LIBXML_NOWARNING
        );

        if (!$loaded) {
            libxml_clear_errors();

            return null;
        }

        $xpath = new DOMXPath($dom);

        $escaped =
            $this->xpathEscape(
                $fieldName
            );

        $query = sprintf(
            '//*[@name=%s or @id=%s]',
            $escaped,
            $escaped
        );

        $nodes =
            $xpath->query($query);

        if (
            $nodes === false ||
            $nodes->length === 0
        ) {
            libxml_clear_errors();

            return null;
        }

        $node = $nodes->item(0);

        $result = [
            'tag' =>
                strtolower(
                    $node->nodeName
                ),

            'id' =>
                $node->getAttribute('id'),

            'name' =>
                $node->getAttribute('name'),

            'type' =>
                $node->getAttribute('type'),

            'value' =>
                $node->getAttribute('value')
        ];

        libxml_clear_errors();

        return $result;
    }

    /**
     * ------------------------------------------------------------
     * PREPARE FIELD INSTRUCTIONS
     * ------------------------------------------------------------
     *
     * Creates instructions showing exactly:
     *
     * SmartApply DB field
     *       ↓
     * Mapping
     *       ↓
     * UNILAK field
     *       ↓
     * value
     */
    public function buildFormInstructions(
        int $applicationId
    ): array {
        $mappedData =
            $this->buildMappedData(
                $applicationId
            );

        $instructions = [];

        foreach ($mappedData as $mapping) {

            $portalField =
                trim(
                    (string)(
                        $mapping['portal_field']
                        ?? ''
                    )
                );

            if ($portalField === '') {
                continue;
            }

            $instructions[] = [
                'mapping_id' =>
                    (int)(
                        $mapping['mapping_id']
                        ?? 0
                    ),

                'source_field' =>
                    $mapping['source_field']
                    ?? null,

                'portal_field' =>
                    $portalField,

                'portal_field_type' =>
                    $mapping['portal_field_type']
                    ?? 'text',

                'value' =>
                    $mapping['value']
                    ?? null,

                'requires_postback' =>
                    (bool)(
                        $mapping['requires_postback']
                        ?? false
                    ),

                'postback_target' =>
                    $mapping['postback_target']
                    ?? null,

                'postback_argument' =>
                    $mapping['postback_argument']
                    ?? null,

                'mapping_order' =>
                    (int)(
                        $mapping['mapping_order']
                        ?? 0
                    )
            ];
        }

        usort(
            $instructions,
            static function (
                array $a,
                array $b
            ): int {
                return
                    $a['mapping_order']
                    <=>
                    $b['mapping_order'];
            }
        );

        return $instructions;
    }

    /**
     * ------------------------------------------------------------
     * PREPARE POSTBACK
     * ------------------------------------------------------------
     */
    public function preparePostback(
        int $applicationId,
        string $target,
        string $argument = ''
    ): array {
        $instructions =
            $this->buildFormInstructions(
                $applicationId
            );

        $postData = [];

        foreach ($instructions as $instruction) {
            $postData[
                $instruction['portal_field']
            ] =
                $instruction['value'];
        }

        $postData['__EVENTTARGET'] =
            $target;

        $postData['__EVENTARGUMENT'] =
            $argument;

        return $postData;
    }

    /**
     * ------------------------------------------------------------
     * BUILD POSTBACK INSTRUCTIONS
     * ------------------------------------------------------------
     */
    public function buildPostbackInstructions(
        int $applicationId
    ): array {
        $mappedData =
            $this->buildMappedData(
                $applicationId
            );

        $postbacks = [];

        foreach ($mappedData as $mapping) {

            if (
                empty(
                    $mapping['requires_postback']
                )
            ) {
                continue;
            }

            $postbacks[] = [
                'mapping_id' =>
                    (int)(
                        $mapping['mapping_id']
                        ?? 0
                    ),

                'source_field' =>
                    $mapping['source_field']
                    ?? null,

                'portal_field' =>
                    $mapping['portal_field']
                    ?? null,

                'value' =>
                    $mapping['value']
                    ?? null,

                'target' =>
                    $mapping['postback_target']
                    ?? null,

                'argument' =>
                    $mapping['postback_argument']
                    ?? ''
            ];
        }

        return $postbacks;
    }

    /**
     * ------------------------------------------------------------
     * HTTP GET
     * ------------------------------------------------------------
     */
    public function httpGet(
        string $url
    ): array {
        return $this->request(
            'GET',
            $url
        );
    }

    /**
     * ------------------------------------------------------------
     * HTTP POST
     * ------------------------------------------------------------
     */
    public function httpPost(
        string $url,
        array $data
    ): array {
        return $this->request(
            'POST',
            $url,
            $data
        );
    }

    /**
     * ------------------------------------------------------------
     * START SESSION
     * ------------------------------------------------------------
     */
    public function startSession(): array
    {
        return $this->httpGet(
            $this->portalUrl
        );
    }

    /**
     * ------------------------------------------------------------
     * HEALTH CHECK
     * ------------------------------------------------------------
     */
    public function healthCheck(): array
    {
        try {

            $result =
                $this->httpGet(
                    $this->portalUrl
                );

            return [
                'success' =>
                    $result['success'],

                'portal' =>
                    $this->portalName,

                'url' =>
                    $this->portalUrl,

                'http_status' =>
                    $result['status'],

                'reachable' =>
                    $result['success']
                    &&
                    $result['status'] >= 200
                    &&
                    $result['status'] < 400,

                'hidden_fields' =>
                    count(
                        $this->extractHiddenFields(
                            $result['body']
                        )
                    ),

                'select_fields' =>
                    count(
                        $this->extractSelectOptions(
                            $result['body']
                        )
                    )
            ];

        } catch (Throwable $e) {

            return [
                'success' => false,
                'portal' =>
                    $this->portalName,
                'url' =>
                    $this->portalUrl,
                'reachable' =>
                    false,
                'error' =>
                    $e->getMessage()
            ];
        }
    }

    /**
     * ------------------------------------------------------------
     * DIAGNOSTICS
     * ------------------------------------------------------------
     */
    public function diagnostics(
        ?int $applicationId = null
    ): array {
        $result = [
            'success' => true,

            'portal_name' =>
                $this->portalName,

            'portal_url' =>
                $this->portalUrl,

            'pdo' =>
                $this->pdo instanceof PDO,

            'mapping_engine' =>
                $this->mappingEngine instanceof MappingEngine,

            'curl_available' =>
                function_exists('curl_init'),

            'dom_available' =>
                class_exists('DOMDocument'),

            'application' =>
                null,

            'mapping_count' =>
                0,

            'validation_errors' =>
                []
        ];

        if ($applicationId !== null) {

            $application =
                $this->getApplication(
                    $applicationId
                );

            if ($application) {

                $result['application'] = [
                    'id' =>
                        $application['id']
                        ?? $applicationId
                ];

                $mapped =
                    $this->buildMappedData(
                        $applicationId
                    );

                $result['mapping_count'] =
                    count($mapped);

                $result['validation_errors'] =
                    $this->validateApplication(
                        $applicationId
                    );
            }
        }

        return $result;
    }

    /**
     * ------------------------------------------------------------
     * ACTIVITY LOG
     * ------------------------------------------------------------
     *
     * Does not fail automation if the log table is absent.
     */
    public function logActivity(
        ?int $applicationId = null,
        string $action = 'PORTAL_AUTOMATION'
    ): bool {
        try {

            if (
                !$this->tableExists(
                    'application_status_history'
                )
            ) {
                return false;
            }

            /**
             * Detect available columns.
             */
            $columns =
                $this->getTableColumns(
                    'application_status_history'
                );

            $data = [];

            if (
                in_array(
                    'application_id',
                    $columns,
                    true
                )
            ) {
                $data['application_id'] =
                    $applicationId;
            }

            if (
                in_array(
                    'status',
                    $columns,
                    true
                )
            ) {
                $data['status'] =
                    'PROCESSING';
            }

            if (
                in_array(
                    'action',
                    $columns,
                    true
                )
            ) {
                $data['action'] =
                    $action;
            }

            if (
                in_array(
                    'description',
                    $columns,
                    true
                )
            ) {
                $data['description'] =
                    'UNILAK portal automation prepared from SmartApply database.';
            }

            if (!$data) {
                return false;
            }

            $columnNames =
                array_keys($data);

            $placeholders =
                array_map(
                    static fn(string $column): string =>
                        ':' . $column,
                    $columnNames
                );

            $sql = sprintf(
                'INSERT INTO application_status_history (%s) VALUES (%s)',
                implode(', ', $columnNames),
                implode(', ', $placeholders)
            );

            $stmt =
                $this->pdo->prepare($sql);

            foreach ($data as $column => $value) {
                $stmt->bindValue(
                    ':' . $column,
                    $value
                );
            }

            return $stmt->execute();

        } catch (Throwable) {

            /**
             * Logging must never destroy the main process.
             */
            return false;
        }
    }

    /**
     * ------------------------------------------------------------
     * CURL REQUEST
     * ------------------------------------------------------------
     */
    private function request(
        string $method,
        string $url,
        array $data = []
    ): array {
        if (!function_exists('curl_init')) {
            throw new RuntimeException(
                'PHP cURL extension is not enabled.'
            );
        }

        $ch = curl_init();

        if ($ch === false) {
            throw new RuntimeException(
                'Could not initialize cURL.'
            );
        }

        $headers = [
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/151.0.0.0 Safari/537.36',
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language: en-US,en;q=0.9'
        ];

        $options = [
            CURLOPT_URL =>
                $url,

            CURLOPT_RETURNTRANSFER =>
                true,

            CURLOPT_FOLLOWLOCATION =>
                true,

            CURLOPT_MAXREDIRS =>
                10,

            CURLOPT_CONNECTTIMEOUT =>
                15,

            CURLOPT_TIMEOUT =>
                60,

            CURLOPT_COOKIEFILE =>
                $this->cookieFile,

            CURLOPT_COOKIEJAR =>
                $this->cookieFile,

            CURLOPT_HTTPHEADER =>
                $headers,

            CURLOPT_ENCODING =>
                '',

            CURLOPT_SSL_VERIFYPEER =>
                false,

            CURLOPT_SSL_VERIFYHOST =>
                false
        ];

        if (
            strtoupper($method) === 'POST'
        ) {
            $options[CURLOPT_POST] = true;

            $options[CURLOPT_POSTFIELDS] =
                http_build_query(
                    $data,
                    '',
                    '&',
                    PHP_QUERY_RFC3986
                );
        }

        curl_setopt_array(
            $ch,
            $options
        );

        $body =
            curl_exec($ch);

        if ($body === false) {

            $error =
                curl_error($ch);

            curl_close($ch);

            $this->lastError =
                $error;

            throw new RuntimeException(
                'UNILAK HTTP request failed: '
                . $error
            );
        }

        $status =
            (int)curl_getinfo(
                $ch,
                CURLINFO_HTTP_CODE
            );

        $effectiveUrl =
            (string)curl_getinfo(
                $ch,
                CURLINFO_EFFECTIVE_URL
            );

        curl_close($ch);

        $this->lastHtml =
            (string)$body;

        $this->lastHttpStatus =
            $status;

        return [
            'success' =>
                $status >= 200
                &&
                $status < 400,

            'status' =>
                $status,

            'url' =>
                $effectiveUrl,

            'body' =>
                (string)$body
        ];
    }

    /**
     * ------------------------------------------------------------
     * GET ROWS BY APPLICATION ID
     * ------------------------------------------------------------
     */
    private function getRowsByApplicationId(
        string $table,
        int $applicationId
    ): array {
        if (
            !$this->isSafeIdentifier(
                $table
            )
        ) {
            return [];
        }

        $columns =
            $this->getTableColumns(
                $table
            );

        if (!$columns) {
            return [];
        }

        $possibleColumns = [
            'application_id',
            'app_id'
        ];

        $applicationColumn = null;

        foreach (
            $possibleColumns as $candidate
        ) {
            if (
                in_array(
                    $candidate,
                    $columns,
                    true
                )
            ) {
                $applicationColumn =
                    $candidate;

                break;
            }
        }

        if ($applicationColumn === null) {
            return [];
        }

        $sql = sprintf(
            'SELECT * FROM `%s` WHERE `%s` = :application_id ORDER BY id ASC',
            $table,
            $applicationColumn
        );

        $stmt =
            $this->pdo->prepare($sql);

        $stmt->execute([
            ':application_id' =>
                $applicationId
        ]);

        return $stmt->fetchAll(
            PDO::FETCH_ASSOC
        );
    }

    /**
     * ------------------------------------------------------------
     * TABLE EXISTS
     * ------------------------------------------------------------
     */
    private function tableExists(
        string $table
    ): bool {
        if (
            !$this->isSafeIdentifier(
                $table
            )
        ) {
            return false;
        }

        try {

            $stmt =
                $this->pdo->prepare(
                    "
                    SELECT COUNT(*)
                    FROM information_schema.tables
                    WHERE table_schema = DATABASE()
                      AND table_name = :table
                    "
                );

            $stmt->execute([
                ':table' =>
                    $table
            ]);

            return
                (int)$stmt->fetchColumn() > 0;

        } catch (Throwable) {

            return false;
        }
    }

    /**
     * ------------------------------------------------------------
     * TABLE COLUMNS
     * ------------------------------------------------------------
     */
    private function getTableColumns(
        string $table
    ): array {
        if (
            !$this->isSafeIdentifier(
                $table
            )
        ) {
            return [];
        }

        try {

            $stmt =
                $this->pdo->prepare(
                    "
                    SELECT COLUMN_NAME
                    FROM information_schema.columns
                    WHERE table_schema = DATABASE()
                      AND table_name = :table
                    ORDER BY ORDINAL_POSITION
                    "
                );

            $stmt->execute([
                ':table' =>
                    $table
            ]);

            return array_map(
                'strval',
                $stmt->fetchAll(
                    PDO::FETCH_COLUMN
                )
            );

        } catch (Throwable) {

            return [];
        }
    }

    /**
     * ------------------------------------------------------------
     * SAFE IDENTIFIER
     * ------------------------------------------------------------
     */
    private function isSafeIdentifier(
        string $identifier
    ): bool {
        return
            preg_match(
                '/^[A-Za-z0-9_]+$/',
                $identifier
            ) === 1;
    }

    /**
     * ------------------------------------------------------------
     * XPATH ESCAPE
     * ------------------------------------------------------------
     */
    private function xpathEscape(
        string $value
    ): string {
        if (
            strpos(
                $value,
                "'"
            ) === false
        ) {
            return "'" . $value . "'";
        }

        if (
            strpos(
                $value,
                '"'
            ) === false
        ) {
            return '"' . $value . '"';
        }

        $parts =
            explode(
                "'",
                $value
            );

        $result =
            'concat(';

        foreach ($parts as $index => $part) {

            if ($index > 0) {
                $result .= ', "\'", ';
            }

            $result .=
                "'" . $part . "'";
        }

        $result .= ')';

        return $result;
    }

    /**
     * ------------------------------------------------------------
     * LAST HTML
     * ------------------------------------------------------------
     */
    public function getLastHtml(): string
    {
        return $this->lastHtml;
    }

    /**
     * ------------------------------------------------------------
     * LAST HTTP STATUS
     * ------------------------------------------------------------
     */
    public function getLastHttpStatus(): int
    {
        return $this->lastHttpStatus;
    }

    /**
     * ------------------------------------------------------------
     * LAST ERROR
     * ------------------------------------------------------------
     */
    public function getLastError(): ?string
    {
        return $this->lastError;
    }

    /**
     * ------------------------------------------------------------
     * CLEANUP
     * ------------------------------------------------------------
     */
    public function __destruct()
    {
        if (
            $this->cookieFile !== ''
            &&
            is_file($this->cookieFile)
        ) {
            @unlink(
                $this->cookieFile
            );
        }
    }
}