<?php
/**
 * Profit Warden — State Manager & Persistence Layer
 *
 * Task 1.1.11 (Orchestratore: Gestione Stato e Persistenza)
 *
 * Fornisce al plugin un livello di state management persistente per:
 *  - stato dell'orchestratore / task in esecuzione (running/completed/failed/pending)
 *  - flag di completamento e progress (0-100)
 *  - dati intermedi arbitrari (payload JSON)
 *  - timestamp (created/started/finished/updated)
 *  - errori (last_error + error_count) e retry
 *  - lock con scadenza (TTL) per protezione race-condition / doppie esecuzioni
 *
 * Storage:
 *  - Primario : SQLite via PDO (pdo_sqlite) — consigliato per plugin WooCommerce
 *               standalone, nessuna dipendenza esterna, già utilizzato dal
 *               backend del progetto (v. changelog 1.5.0 "SQLite update queries").
 *  - Fallback : WordPress options (get_option/update_option) per hosting
 *               senza estensione PDO SQLite. Trasparente per il chiamante.
 *
 * Convenzioni: prefisso `profwabr_` conforme al codebase; wrapper procedurali
 * intorno alla classe singleton `Profwabr_State_Manager`.
 *
 * API pubblica (per l'orchestratore centrale):
 *   profwabr_state_init()
 *   profwabr_state_install()
 *   profwabr_state_set($task, $data)
 *   profwabr_state_get($task)
 *   profwabr_state_update($task, $data)
 *   profwabr_state_delete($task)
 *   profwabr_state_start_task($task, $data = array())
 *   profwabr_state_complete_task($task, $result = null)
 *   profwabr_state_fail_task($task, $error, $data = null)
 *   profwabr_state_set_progress($task, $progress)
 *   profwabr_state_acquire_lock($task, $ttl = 300)
 *   profwabr_state_release_lock($task, $token)
 *   profwabr_state_is_locked($task)
 *   profwabr_state_get_all($status = null, $limit = 100)
 *   profwabr_state_cleanup($older_than = null)
 *
 * Hook emessi:
 *   do_action('profwabr_state_changed',  $task, $action, $data)
 *   do_action('profwabr_state_completed', $task, $result)
 *   do_action('profwabr_state_failed',    $task, $error)
 *
 * @package ProfitWarden
 * @since   3.2.0
 */

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

if (!defined('PROFWABR_STATE_DB_VERSION')) {
    define('PROFWABR_STATE_DB_VERSION', '1.0.0');
}

if (!class_exists('Profwabr_State_Manager')) :

/**
 * Classe singleton di state management con storage SQLite/options.
 */
class Profwabr_State_Manager
{
    /**
     * Istanza singleton.
     *
     * @var Profwabr_State_Manager|null
     */
    private static $instance = null;

    /**
     * PDO handler SQLite (null se storage fallback attivo).
     *
     * @var PDO|null
     */
    private $pdo = null;

    /**
     * True se lo storage SQLite è disponibile e inizializzato.
     *
     * @var bool
     */
    private $sqlite_ready = false;

    /**
     * Percorso assoluto del database SQLite.
     *
     * @var string
     */
    private $db_path = '';

    /**
     * Flag "tentativo fallback già loggato" per evitare spam di notice.
     *
     * @var bool
     */
    private $fallback_logged = false;

    /**
     * Costruttore privato (singleton).
     */
    private function __construct()
    {
        $this->db_path = $this->resolve_db_path();
    }

