<?php
// =============================================================
// EXECUTOR — mock / live signal execution
// =============================================================

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/logger.php';
require_once __DIR__ . '/storage.php';
require_once __DIR__ . '/position_tracker.php';
require_once __DIR__ . '/price_fetcher.php';
require_once __DIR__ . '/webhook_notifier.php';
require_once __DIR__ . '/hl_executor.php';


/**
 * Исполняет массив сигналов для одного кошелька.
 * В зависимости от $settings['executor_mode']: логирует или отправляет на endpoint.
 * Также отслеживает позиции на виртуальном счёте для расчёта PnL (track_position).
 *
 * @param array  $signals     массив сигналов из process_fills_prepare()
 * @param array  $settings    эффективные настройки кошелька
 * @param array  $walletMeta  ['id' => ..., 'label' => ..., 'address' => ...]
 * @param string $entityType  'wallet' (позиции/баланс адресуются этой парой)
 * @param string $entityId    id кошелька
 */
function execute_signals(array $signals, array $settings, array $walletMeta, string $entityType, string $entityId): void
{
    if (empty($signals)) {
        return;
    }

    foreach ($signals as $signal) {
        $isDirect = ($settings['execution_mode'] ?? 'webhook') === 'direct';

        // 1) Бумажный трекинг (симулятор) — ПЕРВЫМ, т.к. именно он решает, не
        // откладывается ли вход в watching (режим copy_after_drawdown). В direct
        // ТОРГОВЫЕ вебхуки симулятору не передаём (реальные ордера идут через
        // биржу — вебхук-бот исполнил бы сигнал второй раз), а информационные
        // (telegram) остаются: README обещает уведомления и в direct.
        $trackSettings = $settings;
        if ($isDirect) {
            $trackSettings['webhooks'] = webhooks_notify_only($settings['webhooks'] ?? []);
        }
        $trackDetail = null;
        $trackResult = track_position($signal, $entityType, $entityId, $trackSettings, $walletMeta, $trackDetail);

        // 2) Отложенный вход по просадке: бумага ушла в watching → реальное
        // исполнение НЕ опережает её. Реальный ордер (direct / executor_url) и
        // open-вебхук уйдут позже, при срабатывании watch (см. check_wallet_watching
        // → execute_real_order). Без этого реал входил немедленно, а симуляция
        // ждала просадку — рассинхрон бумаги и реала.
        if ($trackResult === 'watching') {
            continue;
        }

        // Паритет бумага/реал: бумага закрыла БЕЗ реверс-ноги (не хватило маржи)
        // → реальному исполнению отдаём чистый close, иначе биржа/приёмник
        // открыли бы ногу, которой нет на бумаге.
        $execSignal = $signal;
        if ($trackResult === 'closed_only'
            && ($execSignal['action'] ?? 'auto') !== 'close'
            && empty($trackDetail['reversed'])
        ) {
            $execSignal['action'] = 'close';
        }

        // 3) Реальное исполнение сигнала.
        if ($isDirect) {
            // Частичное закрытие бумаги (partial_close_mode=on) → reduce-only ордер на
            // ту же долю реальной позиции (а не полный marketClose, который закрыл бы
            // её целиком и разошёлся с бумагой). Для остальных статусов действие
            // выводит hl_execute_signal по реальной позиции.
            if ($trackResult === 'closed_partial') {
                hl_partial_close_position($execSignal, $settings, $walletMeta, $entityType, $entityId, (float) ($trackDetail['fraction'] ?? 0));
            } else {
                hl_execute_signal($execSignal, $settings, $walletMeta, $entityType, $entityId);
            }
            record_signal($signal, 'direct', $walletMeta);
        } elseif ($settings['executor_mode'] === 'live') {
            execute_live($execSignal, $settings, $walletMeta);
        } else {
            execute_mock($execSignal, $walletMeta);
        }

        // 4) Webhook — независимый потребитель сигнала (открытие позиции).
        // Закрытие уходит из position_tracker после расчёта PnL. В direct список
        // уже отфильтрован до информационных (telegram) — см. шаг 1.
        $openWebhooks = $trackSettings['webhooks'] ?? [];
        if (!empty($openWebhooks) && $trackResult === 'opened') {
            $currentPrice = fetch_mid_price($signal['symbol']);
            // Берём фактический размер из уже открытой позиции, а не пересчитываем заново.
            // Повторный resolve_position_size_usd() дал бы другое число для percent_balance
            // и proportional (баланс/доля уже изменились), плюс лишний запрос equity трейдера.
            $openPositions = load_positions($entityType, $entityId);
            $openedSizeUsd = $openPositions[$signal['symbol']]['size_usd'] ?? 0.0;
            webhook_notify_open(
                $openWebhooks, $signal,
                $walletMeta['id'], $walletMeta['label'] ?? $walletMeta['id'],
                (float) $openedSizeUsd, $currentPrice
            );
        }
    }
}

