<?php
// =============================================================
// WATCHING TRACKER — отложенный вход по просадке трейдера
// =============================================================
//
// Бэкенд: SQLite (таблица watching через storage.php).

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/logger.php';
require_once __DIR__ . '/storage.php';
require_once __DIR__ . '/balance_tracker.php';
require_once __DIR__ . '/position_tracker.php';
require_once __DIR__ . '/pool_tracker.php';
require_once __DIR__ . '/price_fetcher.php';

//
// Логика не изменилась — на каждом cron-тике считается UPNL
// трейдера по его trader_entry_price и текущей mid price.
// Когда UPNL достигает -copy_after_drawdown_pct% — открываем позицию.
//
// State адресуется парой (entity_type, entity_id):
//   wallet → ('wallet', $walletId)
//   pool   → ('pool',   $poolId)
//
// Публичный API:
//   add_to_watching_wallet(walletId, signal)            → void
//   add_to_watching_pool(poolId, ...)                   → void
//   remove_from_watching(entityType, entityId, symbol)  → void
//   check_wallet_watching(...)                          → void
//   check_pool_watching(...)                            → void
// =============================================================

// ---------------------------------------------------------------
// Загрузка / сохранение watching (делегируем в storage)
// ---------------------------------------------------------------

function load_watching(string $entityType, string $entityId): array
{
    return storage_load_watching($entityType, $entityId);
}

function save_watching(string $entityType, string $entityId, array $watching): void
{
    storage_save_watching($entityType, $entityId, $watching);
}

// ---------------------------------------------------------------
// Добавление в наблюдение
// ---------------------------------------------------------------

/**
 * Добавляет позицию трейдера в watching (wallet-режим).
 */
function add_to_watching_wallet(string $walletId, array $signal): void
{
    if ($signal['trader_price'] === null) {
        bot_log("WATCHING: no trader_price for {$signal['symbol']}, cannot watch drawdown");
        return;
    }

    // Если по этому символу уже есть watching-запись в ПРОТИВОПОЛОЖНУЮ сторону —
    // значит трейдер закрыл (или развернул) позицию раньше, чем наступила просадка.
    // Снимаем устаревшую запись и выходим: наблюдать больше не за чем. Если трейдер
    // затем откроет новую позицию, придёт отдельный сигнал и watch создастся заново.
    // (Без этого INSERT OR IGNORE проигнорировал бы новый сигнал и оставил протухший watch.)
    $existingWatch = storage_load_watching('wallet', $walletId);
    if (isset($existingWatch[$signal['symbol']])
        && $existingWatch[$signal['symbol']]['side'] !== $signal['side']) {
        storage_remove_watching('wallet', $walletId, $signal['symbol']);
        bot_log("WATCHING: {$signal['symbol']} — противоположный сигнал, трейдер закрыл позицию, снимаю watch ({$existingWatch[$signal['symbol']]['side']})");
        return;
    }

    $added = storage_add_watching('wallet', $walletId, $signal['symbol'], [
        'side'               => $signal['side'],
        'trader_entry_price' => $signal['trader_price'],
        'trade_id'           => $signal['source_trade'],
        'leverage'           => $signal['leverage'],
        'source'             => 'wallet',
        'wallet_id'          => $walletId,
        'trader_size_usd'    => $signal['trader_size_usd'] ?? null,  // для proportional sizing
        'symbol_exchange'    => $signal['symbol_exchange'] ?? null,  // точный тикер биржи для вебхука при триггере
        'started_at'         => time(),
    ]);

    if ($added) {
        bot_log("WATCHING: started watching {$signal['symbol']} for wallet $walletId (trader_entry={$signal['trader_price']})");
    }
    // Если INSERT OR IGNORE ничего не вставил — символ уже наблюдался в ту же сторону, молча пропускаем
}

/**
 * Добавляет позицию трейдера в watching (pool-режим).
 */