    /**
     * Recupera l'istanza singleton.
     *
     * @return Profwabr_State_Manager
     */
    public static function instance()
    {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Risolve il percorso del file SQLite in wp-content/uploads/profit-warden/.
     *
     * @return string
     */
    private function resolve_db_path()
    {
        $uploads = wp_upload_dir();
        $base    = trailingslashit($uploads['basedir']) . 'profit-warden';
        
        if (!is_dir($base)) {
            wp_mkdir_p($base);
        }
        
        // Ensure protections are always present
        if (!file_exists($base . '/index.php')) {
            @file_put_contents($base . '/index.php', "<?php
// Silence is golden.
");
        }
        if (!file_exists($base . '/.htaccess')) {
            @file_put_contents(
                $base . '/.htaccess',
                "# Block direct access
<FilesMatch \"\.(db|sqlite|sqlite3|php|txt)$\">
  Require all denied
  Order allow,deny
  Deny from all
</FilesMatch>
"
            );
        }
        
        // Use a randomized DB name to protect against direct Nginx access where .htaccess is ignored
        $db_name = get_option('profit_warden_sqlite_db_name');
        if (empty($db_name)) {
            $db_name = 'profit-warden-state-' . wp_generate_password(24, false) . '.sqlite';
            update_option('profit_warden_sqlite_db_name', $db_name);
        }
        
        $new_db_path = trailingslashit($base) . $db_name;
        $legacy_db_path = trailingslashit($base) . 'profit-warden-state.sqlite';
        
        // Migrate legacy db if it exists
        if (file_exists($legacy_db_path) && !file_exists($new_db_path)) {
            @rename($legacy_db_path, $new_db_path);
        }
        
        return $new_db_path;
    }

    /**
     * Inizializza il layer: seleziona storage e crea lo schema se necessario.
     * Idempotente; sicuro chiamarlo più volte.
     *
     * @return bool True se il layer è pronto.
     */
    public function init()
    {
        if (extension_loaded('pdo_sqlite')) {
            try {
                $this->connect_sqlite();
                $this->install_sqlite();
                $this->sqlite_ready = true;
            } catch (\Throwable $e) {
                $this->sqlite_ready = false;
                $this->log_fallback($e);
            }
        } else {
            $this->log_fallback(null);
        }

        if (!$this->sqlite_ready) {
            // Storage fallback: assicura che l'opzione di versione esista.
            if (!get_option('profwabr_state_db_version')) {
                update_option('profwabr_state_db_version', PROFWABR_STATE_DB_VERSION, false);
            }
        }

        // Pulizia periodica leggera (registrata solo quando WP è pronto).
        if (function_exists('wp_next_scheduled') && !wp_next_scheduled('profwabr_state_cleanup_event')) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', 'profwabr_state_cleanup_event');
        }

        return $this->ready();
    }

    /**
     * True se almeno uno storage è utilizzabile.
     *
     * @return bool
     */
    public function ready()
    {
        return true; // SQLite oppure fallback options: sempre disponibile.
    }

    /**
     * True se lo storage attivo è SQLite.
     *
     * @return bool
     */
    public function using_sqlite()
    {
        return $this->sqlite_ready;
    }

    /**
     * Percorso del DB SQLite (utile per debug/report).
     *
     * @return string
     */
    public function get_db_path()
    {
        return $this->db_path;
    }

    /**
     * Logga (una sola volta) l'uso del fallback.
     *
     * @param \Throwable|null $e Eccezione che ha causato il fallback, se presente.
     * @return void
     */
    private function log_fallback($e)
    {
        if ($this->fallback_logged) {
            return;
        }
        $this->fallback_logged = true;
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('Profit Warden State: PDO SQLite non disponibile, uso storage fallback (WP options). ' . ($e ? $e->getMessage() : 'estensione non caricata'));
        }
    }

    /**
     * Apre la connessione PDO a SQLite con WAL e busy timeout.
     *
     * @throws RuntimeException Se la connessione fallisce.
     * @return void
     */
    private function connect_sqlite()
    {
        if (null !== $this->pdo) {
            return;
        }
        $dsn = 'sqlite:' . $this->db_path;
        $this->pdo = new PDO($dsn, null, null, array(
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_TIMEOUT            => 5,
        ));
        // WAL: migliore concorrenza lettura/scrittura; busy_timeout: evita "database is locked".
        $this->pdo->exec('PRAGMA journal_mode = WAL;');
        $this->pdo->exec('PRAGMA busy_timeout = 5000;');
        $this->pdo->exec('PRAGMA synchronous = NORMAL;');
    }

