<?php
// =============================================================
// STORAGE/DB — подключение (singleton), retry-обёртка, схема (миграции)
// =============================================================
require_once __DIR__ . '/../config.php';

// ---------------------------------------------------------------
// Подключение к БД (singleton)
// ---------------------------------------------------------------

function storage_db(): PDO
{
    static $pdo = null;

    if ($pdo !== null) {
        return $pdo;
    }

    $dir = dirname(DB_FILE);
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    $pdo = new PDO('sqlite:' . DB_FILE);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

    // WAL-режим: параллельные читатели не блокируют писателя (cron + dashboard)
    $pdo->exec('PRAGMA journal_mode = WAL');
    $pdo->exec('PRAGMA busy_timeout    = 5000');
    $pdo->exec('PRAGMA synchronous  = NORMAL');
    $pdo->exec('PRAGMA foreign_keys = ON');

    storage_migrate($pdo);

    return $pdo;
}


// ---------------------------------------------------------------
// Retry-обёртка для SQLite-транзакций
// ---------------------------------------------------------------

/**
 * Выполняет $fn() в контексте PDO с повтором при SQLITE_BUSY/SQLITE_LOCKED.
 * busy_timeout=5000 уже стоит, но при очень активной конкуренции
 * (dashboard + cron + analytics одновременно) одного timeout мало.
 *
 * Использование:
 *   storage_retry(function() use ($db, $data) {
 *       $db->beginTransaction();
 *       // ...
 *       $db->commit();
 *   });
 */
function storage_retry(callable $fn, int $maxAttempts = 3, int $sleepMs = 200): mixed
{
    $attempt = 0;
    while (true) {
        try {
            return $fn();
        } catch (PDOException $e) {
            $attempt++;
            $msg = $e->getMessage();
            $isLocked = str_contains($msg, 'database is locked')
                     || str_contains($msg, 'SQLITE_BUSY')
                     || str_contains($msg, 'SQLITE_LOCKED');

            if (!$isLocked || $attempt >= $maxAttempts) {
                throw $e;
            }

            // Логируем только с третьей попытки — не засорять лог при норме
            if ($attempt >= 2) {
                error_log("storage_retry: attempt $attempt after lock: $msg");
            }

            usleep($sleepMs * 1000 * $attempt); // экспоненциальная задержка
        }
    }
}

// ---------------------------------------------------------------
// Миграции схемы
// ---------------------------------------------------------------