function add_to_watching_pool(string $poolId, string $traderId, string $traderLabel, array $signal): void
{
    if ($signal['trader_price'] === null) {
        bot_log("WATCHING POOL: no trader_price for {$signal['symbol']}, cannot watch drawdown");
        return;
    }

    // Противоположный сигнал по уже наблюдаемому символу = трейдер закрыл/развернул
    // позицию до просадки. Снимаем устаревшую запись и выходим (только если её владелец —
    // тот же трейдер; чужой символ в пуле трогать нельзя из-за ownership-логики).
    $existingWatch = storage_load_watching('pool', $poolId);
    if (isset($existingWatch[$signal['symbol']])
        && ($existingWatch[$signal['symbol']]['trader_id'] ?? null) === $traderId
        && $existingWatch[$signal['symbol']]['side'] !== $signal['side']) {
        storage_remove_watching('pool', $poolId, $signal['symbol']);
        bot_log("WATCHING POOL [$poolId]: {$signal['symbol']} — противоположный сигнал от $traderLabel, трейдер закрыл позицию, снимаю watch");
        return;
    }

    $added = storage_add_watching('pool', $poolId, $signal['symbol'], [
        'side'               => $signal['side'],
        'trader_entry_price' => $signal['trader_price'],
        'trade_id'           => $signal['source_trade'],
        'leverage'           => $signal['leverage'],
        'source'             => 'pool',
        'pool_id'            => $poolId,
        'trader_id'          => $traderId,
        'trader_label'       => $traderLabel,
        'trader_size_usd'    => $signal['trader_size_usd'] ?? null,  // для proportional sizing
        'symbol_exchange'    => $signal['symbol_exchange'] ?? null,  // точный тикер биржи для вебхука при триггере
        'started_at'         => time(),
    ]);

    if ($added) {
        bot_log("WATCHING POOL [$poolId]: started watching {$signal['symbol']} by $traderLabel (trader_entry={$signal['trader_price']})");
    } else {
        bot_log("WATCHING POOL: {$signal['symbol']} already watched in pool $poolId, ignoring signal from $traderLabel");
    }
}

/**
 * Снимает символ с наблюдения.
 */
function remove_from_watching(string $entityType, string $entityId, string $symbol): void
{
    storage_remove_watching($entityType, $entityId, $symbol);
}

// ---------------------------------------------------------------
// Расчёт UPNL трейдера (логика не изменилась)
// ---------------------------------------------------------------

/**
 * Истекла ли watch-запись по TTL. $ttlHours<=0 → срока нет (никогда не истекает).
 * Запись без валидного started_at не считается просроченной (нечего сравнивать).
 */
function watching_is_expired(array $entry, float $ttlHours, ?int $now = null): bool
{
    if ($ttlHours <= 0) {
        return false;
    }
    $startedAt = (int) ($entry['started_at'] ?? 0);
    if ($startedAt <= 0) {
        return false;
    }
    $now = $now ?? time();
    return ($now - $startedAt) >= (int) round($ttlHours * 3600);
}

function calculate_trader_upnl_pct(float $traderEntryPrice, float $currentPrice, string $side, int $leverage): float
{
    if ($traderEntryPrice <= 0) {
        return 0.0;
    }

    $direction      = ($side === 'long') ? 1 : -1;
    $priceChangePct = ($currentPrice - $traderEntryPrice) / $traderEntryPrice;
    return round($priceChangePct * $direction * $leverage * 100, 4);
}

// ---------------------------------------------------------------
// Основной цикл проверки watching (вызывается из bot.php)
// ---------------------------------------------------------------

/**
 * @param callable|null $onTrigger fn(array $signal, array $settings, array $walletMeta): void —
 *   вызывается при срабатывании отложенного входа ПОСЛЕ открытия бумажной позиции,
 *   чтобы отправить реальный ордер (direct/executor_url) ровно в момент входа, а не
 *   при первом сигнале. bot.php передаёт сюда execute_real_order; в тестах — null
 *   (только симуляция, без реального исполнения).
 */