    /**
     * Crea lo schema SQLite se non esiste.
     *
     * @return void
     */
    private function install_sqlite()
    {
        $this->pdo->exec(
            "CREATE TABLE IF NOT EXISTS profwabr_state (
                task_name       TEXT PRIMARY KEY,
                status          TEXT NOT NULL DEFAULT 'pending',
                payload         TEXT,
                progress        INTEGER NOT NULL DEFAULT 0,
                created_at      TEXT NOT NULL,
                started_at      TEXT,
                finished_at     TEXT,
                updated_at      TEXT NOT NULL,
                last_error      TEXT,
                error_count     INTEGER NOT NULL DEFAULT 0,
                lock_token      TEXT,
                lock_expires_at TEXT
            );"
        );
        $this->pdo->exec(
            'CREATE INDEX IF NOT EXISTS idx_profwabr_state_status ON profwabr_state(status);'
        );
        $this->pdo->exec(
            'CREATE INDEX IF NOT EXISTS idx_profwabr_state_updated ON profwabr_state(updated_at);'
        );
    }

    /**
     * Timestamp corrente in formato mysql (UTC).
     *
     * @return string
     */
    private function now()
    {
        return gmdate('Y-m-d H:i:s');
    }

    /**
     * Decodifica un payload JSON con fallback sicuro.
     *
     * @param string|null $json
     * @return array
     */
    private function decode_payload($json)
    {
        if (empty($json)) {
            return array();
        }
        $data = json_decode($json, true);
        return is_array($data) ? $data : array();
    }

    /**
     * Normalizza un record in array associativo completo.
     *
     * @param array $row
     * @return array
     */
    private function normalize_row($row)
    {
        if (!is_array($row) || empty($row)) {
            return null;
        }
        $row['payload'] = $this->decode_payload(isset($row['payload']) ? $row['payload'] : null);
        return $row;
    }

    /* ---------------------------------------------------------------------
     * CORE CRUD — SQLite
     * ------------------------------------------------------------------- */

    /**
     * Set/replace completo dello stato di un task (SQLite).
     *
     * @param string $task_name
     * @param array  $data
     * @return bool
     */
    public function set($task_name, $data = array())
    {
        $task_name = sanitize_key((string) $task_name);
        if ('' === $task_name) {
            return false;
        }
        $now      = $this->now();
        $status   = isset($data['status']) ? sanitize_key((string) $data['status']) : 'pending';
        $progress = isset($data['progress']) ? max(0, min(100, (int) $data['progress'])) : 0;
        $payload  = isset($data['payload']) ? wp_json_encode($data['payload']) : null;
        $created  = isset($data['created_at']) ? sanitize_text_field($data['created_at']) : $now;
        $started  = isset($data['started_at']) ? sanitize_text_field($data['started_at']) : null;
        $finished = isset($data['finished_at']) ? sanitize_text_field($data['finished_at']) : null;
        $error    = isset($data['last_error']) ? sanitize_textarea_field((string) $data['last_error']) : null;
        $errors   = isset($data['error_count']) ? max(0, (int) $data['error_count']) : 0;

        try {
            $sql = "INSERT INTO profwabr_state
                    (task_name, status, payload, progress, created_at, started_at,
                     finished_at, updated_at, last_error, error_count, lock_token, lock_expires_at)
                    VALUES (:name, :status, :payload, :progress, :created, :started,
                            :finished, :updated, :error, :errors, :lock_token, :lock_expires)
                    ON CONFLICT(task_name) DO UPDATE SET
                        status = excluded.status,
                        payload = excluded.payload,
                        progress = excluded.progress,
                        started_at = COALESCE(excluded.started_at, profwabr_state.started_at),
                        finished_at = COALESCE(excluded.finished_at, profwabr_state.finished_at),
                        updated_at = excluded.updated_at,
                        last_error = excluded.last_error,
                        error_count = excluded.error_count,
                        lock_token = COALESCE(excluded.lock_token, profwabr_state.lock_token),
                        lock_expires_at = COALESCE(excluded.lock_expires_at, profwabr_state.lock_expires_at)";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute(array(
                ':name'          => $task_name,
                ':status'        => $status,
                ':payload'       => $payload,
                ':progress'      => $progress,
                ':created'       => $created,
                ':started'       => $started,
                ':finished'      => $finished,
                ':updated'       => $now,
                ':error'         => $error,
                ':errors'        => $errors,
                ':lock_token'    => null,
                ':lock_expires'  => null,
            ));
            $this->after_change($task_name, 'set', $data);
            return true;
        } catch (\Throwable $e) {
            $this->handle_error('set', $e);
            return false;
        }
    }

    /**
     * Get dello stato di un task (SQLite).
     *
     * @param string $task_name
     * @return array|null
     */
    public function get($task_name)
    {
        $task_name = sanitize_key((string) $task_name);
        if ('' === $task_name) {
            return null;
        }
        try {
            $stmt = $this->pdo->prepare('SELECT * FROM profwabr_state WHERE task_name = :name LIMIT 1');
            $stmt->execute(array(':name' => $task_name));
            $row = $stmt->fetch();
            return $this->normalize_row($row);
        } catch (\Throwable $e) {
            $this->handle_error('get', $e);
            return null;
        }
    }

    /**
     * Merge parziale dello stato di un task (SQLite).
     *
     * @param string $task_name
     * @param array  $data
     * @return bool
     */
    public function update($task_name, $data = array())
    {
        $current = $this->get($task_name);
        if (null === $current) {
            return $this->set($task_name, $data);
        }
        $merged = array_merge($current, $data);
        if (isset($merged['payload']) && is_array($merged['payload'])) {
            // Merge ricorsivo sul payload.
            $base_payload = isset($data['payload']) && is_array($data['payload']) ? $data['payload'] : array();
            $merged['payload'] = array_merge($current['payload'], $base_payload);
        }
        return $this->set($task_name, $merged);
    }

    /**
     * Delete dello stato di un task (SQLite).
     *
     * @param string $task_name
     * @return bool
     */
    public function delete($task_name)
    {
        $task_name = sanitize_key((string) $task_name);
        if ('' === $task_name) {
            return false;
        }
        try {
            $stmt = $this->pdo->prepare('DELETE FROM profwabr_state WHERE task_name = :name');
            $stmt->execute(array(':name' => $task_name));
            $this->after_change($task_name, 'delete', null);
            return true;
        } catch (\Throwable $e) {
            $this->handle_error('delete', $e);
            return false;
        }
    }

    /* ---------------------------------------------------------------------
     * CICLO DI VITA TASK
     * ------------------------------------------------------------------- */

    /**
     * Marca un task come "running" (con avvio timestamp e lock).
     *
     * @param string $task_name
     * @param array  $data       Dati iniziali opzionali.
     * @param int    $lock_ttl   Durata lock in secondi (default 300).
     * @return bool
     */
    public function start_task($task_name, $data = array(), $lock_ttl = 300)
    {
        if (!$this->acquire_lock($task_name, $lock_ttl)) {
            return false;
        }
        $task_data = array_merge($data, array(
            'status'      => 'running',
            'started_at'  => $this->now(),
            'finished_at' => null,
            'last_error'  => null,
        ));
        $ok = $this->set($task_name, $task_data);
        if (!$ok) {
            $this->release_lock($task_name, null);
        }
        return $ok;
    }

    /**
     * Marca un task come "completed".
     *
     * @param string $task_name
     * @param mixed  $result    Risultato (salvato in payload).
     * @return bool
     */
    public function complete_task($task_name, $result = null)
    {
        $data = array(
            'status'      => 'completed',
            'finished_at' => $this->now(),
            'progress'    => 100,
            'last_error'  => null,
        );
        if (null !== $result) {
            $current = $this->get($task_name);
            $payload = is_array($current) && is_array($current['payload']) ? $current['payload'] : array();
            $payload['result'] = $result;
            $data['payload'] = $payload;
        }
        $ok = $this->update($task_name, $data);
        $this->release_lock($task_name, null);
        if ($ok) {
            do_action('profwabr_state_completed', $task_name, $result);
        }
        return $ok;
    }

    /**
     * Marca un task come "failed" con messaggio di errore e contatore.
     *
     * @param string $task_name
     * @param string $error
     * @param array  $data       Dati extra opzionali (es. per retry).
     * @return bool
     */
    public function fail_task($task_name, $error, $data = array())
    {
        $current = $this->get($task_name);
        $errors  = (is_array($current) && isset($current['error_count'])) ? (int) $current['error_count'] + 1 : 1;
        $task_data = array_merge($data, array(
            'status'      => 'failed',
            'finished_at' => $this->now(),
            'last_error'  => sanitize_textarea_field((string) $error),
            'error_count' => $errors,
        ));
        $ok = $this->update($task_name, $task_data);
        $this->release_lock($task_name, null);
        if ($ok) {
            do_action('profwabr_state_failed', $task_name, $error);
        }
        return $ok;
    }

    /**
     * Aggiorna il progresso (0-100) di un task.
     *
     * @param string $task_name
     * @param int    $progress
     * @return bool
     */
    public function set_progress($task_name, $progress)
    {
        return $this->update($task_name, array(
            'progress' => max(0, min(100, (int) $progress)),
        ));
    }

    /* ---------------------------------------------------------------------
     * LOCK / RACE-CONDITION PROTECTION
     * ------------------------------------------------------------------- */

    /**
     * Acquisisce un lock atomico su un task (SQLite, transazione + UPDATE condizionale).
     *
     * @param string $task_name
     * @param int    $ttl        Secondi di validità del lock.
     * @return string|false Token del lock, false se già lockato da altri.
     */
    public function acquire_lock($task_name, $ttl = 300)
    {
        $task_name = sanitize_key((string) $task_name);
        if ('' === $task_name) {
            return false;
        }
        $ttl      = max(1, (int) $ttl);
        $token    = wp_generate_uuid4();
        $expires  = gmdate('Y-m-d H:i:s', time() + $ttl);
        $now      = $this->now();

        try {
            $this->pdo->beginTransaction();
            // Garantisce l'esistenza della riga (INSERT OR IGNORE è atomico e senza race).
            $this->pdo->exec(
                "INSERT OR IGNORE INTO profwabr_state (task_name, status, created_at, updated_at)
                 VALUES ('" . $task_name . "', 'pending', '" . $now . "', '" . $now . "')"
            );
            // Acquisizione condizionale: solo se lock libero o scaduto.
            $stmt = $this->pdo->prepare(
                "UPDATE profwabr_state
                    SET lock_token = :token, lock_expires_at = :expires, updated_at = :now
                  WHERE task_name = :name
                    AND (lock_token IS NULL OR lock_expires_at IS NULL OR lock_expires_at < :now)"
            );
            $stmt->execute(array(
                ':token'   => $token,
                ':expires' => $expires,
                ':now'     => $now,
                ':name'    => $task_name,
            ));
            $acquired = (1 === (int) $stmt->rowCount());
            $this->pdo->commit();
            return $acquired ? $token : false;
        } catch (\Throwable $e) {
            if ($this->pdo && $this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            $this->handle_error('acquire_lock', $e);
            return false;
        }
    }

    /**
     * Rilascia il lock (solo se il token corrisponde, oppure forzato).
     *
     * @param string      $task_name
     * @param string|null $token     Token posseduto; null forza il rilascio.
     * @return bool
     */
    public function release_lock($task_name, $token = null)
    {
        $task_name = sanitize_key((string) $task_name);
        if ('' === $task_name) {
            return false;
        }
        try {
            if (null === $token) {
                $stmt = $this->pdo->prepare(
                    "UPDATE profwabr_state SET lock_token = NULL, lock_expires_at = NULL, updated_at = :now
                      WHERE task_name = :name"
                );
                $stmt->execute(array(':now' => $this->now(), ':name' => $task_name));
            } else {
                $stmt = $this->pdo->prepare(
                    "UPDATE profwabr_state SET lock_token = NULL, lock_expires_at = NULL, updated_at = :now
                      WHERE task_name = :name AND lock_token = :token"
                );
                $stmt->execute(array(':now' => $this->now(), ':name' => $task_name, ':token' => $token));
            }
            return true;
        } catch (\Throwable $e) {
            $this->handle_error('release_lock', $e);
            return false;
        }
    }

    /**
     * Verifica se un task è attualmente lockato (lock non scaduto).
     *
     * @param string $task_name
     * @return bool
     */
    public function is_locked($task_name)
    {
        $current = $this->get($task_name);
        if (!is_array($current) || empty($current['lock_token'])) {
            return false;
        }
        if (!empty($current['lock_expires_at']) && $current['lock_expires_at'] <= $this->now()) {
            return false; // scaduto
        }
        return true;
    }

    /* ---------------------------------------------------------------------
     * QUERY AGGREGATE E MANUTENZIONE
     * ------------------------------------------------------------------- */

    /**
     * Elenca task per stato (opzionale), ordinati per ultimo aggiornamento.
     *
     * @param string|null $status
     * @param int         $limit
     * @return array
     */
    public function get_all($status = null, $limit = 100)
    {
        $limit = max(1, min(500, (int) $limit));
        try {
            if (null !== $status) {
                $stmt = $this->pdo->prepare(
                    'SELECT * FROM profwabr_state WHERE status = :status ORDER BY updated_at DESC LIMIT ' . $limit
                );
                $stmt->execute(array(':status' => sanitize_key((string) $status)));
            } else {
                $stmt = $this->pdo->query('SELECT * FROM profwabr_state ORDER BY updated_at DESC LIMIT ' . $limit);
            }
            $rows = $stmt->fetchAll();
            $out  = array();
            foreach ($rows as $row) {
                $out[] = $this->normalize_row($row);
            }
            return $out;
        } catch (\Throwable $e) {
            $this->handle_error('get_all', $e);
            return array();
        }
    }

    /**
     * Conta i task per stato.
     *
     * @param string|null $status
     * @return int
     */
    public function count($status = null)
    {
        try {
            if (null !== $status) {
                $stmt = $this->pdo->prepare('SELECT COUNT(*) AS c FROM profwabr_state WHERE status = :status');
                $stmt->execute(array(':status' => sanitize_key((string) $status)));
            } else {
                $stmt = $this->pdo->query('SELECT COUNT(*) AS c FROM profwabr_state');
            }
            $row = $stmt->fetch();
            return (int) (is_array($row) && isset($row['c']) ? $row['c'] : 0);
        } catch (\Throwable $e) {
            $this->handle_error('count', $e);
            return 0;
        }
    }

    /**
     * Pulizia task vecchi (es. completati da più di N giorni).
     *
     * @param string|null $older_than Data limite 'Y-m-d H:i:s'; default -30 giorni.
     * @return int Numero di record eliminati.
     */
    public function cleanup($older_than = null)
    {
        if (null === $older_than) {
            $older_than = gmdate('Y-m-d H:i:s', strtotime('-30 days'));
        }
        try {
            $stmt = $this->pdo->prepare(
                "DELETE FROM profwabr_state
                  WHERE updated_at < :older
                    AND status IN ('completed', 'failed')"
            );
            $stmt->execute(array(':older' => $older_than));
            return (int) $stmt->rowCount();
        } catch (\Throwable $e) {
            $this->handle_error('cleanup', $e);
            return 0;
        }
    }

    /**
     * Azzera completamente lo state store (utility per debug/reset orchestratore).
     *
     * @return bool
     */
    public function reset_all()
    {
        try {
            $this->pdo->exec('DELETE FROM profwabr_state;');
            do_action('profwabr_state_changed', '*', 'reset', null);
            return true;
        } catch (\Throwable $e) {
            $this->handle_error('reset_all', $e);
            return false;
        }
    }

    /* ---------------------------------------------------------------------
     * INTERNAL HELPERS
     * ------------------------------------------------------------------- */

    /**
     * Emette l'hook di notifica dopo ogni modifica.
     *
     * @param string $task_name
     * @param string $action
     * @param mixed  $data
     * @return void
     */
    private function after_change($task_name, $action, $data)
    {
        do_action('profwabr_state_changed', $task_name, $action, $data);
    }

    /**
     * Gestione centralizzata errori PDO: log + azione hook.
     *
     * @param string         $context
     * @param \Throwable     $e
     * @return void
     */
    private function handle_error($context, $e)
    {
        $message = 'Profit Warden State [' . $context . ']: ' . $e->getMessage();
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log($message);
        }
        do_action('profwabr_state_error', $context, $e->getMessage());
    }
}

