<?php
/**
 * @file qtrDbSnapshot.php
 * @brief Database scan snapshot — tracks remaining tables/rows between cron firings
 *
 * Mirrors CQtrFsSnapShot for the database/transients scanner.
 * State is serialized and stored in wp_options between cron executions,
 * allowing the scan to resume exactly where it left off.
 *
 * Phases:
 *  'tables'     — iterating database tables row by row (LIMIT/OFFSET)
 *  'transients' — iterating wp_options _transient_* entries
 *
 * @author Quttera
 */

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

class CQtrDbSnapshot
{
    /** @var array Internal state persisted between cron firings */
    private $_state;

    public function __construct()
    {
        $this->_state = [
            'tables'        => [],
            'current_table' => null,
            'offset'        => 0,
            'columns'       => [],   // string-type column names for current_table (cached)
            'pk_column'     => null, // primary key column name for current_table (cached)
            'phase'         => 'tables',
        ];
    }

    /**
     * Run SHOW TABLES, populate the tables list, and set current_table to the first entry.
     * Call once before the first cron execution.
     *
     * @return bool TRUE on success, FALSE if no tables found
     */
    public function InitTables(): bool
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $rows = $wpdb->get_results('SHOW TABLES', ARRAY_N);

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

        $tables = [];
        foreach ($rows as $row) {
            $tables[] = $row[0];
        }

        $this->_state['tables']        = $tables;
        $this->_state['current_table'] = array_shift($this->_state['tables']);
        $this->_state['offset']        = 0;
        $this->_state['columns']       = [];
        $this->_state['pk_column']     = null;
        $this->_state['phase']         = 'tables';

        return true;
    }

    /**
     * Return the table currently being scanned, or NULL if all tables are done.
     *
     * @return string|null
     */
    public function CurrentTable(): ?string
    {
        return $this->_state['current_table'];
    }

    /**
     * Return the current LIMIT offset within the current table.
     *
     * @return int
     */
    public function GetOffset(): int
    {
        return (int)$this->_state['offset'];
    }

    /**
     * Advance the LIMIT offset by $by rows.
     *
     * @param int $by Number of rows processed in the last batch
     */
    public function AdvanceOffset(int $by): void
    {
        $this->_state['offset'] += $by;
    }

    /**
     * Return the cached string-type column names for the current table.
     *
     * @return array
     */
    public function GetColumns(): array
    {
        return $this->_state['columns'];
    }

    /**
     * Cache the string-type column names for the current table.
     *
     * @param array $cols Column name strings
     */
    public function SetColumns(array $cols): void
    {
        $this->_state['columns'] = $cols;
    }

    /**
     * Return the cached primary key column name for the current table.
     *
     * @return string|null
     */
    public function GetPkColumn(): ?string
    {
        return $this->_state['pk_column'];
    }

    /**
     * Cache the primary key column name for the current table.
     *
     * @param string|null $col
     */
    public function SetPkColumn(?string $col): void
    {
        $this->_state['pk_column'] = $col;
    }

    /**
     * Mark the current table as done and advance to the next one.
     * Resets offset and clears the column/pk cache for the new table.
     */
    public function NextTable(): void
    {
        if (empty($this->_state['tables'])) {
            $this->_state['current_table'] = null;
        } else {
            $this->_state['current_table'] = array_shift($this->_state['tables']);
        }
        $this->_state['offset']    = 0;
        $this->_state['columns']   = [];
        $this->_state['pk_column'] = null;
    }

    /**
     * Return the number of tables remaining (including current).
     *
     * @return int
     */
    public function TablesRemaining(): int
    {
        $count = count($this->_state['tables']);
        if ($this->_state['current_table'] !== null) {
            $count++;
        }
        return $count;
    }

    /**
     * Return the current scan phase: 'tables' or 'transients'.
     *
     * @return string
     */
    public function GetPhase(): string
    {
        return $this->_state['phase'];
    }

    /**
     * Switch to a new scan phase.
     *
     * @param string $phase 'tables' or 'transients'
     */
    public function SetPhase(string $phase): void
    {
        $this->_state['phase']  = $phase;
        $this->_state['offset'] = 0;
    }

    /**
     * Serialize state to a string for persistence in wp_options.
     *
     * @return string PHP-serialized state
     */
    public function ToString(): string
    {
        return serialize($this->_state);
    }

    /**
     * Restore state from a serialized string previously produced by ToString().
     *
     * @param string $input Serialized state string
     * @return bool TRUE on success, FALSE if deserialization fails
     */
    public function FromString(string $input): bool
    {
        $state = unserialize($input, ['allowed_classes' => false]);

        if (!is_array($state)) {
            return false;
        }

        // Validate required keys to guard against corrupt/stale data
        $required = ['tables', 'current_table', 'offset', 'columns', 'pk_column', 'phase'];
        foreach ($required as $key) {
            if (!array_key_exists($key, $state)) {
                return false;
            }
        }

        $this->_state = $state;
        return true;
    }
}