function check_wallet_watching(string $walletId, array $settings, array $walletMeta, ?callable $onTrigger = null): void
{
    $watching = storage_load_watching('wallet', $walletId);

    if (empty($watching)) {
        return;
    }

    // В direct-режиме реальные ордера идут через биржу (onTrigger), поэтому
    // ТОРГОВЫЕ вебхуки при срабатывании watch глушатся (паритет с обычным входом,
    // где execute_signals в direct тоже их фильтрует), а информационные
    // (telegram) остаются — уведомление о входе должно прийти и в direct.
    $isDirect      = ($settings['execution_mode'] ?? 'webhook') === 'direct';
    $triggerWebhooks = $isDirect
        ? webhooks_notify_only($settings['webhooks'] ?? [])
        : ($settings['webhooks'] ?? []);

    $drawdownPct   = (float) ($settings['copy_after_drawdown_pct'] ?? 5);
    $ttlHours      = (float) ($settings['copy_after_drawdown_ttl_hours'] ?? 0);

    foreach ($watching as $symbol => $entry) {
        // TTL: просроченную запись снимаем не дожидаясь просадки (и без запроса цены).
        if (watching_is_expired($entry, $ttlHours)) {
            storage_remove_watching('wallet', $walletId, $symbol);
            bot_log(sprintf('WATCHING [%s] %s — TTL %.0fh истёк, снимаю watch', $walletMeta['label'], $symbol, $ttlHours));
            continue;
        }

        $currentPrice = fetch_mid_price($symbol);
        if ($currentPrice === null) {
            continue;
        }

        $upnlPct = calculate_trader_upnl_pct(
            $entry['trader_entry_price'],
            $currentPrice,
            $entry['side'],
            (int) $entry['leverage']
        );

        bot_log(sprintf(
            'WATCHING [%s] %s UPNL=%.2f%% threshold=-%.2f%%',
            $walletMeta['label'], $symbol, $upnlPct, $drawdownPct
        ));

        if ($upnlPct <= -$drawdownPct) {
            $positions = load_positions('wallet', $walletId);

            if (isset($positions[$symbol])) {
                bot_log("WATCHING: $symbol already has open position, removing from watch");
                storage_remove_watching('wallet', $walletId, $symbol);
                continue;
            }

            $balanceState = load_balance('wallet', $walletId, $settings);

            // Размер трейдера в монетах (для доли частичного закрытия):
            // trader_size_usd / цена входа трейдера. Может быть null, если данных нет.
            $traderCoins = (isset($entry['trader_size_usd']) && $entry['trader_size_usd'] !== null
                            && $entry['trader_entry_price'] > 0)
                ? (float) $entry['trader_size_usd'] / (float) $entry['trader_entry_price']
                : null;

            // Pseudo-signal из watch-записи: нужен общему open_tracked_position для
            // proportional sizing и переноса trader_price/coins в позицию.
            $entrySignal = [
                'symbol'            => $symbol,
                'symbol_exchange'   => $entry['symbol_exchange'] ?? $symbol,
                'side'              => $entry['side'],
                'action'            => 'auto',   // отложенный вход = открытие
                // size в монетах (для executor_url/mock-лога при срабатывании):
                // монеты трейдера × scale_factor, как в map_to_signal.
                'size'              => $traderCoins !== null
                    ? round($traderCoins * (float) ($settings['scale_factor'] ?? 1), 6) : 0.0,
                'leverage'          => (int) $entry['leverage'],
                'source_trade'      => $entry['trade_id'],
                'trader_price'      => $entry['trader_entry_price'],
                'trader_size_usd'   => $entry['trader_size_usd'] ?? null,
                'trader_size_coins' => $traderCoins,
            ];

            // Отложенный вход = фактическое открытие. Общий путь open_tracked_position;
            // open-вебхук шлём здесь (webhookMeta задан) — сигнал был отложен в watch
            // и не доходит до executor, иначе потребитель не откроет позицию.
            $result = open_tracked_position(
                'wallet', $walletId, $positions, $balanceState['balance'],
                $entrySignal, $currentPrice,
                fn(float $bal) => resolve_position_size_usd($settings, $bal, $entrySignal, $walletMeta['address'] ?? null),
                (int) $entry['leverage'], $settings,
                null,
                $triggerWebhooks,
                ['id' => $walletMeta['id'], 'label' => $walletMeta['label'] ?? $walletMeta['id']],
                'drawdown'
            );

            if ($result['status'] !== 'opened') {
                bot_log("WATCHING [{$walletMeta['label']}]: $symbol triggered but insufficient margin, keeping watch");
                continue;
            }

            storage_remove_watching('wallet', $walletId, $symbol);

            // Реальный ордер уходит ИМЕННО сейчас (вход состоялся), а не при первом
            // сигнале — паритет реала с симуляцией для drawdown-режима.
            if ($onTrigger !== null) {
                $onTrigger($entrySignal, $settings, $walletMeta);
            }

            bot_log(sprintf(
                'WATCHING TRIGGERED [%s]: %s %s @ %.4f | trader_entry=%.4f | UPNL=%.2f%% (<= -%.2f%%)',
                $walletMeta['label'], $symbol, strtoupper($entry['side']),
                $result['positions'][$symbol]['entry_price'], $entry['trader_entry_price'], $upnlPct, $drawdownPct
            ));
        }
    }
}

/**
 * @param callable|null $onTrigger fn(array $signal, array $pool, array $trader): void —
 *   вызывается при срабатывании отложенного входа ПОСЛЕ открытия бумажной позиции,
 *   чтобы отправить реальный ордер пула на биржу ровно в момент входа (паритет реала
 *   с симуляцией). bot.php передаёт сюда execute_pool_real_order; в тестах — null.
 */