endif; // class_exists

/* =====================================================================
 * WRAPPER PROCEDURALI (convenzione codebase: prefisso profwabr_)
 * Gestiscono in modo trasparente anche lo storage fallback su options WP.
 * =================================================================== */

if (!function_exists('profwabr_state_init')) :
/**
 * Inizializza il layer di state management (hook su 'init' a bassa priorità).
 *
 * @return bool
 */
function profwabr_state_init()
{
    return Profwabr_State_Manager::instance()->init();
}
endif;

if (!function_exists('profwabr_state_install')) :
/**
 * Forza la creazione/verifica dello schema (usata all'attivazione del plugin).
 *
 * @return bool
 */
function profwabr_state_install()
{
    return Profwabr_State_Manager::instance()->init();
}
endif;

if (!function_exists('profwabr_state_using_sqlite')) :
/**
 * True se lo storage attivo è SQLite.
 *
 * @return bool
 */
function profwabr_state_using_sqlite()
{
    return Profwabr_State_Manager::instance()->using_sqlite();
}
endif;

if (!function_exists('profwabr_state_db_path')) :
/**
 * Percorso del DB SQLite (o stringa vuota se fallback).
 *
 * @return string
 */
function profwabr_state_db_path()
{
    return Profwabr_State_Manager::instance()->get_db_path();
}
endif;