/**
 * Реальное исполнение ОДНОГО сигнала (direct / live executor_url / mock) без
 * бумажного трекинга и без open-вебхука. Нужен для отложенного входа по просадке:
 * при срабатывании watch бумага и open-вебхук уже отправлены в check_wallet_watching,
 * а реальный ордер должен уйти ровно в этот момент (а не при первом сигнале) —
 * иначе реал опережал бы симуляцию. Вызывается как колбэк из check_wallet_watching.
 *
 * Только для кошельков (entity_type='wallet'); пулы работают исключительно через
 * вебхуки и реального исполнения не имеют.
 */
function execute_real_order(array $signal, array $settings, array $walletMeta): void
{
    if (($settings['execution_mode'] ?? 'webhook') === 'direct') {
        hl_execute_signal($signal, $settings, $walletMeta, 'wallet', $walletMeta['id']);
        record_signal($signal, 'direct', $walletMeta);
        return;
    }
    if (($settings['executor_mode'] ?? 'mock') === 'live') {
        execute_live($signal, $settings, $walletMeta);   // POST на executor_url (+ record 'live')
        return;
    }
    execute_mock($signal, $walletMeta);   // лог + record 'mock' (как в обычном пути)
}

/**
 * Записывает сигнал в SQLite (таблица signals) для последующего анализа.
 * Формат: datetime, wallet_id, wallet_label, symbol, side, size, leverage, trade_id, mode
 */
function record_signal(array $signal, string $mode, array $walletMeta): void
{
    storage_insert_signal($signal, $walletMeta, $mode);
}

/**
 * MOCK MODE — логирует сигнал, ничего не отправляет.
 */
function execute_mock(array $signal, array $walletMeta): void
{
    $line = sprintf(
        '[%s] [SIGNAL] %s %s [%s] size=%.6f leverage=x%d trade_id=%s',
        $walletMeta['label'] ?? ($walletMeta['id'] ?? '?'),
        $signal['symbol'],
        strtoupper($signal['side']),
        $signal['action'] ?? 'auto',
        $signal['size'],
        $signal['leverage'],
        $signal['source_trade']
    );

    bot_log($line);
    echo $line . PHP_EOL;

    record_signal($signal, 'mock', $walletMeta);
}

/**
 * LIVE MODE — отправляет сигнал на execution endpoint.
 * Endpoint должен принимать POST JSON и возвращать HTTP 200.
 */
function execute_live(array $signal, array $settings, array $walletMeta): void
{
    $url = $settings['executor_url'] ?? '';

    // SSRF-защита (та же, что у webhook): только публичный http/https хост, с
    // проверкой КАЖДОГО резолвящегося IP. Проверенные IP пиним в curl, иначе
    // curl резолвил бы DNS заново и проверка от rebinding была бы бесполезна.
    // На небезопасный URL ордер НЕ отправляем (пропуск + лог) — так же дефолтный
    // placeholder из config (your-executor-service) не зарезолвится и не уйдёт.
    if (!webhook_url_is_safe($url, $reason, $pinnedIps)) {
        bot_log('EXECUTOR: небезопасный executor_url (SSRF-защита: ' . $reason . '), пропуск ордера: ' . $url);
        return;
    }

    $payload = json_encode([
        'symbol'        => $signal['symbol'],
        'side'          => $signal['side'],
        'action'        => $signal['action'] ?? 'auto',  // open | close | reverse | auto
        'size'          => $signal['size'],
        'leverage'      => $signal['leverage'],
        'source_wallet' => $walletMeta['address'] ?? '',
    ]);

    // Тот же транспорт, что у вебхуков (SSRF-пиннинг + запрет редиректов/протоколов),
    // переиспользуем webhook_http_request вместо собственного curl-блока. Таймаут —
    // API_TIMEOUT_SEC (как было у executor), а не дефолтные 10с вебхука.
    $result    = webhook_http_request($url, $payload, 'POST', [], $pinnedIps, API_TIMEOUT_SEC);
    $response  = $result['response'];
    $httpCode  = $result['http_code'];
    $curlError = $result['error'];

    if ($curlError) {
        bot_log('EXECUTOR ERROR: ' . $curlError . ' | signal: ' . $payload);
        return;
    }

    if ($httpCode !== 200) {
        bot_log('EXECUTOR HTTP ' . $httpCode . ' | response: ' . $response . ' | signal: ' . $payload);
        return;
    }

    bot_log('EXECUTED: ' . $signal['symbol'] . ' ' . strtoupper($signal['side']) . ' size=' . $signal['size']);
    record_signal($signal, 'live', $walletMeta);
}