function storage_migrate(PDO $pdo): void
{
    // ----------------------------------------------------------
    // Текущий баланс виртуального счёта (1 строка на entity)
    // entity_type: 'wallet' | 'pool'
    // ----------------------------------------------------------
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS wallet_balances (
            entity_type        TEXT    NOT NULL,
            entity_id          TEXT    NOT NULL,
            balance            REAL    NOT NULL DEFAULT 0,
            peak_balance       REAL    NOT NULL DEFAULT 0,
            max_drawdown_pct   REAL    NOT NULL DEFAULT 0,
            total_gross_pnl    REAL    NOT NULL DEFAULT 0,
            total_net_pnl      REAL    NOT NULL DEFAULT 0,
            total_fees         REAL    NOT NULL DEFAULT 0,
            total_funding      REAL    NOT NULL DEFAULT 0,
            trades_count       INTEGER NOT NULL DEFAULT 0,
            wins_count         INTEGER NOT NULL DEFAULT 0,
            gross_profit_sum   REAL    NOT NULL DEFAULT 0,
            gross_loss_sum     REAL    NOT NULL DEFAULT 0,
            mtm_peak_equity      REAL  NOT NULL DEFAULT 0,
            mtm_max_drawdown_pct REAL  NOT NULL DEFAULT 0,
            updated_at         TEXT    NOT NULL DEFAULT (datetime(\'now\')),
            PRIMARY KEY (entity_type, entity_id)
        )
    ');

    // ----------------------------------------------------------
    // Открытые позиции (0-N строк на entity, 1 строка на символ)
    // ----------------------------------------------------------
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS open_positions (
            entity_type          TEXT    NOT NULL,
            entity_id            TEXT    NOT NULL,
            symbol               TEXT    NOT NULL,
            side                 TEXT    NOT NULL,
            entry_price          REAL    NOT NULL DEFAULT 0,
            trader_entry_price   REAL,
            size_usd             REAL    NOT NULL DEFAULT 0,
            leverage             INTEGER NOT NULL DEFAULT 1,
            entry_fee            REAL    NOT NULL DEFAULT 0,
            opened_at            INTEGER NOT NULL DEFAULT 0,
            trade_id             TEXT    NOT NULL DEFAULT "",
            entry_reason         TEXT    NOT NULL DEFAULT "normal",
            owner_trader_id      TEXT,
            owner_trader_label   TEXT,
            trader_size_coins    REAL,
            symbol_exchange      TEXT,
            PRIMARY KEY (entity_type, entity_id, symbol)
        )
    ');

    $pdo->exec('
        CREATE INDEX IF NOT EXISTS idx_open_positions_entity
            ON open_positions (entity_type, entity_id)
    ');

    // ----------------------------------------------------------
    // Engine state: watermark + cooldown
    // entity_type: 'wallet' | 'pool_trader'
    // entity_id:   wallet_id  | pool_id:trader_id
    // ----------------------------------------------------------
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS entity_state (
            entity_type    TEXT    NOT NULL,
            entity_id      TEXT    NOT NULL,
            last_seen_ts   INTEGER,
            cooldown_json  TEXT    NOT NULL DEFAULT "{}",
            updated_at     TEXT    NOT NULL DEFAULT (datetime(\'now\')),
            PRIMARY KEY (entity_type, entity_id)
        )
    ');

    // ----------------------------------------------------------
    // Seen fills — дедупликация (отдельная таблица для быстрой проверки)
    // ----------------------------------------------------------
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS seen_fills (
            entity_type  TEXT    NOT NULL,
            entity_id    TEXT    NOT NULL,
            fill_id      TEXT    NOT NULL,
            seen_at      INTEGER NOT NULL,
            PRIMARY KEY (entity_type, entity_id, fill_id)
        )
    ');

    $pdo->exec('
        CREATE INDEX IF NOT EXISTS idx_seen_fills_seen_at ON seen_fills (seen_at)
    ');

    // ----------------------------------------------------------
    // Watching — отложенный вход по просадке трейдера
    // ----------------------------------------------------------
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS watching (
            entity_type          TEXT    NOT NULL,
            entity_id            TEXT    NOT NULL,
            symbol               TEXT    NOT NULL,
            side                 TEXT    NOT NULL,
            trader_entry_price   REAL    NOT NULL DEFAULT 0,
            trade_id             TEXT    NOT NULL DEFAULT "",
            leverage             INTEGER NOT NULL DEFAULT 1,
            source               TEXT    NOT NULL DEFAULT "wallet",
            pool_id              TEXT,
            trader_id            TEXT,
            trader_label         TEXT,
            trader_size_usd      REAL,
            symbol_exchange      TEXT,
            started_at           INTEGER NOT NULL DEFAULT 0,
            PRIMARY KEY (entity_type, entity_id, symbol)
        )
    ');

    $pdo->exec('
        CREATE INDEX IF NOT EXISTS idx_watching_entity ON watching (entity_type, entity_id)
    ');

    // Миграции для существующих БД: добавляем недостающие столбцы.
    // SQLite не поддерживает ADD COLUMN IF NOT EXISTS, поэтому проверяем через PRAGMA.
    $watchingCols = $pdo->query('PRAGMA table_info(watching)')->fetchAll(PDO::FETCH_COLUMN, 1);
    if (!in_array('trader_size_usd', $watchingCols, true)) {
        $pdo->exec('ALTER TABLE watching ADD COLUMN trader_size_usd REAL');
    }
    // symbol_exchange — точный тикер биржи ('kPEPE'): нужен вебхукам, которые
    // шлются позже входа из записи позиции/watch (ликвидация, watching-триггер).
    if (!in_array('symbol_exchange', $watchingCols, true)) {
        $pdo->exec('ALTER TABLE watching ADD COLUMN symbol_exchange TEXT');
    }
    $posCols = $pdo->query('PRAGMA table_info(open_positions)')->fetchAll(PDO::FETCH_COLUMN, 1);
    if (!in_array('symbol_exchange', $posCols, true)) {
        $pdo->exec('ALTER TABLE open_positions ADD COLUMN symbol_exchange TEXT');
    }

    // ----------------------------------------------------------
    // Исторические сделки
    // ----------------------------------------------------------
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS trades_closed (
            id                  INTEGER PRIMARY KEY AUTOINCREMENT,
            open_time           TEXT    NOT NULL,
            close_time          TEXT    NOT NULL,
            symbol              TEXT    NOT NULL,
            side                TEXT    NOT NULL,
            entry_price         REAL    NOT NULL DEFAULT 0,
            trader_entry_price  REAL,
            exit_price          REAL    NOT NULL DEFAULT 0,
            size_usd            REAL    NOT NULL DEFAULT 0,
            leverage            INTEGER NOT NULL DEFAULT 1,
            gross_pnl           REAL    NOT NULL DEFAULT 0,
            entry_fee           REAL    NOT NULL DEFAULT 0,
            exit_fee            REAL    NOT NULL DEFAULT 0,
            funding             REAL    NOT NULL DEFAULT 0,
            net_pnl             REAL    NOT NULL DEFAULT 0,
            pnl_pct             REAL    NOT NULL DEFAULT 0,
            entry_slippage_pct  REAL,
            exit_slippage_pct   REAL,
            trade_id            TEXT    NOT NULL DEFAULT "",
            wallet_id           TEXT    NOT NULL DEFAULT "",
            wallet_label        TEXT    NOT NULL DEFAULT "",
            entry_reason        TEXT    NOT NULL DEFAULT "normal",
            exit_reason         TEXT    NOT NULL DEFAULT "signal"
        )
    ');

    $pdo->exec('
        CREATE INDEX IF NOT EXISTS idx_trades_wallet_id  ON trades_closed (wallet_id);
        CREATE INDEX IF NOT EXISTS idx_trades_close_time ON trades_closed (close_time);
        CREATE INDEX IF NOT EXISTS idx_trades_symbol     ON trades_closed (symbol)
    ');

    $pdo->exec('
        CREATE TABLE IF NOT EXISTS signals (
            id           INTEGER PRIMARY KEY AUTOINCREMENT,
            datetime     TEXT    NOT NULL,
            symbol       TEXT    NOT NULL,
            side         TEXT    NOT NULL,
            size         REAL    NOT NULL DEFAULT 0,
            leverage     INTEGER NOT NULL DEFAULT 1,
            source_trade TEXT    NOT NULL DEFAULT "",
            trader_price REAL,
            mode         TEXT    NOT NULL DEFAULT "mock",
            wallet_id    TEXT    NOT NULL DEFAULT "",
            wallet_label TEXT    NOT NULL DEFAULT ""
        )
    ');

    $pdo->exec('
        CREATE INDEX IF NOT EXISTS idx_signals_wallet_id ON signals (wallet_id);
        CREATE INDEX IF NOT EXISTS idx_signals_datetime  ON signals (datetime)
    ');

    // ----------------------------------------------------------
    // last_sync — для распределения нагрузки при батч-обработке
    // ----------------------------------------------------------
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS wallet_sync (
            wallet_id   TEXT    NOT NULL PRIMARY KEY,
            last_sync   INTEGER NOT NULL DEFAULT 0
        )
    ');

    // ----------------------------------------------------------
    // wallet_log — ручной журнал заметок по кошельку
    // ----------------------------------------------------------
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS wallet_log (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            wallet_id   TEXT    NOT NULL,
            created_at  TEXT    NOT NULL DEFAULT (datetime(\'now\')),
            note        TEXT    NOT NULL
        )
    ');

    $pdo->exec('
        CREATE INDEX IF NOT EXISTS idx_wallet_log_wallet_id ON wallet_log (wallet_id);
        CREATE INDEX IF NOT EXISTS idx_wallet_log_created_at ON wallet_log (created_at)
    ');

    // Учёт ордеров прямой торговли (execution_mode=direct): основной ордер,
    // сеточные доборы, Stop Loss, Take Profit — чтобы при закрытии позиции
    // отменить ВСЕ связанные ордера по (entity, symbol). Тегируются нашим cloid.
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS hl_orders (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            entity_type TEXT    NOT NULL,
            entity_id   TEXT    NOT NULL,
            symbol      TEXT    NOT NULL,
            cloid       TEXT    NOT NULL,
            oid         INTEGER,
            kind        TEXT    NOT NULL,
            created_at  TEXT    NOT NULL DEFAULT (datetime(\'now\'))
        )
    ');
    $pdo->exec('
        CREATE INDEX IF NOT EXISTS idx_hl_orders_entity
            ON hl_orders (entity_type, entity_id, symbol)
    ');
    // Лизы владения символом на бирже (PRD §10): PRIMARY KEY (account, symbol),
    // поэтому захват атомарен и действует В РАМКАХ КОНКРЕТНОГО АККАУНТА. account —
    // адрес субаккаунта direct-сущности (или мастер-аккаунт, если субаккаунт не
    // задан). Так несколько сущностей на РАЗНЫХ субаккаунтах могут держать один
    // символ независимо, а сущности на ОДНОМ аккаунте — взаимно исключаются.
    // Захватывается ДО marketOpen, освобождается при закрытии/аборте.
    $pdo->exec('
        CREATE TABLE IF NOT EXISTS hl_locks (
            account     TEXT    NOT NULL,
            symbol      TEXT    NOT NULL,
            entity_type TEXT    NOT NULL,
            entity_id   TEXT    NOT NULL,
            claimed_at  INTEGER NOT NULL,
            PRIMARY KEY (account, symbol)
        )
    ');
    // Миграция старой схемы (PRIMARY KEY symbol, без account): такие лизы были на
    // мастер-аккаунте — переносим их под его адрес. SQLite не умеет менять PK через
    // ALTER, поэтому пересоздаём таблицу с переносом данных.
    $lockCols = $pdo->query('PRAGMA table_info(hl_locks)')->fetchAll(PDO::FETCH_COLUMN, 1);
    if ($lockCols && !in_array('account', $lockCols, true)) {
        $master = strtolower((string) (defined('HL_ACCOUNT_ADDRESS') ? HL_ACCOUNT_ADDRESS : ''));
        $pdo->exec('ALTER TABLE hl_locks RENAME TO hl_locks_old');
        $pdo->exec('
            CREATE TABLE hl_locks (
                account     TEXT    NOT NULL,
                symbol      TEXT    NOT NULL,
                entity_type TEXT    NOT NULL,
                entity_id   TEXT    NOT NULL,
                claimed_at  INTEGER NOT NULL,
                PRIMARY KEY (account, symbol)
            )
        ');
        $ins = $pdo->prepare('
            INSERT OR IGNORE INTO hl_locks (account, symbol, entity_type, entity_id, claimed_at)
            SELECT ?, symbol, entity_type, entity_id, claimed_at FROM hl_locks_old
        ');
        $ins->execute([$master]);
        $pdo->exec('DROP TABLE hl_locks_old');
    }
}