if (!function_exists('profwabr_state_set')) :
/**
 * Set/replace dello stato di un task.
 *
 * @param string $task_name
 * @param array  $data
 * @return bool
 */
function profwabr_state_set($task_name, $data = array())
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->set($task_name, $data);
    }
    return profwabr_state_fallback_set($task_name, $data);
}
endif;

if (!function_exists('profwabr_state_get')) :
/**
 * Get dello stato di un task.
 *
 * @param string $task_name
 * @return array|null
 */
function profwabr_state_get($task_name)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->get($task_name);
    }
    return profwabr_state_fallback_get($task_name);
}
endif;

if (!function_exists('profwabr_state_update')) :
/**
 * Merge parziale dello stato di un task.
 *
 * @param string $task_name
 * @param array  $data
 * @return bool
 */
function profwabr_state_update($task_name, $data = array())
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->update($task_name, $data);
    }
    $current = profwabr_state_fallback_get($task_name);
    if (null === $current) {
        return profwabr_state_fallback_set($task_name, $data);
    }
    return profwabr_state_fallback_set($task_name, array_merge($current, $data));
}
endif;

if (!function_exists('profwabr_state_delete')) :
/**
 * Delete dello stato di un task.
 *
 * @param string $task_name
 * @return bool
 */
function profwabr_state_delete($task_name)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->delete($task_name);
    }
    delete_option('profwabr_state_' . sanitize_key((string) $task_name));
    do_action('profwabr_state_changed', $task_name, 'delete', null);
    return true;
}
endif;