function check_pool_watching(string $poolId, array $pool, array $traders, ?callable $onTrigger = null): void
{
    $watching = storage_load_watching('pool', $poolId);

    if (empty($watching)) {
        return;
    }

    $poolLabel = $pool['label'] ?? $poolId;

    foreach ($watching as $symbol => $entry) {
        // Владельца и TTL разрешаем ДО запроса цены: просроченный watch (или watch
        // осиротевшего трейдера) снимаем независимо от доступности цены — иначе
        // символ без цены (делистинг/сбой фида) держал бы протухший watch вечно
        // (паритет с check_wallet_watching, где TTL тоже проверяется до цены).
        $ownerTrader = null;
        foreach ($traders as $t) {
            if ($t['id'] === $entry['trader_id']) {
                $ownerTrader = $t;
                break;
            }
        }

        if ($ownerTrader === null) {
            bot_log("WATCHING POOL [$poolLabel]: trader {$entry['trader_id']} not found, removing $symbol from watch");
            storage_remove_watching('pool', $poolId, $symbol);
            continue;
        }

        // TTL берётся с трейдера-владельца (как и порог просадки) — просроченный watch снимаем.
        $ttlHours = (float) ($ownerTrader['copy_after_drawdown_ttl_hours'] ?? 0);
        if (watching_is_expired($entry, $ttlHours)) {
            storage_remove_watching('pool', $poolId, $symbol);
            bot_log(sprintf('WATCHING POOL [%s] %s (%s) — TTL %.0fh истёк, снимаю watch',
                $poolLabel, $symbol, $entry['trader_label'] ?? $entry['trader_id'], $ttlHours));
            continue;
        }

        $currentPrice = fetch_mid_price($symbol);
        if ($currentPrice === null) {
            continue;
        }

        $drawdownPct = (float) ($ownerTrader['copy_after_drawdown_pct'] ?? 5);

        $upnlPct = calculate_trader_upnl_pct(
            $entry['trader_entry_price'],
            $currentPrice,
            $entry['side'],
            (int) $entry['leverage']
        );

        bot_log(sprintf(
            'WATCHING POOL [%s] %s %s UPNL=%.2f%% threshold=-%.2f%%',
            $poolLabel, $entry['trader_label'] ?? $entry['trader_id'], $symbol, $upnlPct, $drawdownPct
        ));

        if ($upnlPct <= -$drawdownPct) {
            $positions    = load_pool_positions($poolId);
            $balanceState = load_pool_balance($poolId, pool_virtual_balance($pool));

            if (isset($positions[$symbol])) {
                bot_log("WATCHING POOL [$poolLabel]: $symbol already occupied, removing from watch");
                storage_remove_watching('pool', $poolId, $symbol);
                continue;
            }

            $leverage    = resolve_trader_leverage($ownerTrader, $pool);   // персональное плечо трейдера
            $traderCoins = (isset($entry['trader_size_usd']) && $entry['trader_size_usd'] !== null
                            && $entry['trader_entry_price'] > 0)
                ? (float) $entry['trader_size_usd'] / (float) $entry['trader_entry_price']
                : null;

            // Pseudo-signal из watch-записи для общего open_tracked_position.
            $entrySignal = [
                'symbol'            => $symbol,
                'symbol_exchange'   => $entry['symbol_exchange'] ?? $symbol,
                'side'              => $entry['side'],
                'leverage'          => $leverage,
                'source_trade'      => $entry['trade_id'],
                'trader_price'      => $entry['trader_entry_price'],
                'trader_size_coins' => $traderCoins,
            ];

            // Отложенный вход пула = фактическое открытие. Общий путь; owner-метки и
            // open-вебхук задаём здесь (пул работает только через вебхуки, без executor).
            $result = open_tracked_position(
                'pool', $poolId, $positions, $balanceState['balance'],
                $entrySignal, $currentPrice,
                fn(float $bal) => resolve_pool_position_size($ownerTrader, $bal, $pool),
                $leverage, resolve_pool_settings($pool, $ownerTrader),
                ['id' => $entry['trader_id'], 'label' => $entry['trader_label'] ?? $entry['trader_id']],
                pool_webhooks($pool),
                ['id' => 'pool_' . $poolId, 'label' => $poolLabel],
                'drawdown'
            );

            if ($result['status'] !== 'opened') {
                bot_log("WATCHING POOL [$poolLabel]: $symbol triggered but insufficient margin, keeping watch");
                continue;
            }

            storage_remove_watching('pool', $poolId, $symbol);

            // Реальный ордер пула уходит ИМЕННО сейчас (вход состоялся) — паритет
            // реала с симуляцией для drawdown-режима (как у кошелька).
            if ($onTrigger !== null) {
                $onTrigger($entrySignal, $pool, $ownerTrader);
            }

            bot_log(sprintf(
                'WATCHING POOL TRIGGERED [%s]: %s %s @ %.4f | trader_entry=%.4f | UPNL=%.2f%% (<= -%.2f%%) | size=%.2f',
                $poolLabel, $symbol, strtoupper($entry['side']),
                $result['positions'][$symbol]['entry_price'], $entry['trader_entry_price'],
                $upnlPct, $drawdownPct, $result['size_usd']
            ));
        }
    }
}
