<?php
// STORAGE/STATE — engine state (watermark+cooldown) + seen_fills
require_once __DIR__ . '/db.php';

// ---------------------------------------------------------------
// Engine state: watermark + cooldown + seen fills
// ---------------------------------------------------------------

/**
 * Загружает state для engine (watermark + cooldown).
 * Seen fills хранятся отдельно в seen_fills, но возвращаются здесь же
 * для совместимости с process_fills_prepare().
 *
 * Возвращает ['trades' => [fill_id => seen_at], 'cooldown' => [...], 'last_seen_ts' => int|null]
 */
function storage_load_state(string $entityType, string $entityId): array
{
    $db = storage_db();

    // Основной state (watermark + cooldown)
    $stmt = $db->prepare('SELECT * FROM entity_state WHERE entity_type = ? AND entity_id = ?');
    $stmt->execute([$entityType, $entityId]);
    $row = $stmt->fetch();

    // Важно: не кастовать (int) до проверки на NULL —
    // (int) null = 0, что ломает логику инициализации watermark в engine.php
    $lastSeenTs = null;
    if ($row && $row['last_seen_ts'] !== null) {
        $lastSeenTs = (int) $row['last_seen_ts'];
    }
    $cooldown = $row ? (json_decode($row['cooldown_json'], true) ?: []) : [];

    // Seen fills (только за последние 24ч — pruning такой же как в файловом варианте)
    $cutoff = time() - 86400;
    $stmt = $db->prepare('
        SELECT fill_id, seen_at FROM seen_fills
        WHERE entity_type = ? AND entity_id = ? AND seen_at >= ?
    ');
    $stmt->execute([$entityType, $entityId, $cutoff]);
    $fills = [];
    foreach ($stmt->fetchAll() as $r) {
        $fills[$r['fill_id']] = (int) $r['seen_at'];
    }

    return [
        'trades'       => $fills,
        'cooldown'     => $cooldown,
        'last_seen_ts' => $lastSeenTs,
    ];
}

/**
 * Сохраняет state: watermark + cooldown (UPSERT) и новые seen fills (INSERT OR IGNORE).
 * Также удаляет seen fills старше 24ч для этого entity (pruning).
 */
function storage_save_state(string $entityType, string $entityId, array $state): void
{
    $db = storage_db();
    storage_retry(function() use ($db, $entityType, $entityId, $state) {
    $db->beginTransaction();
    try {
        // UPSERT основного state
        $db->prepare('
            INSERT INTO entity_state (entity_type, entity_id, last_seen_ts, cooldown_json, updated_at)
            VALUES (?, ?, ?, ?, datetime(\'now\'))
            ON CONFLICT(entity_type, entity_id) DO UPDATE SET
                last_seen_ts  = excluded.last_seen_ts,
                cooldown_json = excluded.cooldown_json,
                updated_at    = excluded.updated_at
        ')->execute([
            $entityType, $entityId,
            $state['last_seen_ts'],
            json_encode($state['cooldown'] ?? []),
        ]);

        // Новые seen fills. ON CONFLICT обновляет seen_at — это нужно для
        // граничных fills (ровно на watermark), которые приходят каждый цикл:
        // освежённый seen_at не даёт pruning'у удалить их и спровоцировать
        // повторную обработку при простое кошелька >24ч (см. engine.php).
        if (!empty($state['trades'])) {
            $stmt = $db->prepare('
                INSERT INTO seen_fills (entity_type, entity_id, fill_id, seen_at)
                VALUES (?, ?, ?, ?)
                ON CONFLICT(entity_type, entity_id, fill_id) DO UPDATE SET
                    seen_at = excluded.seen_at
            ');
            foreach ($state['trades'] as $fillId => $seenAt) {
                $stmt->execute([$entityType, $entityId, (string) $fillId, (int) $seenAt]);
            }
        }

        // Pruning старых fills
        $cutoff = time() - 86400;
        $db->prepare('DELETE FROM seen_fills WHERE entity_type = ? AND entity_id = ? AND seen_at < ?')
           ->execute([$entityType, $entityId, $cutoff]);

        $db->commit();
    } catch (Throwable $e) {
        $db->rollBack();
        throw $e;
    }
    }); // storage_retry
}