if (!function_exists('profwabr_state_start_task')) :
/**
 * Avvia un task (status running + lock anti doppia esecuzione).
 *
 * @param string $task_name
 * @param array  $data
 * @param int    $lock_ttl
 * @return bool
 */
function profwabr_state_start_task($task_name, $data = array(), $lock_ttl = 300)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->start_task($task_name, $data, $lock_ttl);
    }
    if (!profwabr_state_acquire_lock($task_name, $lock_ttl)) {
        return false;
    }
    return profwabr_state_fallback_set($task_name, array_merge($data, array(
        'status'      => 'running',
        'started_at'  => gmdate('Y-m-d H:i:s'),
        'finished_at' => null,
        'last_error'  => null,
    )));
}
endif;

if (!function_exists('profwabr_state_complete_task')) :
/**
 * Completa un task (status completed, finished_at, result).
 *
 * @param string $task_name
 * @param mixed  $result
 * @return bool
 */
function profwabr_state_complete_task($task_name, $result = null)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->complete_task($task_name, $result);
    }
    $ok = profwabr_state_fallback_set($task_name, array(
        'status'      => 'completed',
        'finished_at' => gmdate('Y-m-d H:i:s'),
        'progress'    => 100,
        'last_error'  => null,
    ));
    profwabr_state_release_lock($task_name, null);
    if ($ok) {
        do_action('profwabr_state_completed', $task_name, $result);
    }
    return $ok;
}
endif;

if (!function_exists('profwabr_state_fail_task')) :
/**
 * Marca un task come fallito.
 *
 * @param string $task_name
 * @param string $error
 * @param array  $data
 * @return bool
 */
function profwabr_state_fail_task($task_name, $error, $data = array())
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->fail_task($task_name, $error, $data);
    }
    $current = profwabr_state_fallback_get($task_name);
    $errors  = (is_array($current) && isset($current['error_count'])) ? (int) $current['error_count'] + 1 : 1;
    $ok = profwabr_state_fallback_set($task_name, array_merge($data, array(
        'status'      => 'failed',
        'finished_at' => gmdate('Y-m-d H:i:s'),
        'last_error'  => sanitize_textarea_field((string) $error),
        'error_count' => $errors,
    )));
    profwabr_state_release_lock($task_name, null);
    if ($ok) {
        do_action('profwabr_state_failed', $task_name, $error);
    }
    return $ok;
}
endif;

if (!function_exists('profwabr_state_set_progress')) :
/**
 * Aggiorna il progresso di un task.
 *
 * @param string $task_name
 * @param int    $progress
 * @return bool
 */
function profwabr_state_set_progress($task_name, $progress)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->set_progress($task_name, $progress);
    }
    return profwabr_state_update($task_name, array('progress' => max(0, min(100, (int) $progress))));
}
endif;

if (!function_exists('profwabr_state_acquire_lock')) :
/**
 * Acquisisce il lock di un task (anti doppie esecuzioni concorrenti).
 *
 * @param string $task_name
 * @param int    $ttl
 * @return string|false
 */
function profwabr_state_acquire_lock($task_name, $ttl = 300)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->acquire_lock($task_name, $ttl);
    }
    // Fallback: add_option è atomico → ottimo lock cross-request.
    $token   = wp_generate_uuid4();
    $expires = time() + max(1, (int) $ttl);
    $option  = 'profwabr_lock_' . sanitize_key((string) $task_name);
    if (add_option($option, $token, '', false)) {
        // Salva anche la scadenza nello stato.
        profwabr_state_fallback_set($task_name, array('lock_expires_at' => gmdate('Y-m-d H:i:s', $expires)));
        return $token;
    }
    // Lock presente: verifica scadenza.
    $existing = get_option($option, '');
    $state    = profwabr_state_fallback_get($task_name);
    if ($existing && is_array($state) && !empty($state['lock_expires_at']) && strtotime($state['lock_expires_at']) < time()) {
        delete_option($option);
        return profwabr_state_acquire_lock($task_name, $ttl);
    }
    return false;
}
endif;

if (!function_exists('profwabr_state_release_lock')) :
/**
 * Rilascia il lock di un task.
 *
 * @param string      $task_name
 * @param string|null $token
 * @return bool
 */
function profwabr_state_release_lock($task_name, $token = null)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->release_lock($task_name, $token);
    }
    delete_option('profwabr_lock_' . sanitize_key((string) $task_name));
    profwabr_state_fallback_set($task_name, array('lock_expires_at' => null));
    return true;
}
endif;

if (!function_exists('profwabr_state_is_locked')) :
/**
 * Verifica se un task è lockato (lock non scaduto).
 *
 * @param string $task_name
 * @return bool
 */
function profwabr_state_is_locked($task_name)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->is_locked($task_name);
    }
    if (!get_option('profwabr_lock_' . sanitize_key((string) $task_name), false)) {
        return false;
    }
    $state = profwabr_state_fallback_get($task_name);
    if (is_array($state) && !empty($state['lock_expires_at']) && strtotime($state['lock_expires_at']) <= time()) {
        return false;
    }
    return true;
}
endif;

if (!function_exists('profwabr_state_get_all')) :
/**
 * Elenca task per stato.
 *
 * @param string|null $status
 * @param int         $limit
 * @return array
 */
function profwabr_state_get_all($status = null, $limit = 100)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->get_all($status, $limit);
    }
    global $wpdb;
    $like = 'profwabr_state_%';
    $rows = $wpdb->get_results(
        $wpdb->prepare("SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s ORDER BY option_name DESC LIMIT %d", $like, (int) $limit),
        ARRAY_A
    );
    $out = array();
    foreach ((array) $rows as $row) {
        $task = str_replace('profwabr_state_', '', $row['option_name']);
        $data = maybe_unserialize($row['option_value']);
        if (is_array($data)) {
            if (null !== $status && (!isset($data['status']) || $data['status'] !== $status)) {
                continue;
            }
            $out[] = array_merge(array('task_name' => $task), $data);
        }
    }
    return $out;
}
endif;

if (!function_exists('profwabr_state_cleanup')) :
/**
 * Pulizia task vecchi completati/falliti.
 *
 * @param string|null $older_than
 * @return int
 */
function profwabr_state_cleanup($older_than = null)
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->cleanup($older_than);
    }
    // Fallback: delete opzioni completate/failed con updated_at vecchio.
    $threshold = (null === $older_than) ? strtotime('-30 days') : strtotime($older_than);
    $all = profwabr_state_get_all(null, 500);
    $deleted = 0;
    foreach ($all as $row) {
        if (in_array($row['status'], array('completed', 'failed'), true)) {
            $updated = isset($row['updated_at']) ? strtotime($row['updated_at']) : 0;
            if ($updated && $updated < $threshold) {
                profwabr_state_delete($row['task_name']);
                $deleted++;
            }
        }
    }
    return $deleted;
}
endif;

if (!function_exists('profwabr_state_reset_all')) :
/**
 * Azzera l'intero state store.
 *
 * @return bool
 */
function profwabr_state_reset_all()
{
    $mgr = Profwabr_State_Manager::instance();
    if ($mgr->using_sqlite()) {
        return $mgr->reset_all();
    }
    global $wpdb;
    $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'profwabr_state_%' OR option_name LIKE 'profwabr_lock_%'");
    do_action('profwabr_state_changed', '*', 'reset', null);
    return true;
}
endif;

/* ---------------------------------------------------------------------
 * FALLBACK STORAGE (WP OPTIONS)
 * ------------------------------------------------------------------- */

if (!function_exists('profwabr_state_fallback_set')) :
/**
 * Set su storage fallback (WP options).
 *
 * @param string $task_name
 * @param array  $data
 * @return bool
 */
function profwabr_state_fallback_set($task_name, $data = array())
{
    $task_name = sanitize_key((string) $task_name);
    if ('' === $task_name) {
        return false;
    }
    $now      = gmdate('Y-m-d H:i:s');
    $status   = isset($data['status']) ? sanitize_key((string) $data['status']) : 'pending';
    $progress = isset($data['progress']) ? max(0, min(100, (int) $data['progress'])) : 0;
    $payload  = isset($data['payload']) ? $data['payload'] : array();
    $current  = profwabr_state_fallback_get($task_name);
    if (!is_array($current)) {
        $current = array();
    }
    $record = array_merge(array(
        'status'          => 'pending',
        'progress'        => 0,
        'payload'         => array(),
        'created_at'      => $now,
        'started_at'      => null,
        'finished_at'     => null,
        'updated_at'      => $now,
        'last_error'      => null,
        'error_count'     => 0,
        'lock_token'      => null,
        'lock_expires_at' => null,
    ), $current, $data);
    $record['status']     = $status;
    $record['progress']   = $progress;
    $record['payload']    = $payload;
    $record['updated_at'] = $now;
    update_option('profwabr_state_' . $task_name, $record, false);
    do_action('profwabr_state_changed', $task_name, 'set', $data);
    return true;
}
endif;

if (!function_exists('profwabr_state_fallback_get')) :
/**
 * Get su storage fallback (WP options).
 *
 * @param string $task_name
 * @return array|null
 */
function profwabr_state_fallback_get($task_name)
{
    $task_name = sanitize_key((string) $task_name);
    if ('' === $task_name) {
        return null;
    }
    $data = get_option('profwabr_state_' . $task_name, null);
    if (!is_array($data)) {
        return null;
    }
    return $data;
}
endif;

/* ---------------------------------------------------------------------
 * HOOK DI INIZIALIZZAZIONE
 * ------------------------------------------------------------------- */

// Inizializza il layer il prima possibile ma dopo che WP è pronto (options/cron).
add_action('init', 'profwabr_state_init', 5);

// Pulizia periodica dei task vecchi (registrata in init via profwabr_state_init).
add_action('profwabr_state_cleanup_event', 'profwabr_state_cleanup');
