<?php
// =============================================================
// DASHBOARD/LIB/FORMS — чистые парсеры и хелперы форм
// =============================================================
// Логика разбора POST-данных, отделённая от HTML-view. Чистые функции
// (без вывода и, где возможно, без I/O) — тестируются в tests/dashboard_test.php.

require_once __DIR__ . '/../../config.php';

// ---------------------------------------------------------------
// Per-wallet lock — защита сброса от гонки с циклом бота
// ---------------------------------------------------------------

/**
 * Берёт per-wallet flock (тот же файл, который bot.php держит на время
 * обработки кошелька) с короткими ретраями. Возвращает ресурс — держать до
 * конца операции и отдать в dashboard_release_wallet_lock() — или null, если
 * бот прямо сейчас обрабатывает кошелёк. Сбрасывать state без лока нельзя:
 * бот закоммитил бы поверх сброса старый watermark, и сброс молча «не сработал».
 */
function dashboard_acquire_wallet_lock(string $walletId, float $timeoutSec = 3.0)
{
    $fp = @fopen(wallet_lock_file($walletId), 'c');
    if (!$fp) {
        return null;
    }
    $deadline = microtime(true) + $timeoutSec;
    while (true) {
        if (flock($fp, LOCK_EX | LOCK_NB)) {
            return $fp;
        }
        if (microtime(true) >= $deadline) {
            fclose($fp);
            return null;
        }
        usleep(100_000);
    }
}

/** Освобождает lock, взятый dashboard_acquire_wallet_lock(). */
function dashboard_release_wallet_lock($fp): void
{
    if (is_resource($fp)) {
        flock($fp, LOCK_UN);
        fclose($fp);
    }
}

// ---------------------------------------------------------------
// Балансы — единый литерал «свежего» виртуального счёта
// ---------------------------------------------------------------

/**
 * Свежее состояние виртуального счёта кошелька на стартовом балансе.
 * Дедуп литерала, который раньше был выписан в wallets.php (add/import/reset)
 * и settings.php (reset_all).
 */
function dashboard_fresh_wallet_balance(float $startBalance): array
{
    return [
        'balance'          => $startBalance,
        'peak_balance'     => $startBalance,
        'max_drawdown_pct' => 0.0,
        'total_gross_pnl'  => 0.0,
        'total_net_pnl'    => 0.0,
        'total_fees'       => 0.0,
        'total_funding'    => 0.0,
        'trades_count'     => 0,
        'wins_count'       => 0,
        'gross_profit_sum' => 0.0,
        'gross_loss_sum'   => 0.0,
    ];
}

/**
 * Свежее состояние баланса пула (структура аналогична кошельку).
 */
function fresh_pool_balance(float $virtualBalance): array
{
    return [
        'balance'          => $virtualBalance,
        'peak_balance'     => $virtualBalance,
        'max_drawdown_pct' => 0.0,
        'total_gross_pnl'  => 0.0,
        'total_net_pnl'    => 0.0,
        'total_fees'       => 0.0,
        // total_funding обязателен: колонка wallet_balances.total_funding —
        // NOT NULL, без неё storage_upsert_balance_row падал с PDOException
        // при создании/сбросе пула.
        'total_funding'    => 0.0,
        'trades_count'     => 0,
        'wins_count'       => 0,
        'gross_profit_sum' => 0.0,
        'gross_loss_sum'   => 0.0,
    ];
}

// ---------------------------------------------------------------
// id-генераторы
// ---------------------------------------------------------------

function generate_pool_id(array $existingPools): string
{
    $existing = array_column($existingPools, 'id');
    $i = 1;
    do {
        $id = 'p' . $i;
        $i++;
    } while (in_array($id, $existing, true));
    return $id;
}

function generate_trader_id(array $existingTraders): string
{
    $existing = array_column($existingTraders, 'id');
    $i = 1;
    do {
        $id = 't' . $i;
        $i++;
    } while (in_array($id, $existing, true));
    return $id;
}

// ---------------------------------------------------------------
// Форматирование значений для полей форм
// ---------------------------------------------------------------

function fmt_val($value): string
{
    return $value !== null ? (string) $value : '';
}

/**
 * Форматирует значение override для отображения в текстовом поле.
 */
function fmt_override($value): string
{
    if (is_array($value)) {
        if (empty($value)) return '';
        if (isset($value[0])) return implode(', ', $value); // allowed_symbols (list)
        $parts = [];
        foreach ($value as $k => $v) $parts[] = "$k=$v";
        return implode(', ', $parts); // symbol_leverage (map)
    }
    return $value === null ? '' : (string) $value;
}

// ---------------------------------------------------------------
// Валидация executor_url (UI-уровень, анти-SSRF предохранитель)
// ---------------------------------------------------------------

/**
 * Допустим ли executor_url для сохранения: только внешние http/https адреса.
 * Это UI-валидация (быстрый отказ в форме); реальная SSRF-защита с резолвом и
 * пиннингом IP живёт в executor.php/webhook_notifier на момент запроса.
 * Вызывается только для непустого URL — пустоту обрабатывает вызывающий код.
 */
function dashboard_executor_url_is_valid(string $url): bool
{
    $parsed = parse_url($url);
    $scheme = strtolower($parsed['scheme'] ?? '');
    $host   = strtolower($parsed['host'] ?? '');

    if (!in_array($scheme, ['http', 'https'], true)) return false;
    if ($host === '') return false;

    $isPrivateHost = (
        $host === 'localhost' ||
        preg_match('/^127\./', $host) ||
        preg_match('/^10\./', $host) ||
        preg_match('/^192\.168\./', $host) ||
        preg_match('/^172\.(1[6-9]|2\d|3[01])\./', $host) ||
        $host === '0.0.0.0' ||
        $host === '::1'
    );

    return !$isPrivateHost;
}

// ---------------------------------------------------------------
// Парсинг полей форм кошелька
// ---------------------------------------------------------------

function parse_symbols_field(string $raw): array
{
    $raw = trim($raw);
    if ($raw === '') return [];
    $parts = preg_split('/[\s,]+/', strtoupper($raw));
    return array_values(array_filter(array_map('trim', $parts)));
}

function parse_leverage_field(string $raw): array
{
    $raw = trim($raw);
    if ($raw === '') return [];
    $result = [];
    foreach (preg_split('/[\s,]+/', $raw) as $pair) {
        if (strpos($pair, '=') === false) continue;
        [$sym, $lev] = explode('=', $pair, 2);
        $sym = trim(strtoupper($sym));
        $lev = (int) trim($lev);
        if ($sym !== '' && $lev > 0) {
            $result[$sym] = $lev;
        }
    }
    return $result;
}

/**
 * Парсит overrides из POST для одного кошелька.
 * Каждое поле: если оставлено пустым -> null (использовать глобальное значение).
 * Для allowed_symbols/symbol_leverage используется отдельный чекбокс
 * "override_*", чтобы отличить "не задано" (null) от "пусто намеренно" ([]).
 */
function parse_wallet_overrides(array $post, string $prefix): array
{
    $overrides = [];

    if (!empty($post[$prefix . 'override_symbols'])) {
        $overrides['allowed_symbols'] = parse_symbols_field($post[$prefix . 'allowed_symbols'] ?? '');
    } else {
        $overrides['allowed_symbols'] = null;
    }

    $scaleFactor = trim($post[$prefix . 'scale_factor'] ?? '');
    $overrides['scale_factor'] = $scaleFactor === '' ? null : (float) $scaleFactor;

    $defaultLeverage = trim($post[$prefix . 'default_leverage'] ?? '');
    $overrides['default_leverage'] = $defaultLeverage === '' ? null : max(1, (int) $defaultLeverage);

    if (!empty($post[$prefix . 'override_symbol_leverage'])) {
        $overrides['symbol_leverage'] = parse_leverage_field($post[$prefix . 'symbol_leverage'] ?? '');
    } else {
        $overrides['symbol_leverage'] = null;
    }

    $cooldown = trim($post[$prefix . 'cooldown_seconds'] ?? '');
    $overrides['cooldown_seconds'] = $cooldown === '' ? null : max(0, (int) $cooldown);

    $pollInterval = trim($post[$prefix . 'poll_interval_minutes'] ?? '');
    $overrides['poll_interval_minutes'] = $pollInterval === '' ? null : max(0, (float) $pollInterval);

    $tgNotify = $post[$prefix . 'telegram_notify'] ?? '';
    $overrides['telegram_notify'] = $tgNotify === '' ? null : ($tgNotify === '1');

    $executorMode = $post[$prefix . 'executor_mode'] ?? '';
    $overrides['executor_mode'] = ($executorMode === 'mock' || $executorMode === 'live') ? $executorMode : null;

    $executorUrl = trim($post[$prefix . 'executor_url'] ?? '');
    if ($executorUrl !== '' && !dashboard_executor_url_is_valid($executorUrl)) {
        $executorUrl = ''; // Игнорируем невалидный URL, не сохраняем
    }
    $overrides['executor_url'] = $executorUrl === '' ? null : $executorUrl;

    // --- Симулятор копитрейдинга ---
    $virtualBalance = trim($post[$prefix . 'virtual_balance'] ?? '');
    $overrides['virtual_balance'] = $virtualBalance === '' ? null : max(0, (float) $virtualBalance);

    $sizingMode = $post[$prefix . 'sizing_mode'] ?? '';
    $overrides['sizing_mode'] = in_array($sizingMode, ['fixed', 'percent_balance', 'proportional'], true) ? $sizingMode : null;

    $sizingFixedUsd = trim($post[$prefix . 'sizing_fixed_usd'] ?? '');
    $overrides['sizing_fixed_usd'] = $sizingFixedUsd === '' ? null : max(0, (float) $sizingFixedUsd);

    $sizingPercent = trim($post[$prefix . 'sizing_percent'] ?? '');
    $overrides['sizing_percent'] = $sizingPercent === '' ? null : max(0, min(100, (float) $sizingPercent));

    // Лимиты объёма НАШЕЙ позиции (нотионал); приоритет над sizing_mode
    $positionMinUsd = trim($post[$prefix . 'position_min_usd'] ?? '');
    $overrides['position_min_usd'] = $positionMinUsd === '' ? null : max(0, (float) $positionMinUsd);

    $positionMaxUsd = trim($post[$prefix . 'position_max_usd'] ?? '');
    $overrides['position_max_usd'] = $positionMaxUsd === '' ? null : max(0, (float) $positionMaxUsd);

    $feePct = trim($post[$prefix . 'fee_pct_per_side'] ?? '');
    $overrides['fee_pct_per_side'] = $feePct === '' ? null : max(0, (float) $feePct);

    // Слиппедж и funding: раньше задавались только глобально, хотя оба ключа
    // есть в GLOBAL_SETTINGS_DEFAULTS и resolve_wallet_settings их принимает.
    // Каталог настроек показывает их в карточке трейдера — без разбора здесь
    // введённое значение молча терялось бы при сохранении.
    $slipPct = trim($post[$prefix . 'slippage_pct_per_side'] ?? '');
    $overrides['slippage_pct_per_side'] = $slipPct === '' ? null : max(0, (float) $slipPct);

    $fundingPct = trim($post[$prefix . 'funding_rate_hourly_pct'] ?? '');
    $overrides['funding_rate_hourly_pct'] = $fundingPct === '' ? null : (float) $fundingPct;

    $drawdownEnabled = $post[$prefix . 'copy_after_drawdown_enabled'] ?? '';
    $overrides['copy_after_drawdown_enabled'] = $drawdownEnabled === '' ? null : ($drawdownEnabled === '1');

    $drawdownPct = trim($post[$prefix . 'copy_after_drawdown_pct'] ?? '');
    $overrides['copy_after_drawdown_pct'] = $drawdownPct === '' ? null : max(0, min(100, (float) $drawdownPct));

    $drawdownTtl = trim($post[$prefix . 'copy_after_drawdown_ttl_hours'] ?? '');
    $overrides['copy_after_drawdown_ttl_hours'] = $drawdownTtl === '' ? null : max(0, (float) $drawdownTtl);

    // --- Прямая торговля на Hyperliquid (PRD §3-10) ---
    $execMode = $post[$prefix . 'execution_mode'] ?? '';
    $overrides['execution_mode'] = in_array($execMode, ['webhook', 'direct'], true) ? $execMode : null;

    $marginMode = $post[$prefix . 'hl_margin_mode'] ?? '';
    $overrides['hl_margin_mode'] = in_array($marginMode, ['cross', 'isolated'], true) ? $marginMode : null;

    // Субаккаунт для direct-ордеров: валидный 0x-адрес → используем; пусто/мусор →
    // null (наследуем глобальное, т.е. торговля на основном аккаунте).
    $subAcc = trim((string) ($post[$prefix . 'hl_subaccount'] ?? ''));
    $overrides['hl_subaccount'] = preg_match('/^0x[0-9a-fA-F]{40}$/', $subAcc) ? strtolower($subAcc) : null;

    // Stop Loss / Take Profit / сетка: для кошелька переопределяются целым блоком
    // по чекбоксу (отмечен → берём значения, иначе null = наследовать глобальное).
    // Сам разбор значений — в общих парсерах (их же использует карточка трейдера пула).
    $overrides['stop_loss']   = !empty($post[$prefix . 'override_sl'])   ? parse_protective_config($post, $prefix, 'sl') : null;
    $overrides['take_profit'] = !empty($post[$prefix . 'override_tp'])   ? parse_protective_config($post, $prefix, 'tp') : null;
    $overrides['grid']        = !empty($post[$prefix . 'override_grid']) ? parse_grid_config($post, $prefix) : null;

    return $overrides;
}

/**
 * Парсит массовое изменение настроек кошельков из POST (bulk_wallets, op=edit).
 * Поле применяется только если отмечен его чекбокс bulk_apply_<поле>; значения
 * разбираются ТЕМИ ЖЕ правилами, что и per-wallet форма (parse_wallet_overrides
 * с префиксом bulk_), поэтому пустое значение отмеченного поля = null =
 * вернуть кошельку наследование глобальной настройки.
 *
 * Теги: bulk_tags_mode = ''|add|remove|replace + список bulk_tags[]
 * (фильтруется по $availableTags, как в update_wallet).
 *
 * Профильные (wallet-level) поля — status/rating — идут отдельным ключом
 * 'profile': это свойства кошелька, а не settings-overrides.
 *
 * Возвращает:
 *   ['settings'  => [ключ => значение|null] — только отмеченные поля,
 *    'profile'   => ['status' => ..., 'rating' => ...] — только отмеченные,
 *    'tags_mode' => ''|'add'|'remove'|'replace',
 *    'tags'      => string[]]
 */
function parse_bulk_wallet_edits(array $post, array $availableTags): array
{
    $editableFields = [
        'poll_interval_minutes', 'cooldown_seconds', 'scale_factor', 'default_leverage',
        'fee_pct_per_side', 'slippage_pct_per_side', 'funding_rate_hourly_pct',
        'executor_mode', 'executor_url',
        'execution_mode', 'hl_margin_mode', 'hl_subaccount',
        'telegram_notify', 'virtual_balance', 'sizing_mode', 'sizing_fixed_usd', 'sizing_percent',
        'position_min_usd', 'position_max_usd',
        'copy_after_drawdown_enabled', 'copy_after_drawdown_pct', 'copy_after_drawdown_ttl_hours',
        'allowed_symbols', 'symbol_leverage',
    ];

    // allowed_symbols/symbol_leverage в parse_wallet_overrides активируются
    // чекбоксом override_*; в bulk-панели этим чекбоксом служит сам
    // bulk_apply_* — транслируем.
    if (!empty($post['bulk_apply_allowed_symbols'])) {
        $post['bulk_override_symbols'] = '1';
    }
    if (!empty($post['bulk_apply_symbol_leverage'])) {
        $post['bulk_override_symbol_leverage'] = '1';
    }

    $parsed   = parse_wallet_overrides($post, 'bulk_');
    $settings = [];
    foreach ($editableFields as $field) {
        if (!empty($post['bulk_apply_' . $field])) {
            $settings[$field] = $parsed[$field];
        }
    }

    // Профильные поля кошелька (не settings)
    $profile = [];
    if (!empty($post['bulk_apply_status'])) {
        $status = $post['bulk_status'] ?? '';
        if (in_array($status, ['WATCH', 'TEST', 'ACTIVE', 'ARCHIVE'], true)) {
            $profile['status'] = $status;
        }
    }
    if (!empty($post['bulk_apply_rating'])) {
        $profile['rating'] = max(0, min(5, (int) ($post['bulk_rating'] ?? 0)));
    }

    $mode = $post['bulk_tags_mode'] ?? '';
    if (!in_array($mode, ['add', 'remove', 'replace'], true)) {
        $mode = '';
    }
    $tags = array_values(array_intersect(
        (array) ($post['bulk_tags'] ?? []),
        $availableTags
    ));

    return ['settings' => $settings, 'profile' => $profile, 'tags_mode' => $mode, 'tags' => $tags];
}

/**
 * Разбор полей сетки из $post по префиксу → конфиг grid (всегда с ключом enabled).
 * Общий для кошелька (внутри override-блока) и трейдера пула (напрямую).
 */
function parse_grid_config(array $post, string $prefix): array
{
    $dist = $post[$prefix . 'grid_distribution'] ?? 'equal';
    return [
        'enabled'      => !empty($post[$prefix . 'grid_enabled']),
        'levels'       => max(0, (int) ($post[$prefix . 'grid_levels'] ?? 0)),
        'step_pct'     => max(0, (float) ($post[$prefix . 'grid_step_pct'] ?? 0)),
        'size_factor'  => max(0, (float) ($post[$prefix . 'grid_size_factor'] ?? 1)),
        'distribution' => in_array($dist, ['equal', 'multiplicative'], true) ? $dist : 'equal',
    ];
}

/**
 * Разбор полей защитного ордера (SL/TP) из $post по префиксу. $field = 'sl'|'tp'.
 * Общий для кошелька и трейдера пула.
 */
function parse_protective_config(array $post, string $prefix, string $field): array
{
    $type = $post[$prefix . $field . '_type'] ?? 'pnl_pct';
    return [
        'enabled' => !empty($post[$prefix . $field . '_enabled']),
        'type'    => in_array($type, ['pnl_pct', 'price_pct', 'price'], true) ? $type : 'pnl_pct',
        'value'   => max(0, (float) ($post[$prefix . $field . '_value'] ?? 0)),
    ];
}

// ---------------------------------------------------------------
// Парсинг webhook-конфигов (пулы)
// ---------------------------------------------------------------

/**
 * Парсит webhook-конфиги из POST-данных.
 * Поддерживает до 3 webhooks с заданным префиксом.
 */
function parse_webhooks_from_post(array $post, string $prefix): array
{
    $webhooks = [];
    for ($i = 0; $i < 3; $i++) {
        $p      = $prefix . $i . '_';
        $service = $post[$p . 'service'] ?? '';
        if (!$service) continue;

        $wh = [
            'enabled' => !empty($post[$p . 'enabled']),
            'service' => $service,
        ];

        switch ($service) {
            case 'katoshi':
                $wh['api_key'] = trim($post[$p . 'api_key'] ?? '');
                $wh['bot_id']  = (int) ($post[$p . 'bot_id'] ?? 0);
                break;
            case 'altrady':
                $wh['api_key']       = trim($post[$p . 'api_key'] ?? '');
                $wh['api_secret']    = trim($post[$p . 'api_secret'] ?? '');
                $wh['exchange']      = trim($post[$p . 'exchange'] ?? 'HYPE');
                $wh['symbol_prefix'] = trim($post[$p . 'symbol_prefix'] ?? 'HYPE_USDT_');
                break;
            case 'telegram':
                $wh['bot_token'] = trim($post[$p . 'bot_token'] ?? '');
                $wh['chat_id']   = trim($post[$p . 'chat_id'] ?? '');
                break;
            case 'custom':
                $wh['url']           = trim($post[$p . 'url'] ?? '');
                $wh['method']        = in_array($post[$p . 'method'] ?? 'POST', ['POST', 'GET'], true) ? $post[$p . 'method'] : 'POST';
                $wh['body_template'] = trim($post[$p . 'body_template'] ?? '');
                // Заголовки: передаются как "Key: Value" по одному в строке
                $headersRaw = trim($post[$p . 'headers_raw'] ?? '');
                $headers = [];
                foreach (explode("\n", $headersRaw) as $line) {
                    $line = trim($line);
                    if (strpos($line, ':') !== false) {
                        [$k, $v] = explode(':', $line, 2);
                        $headers[trim($k)] = trim($v);
                    }
                }
                $wh['headers'] = $headers;
                break;
        }

        $webhooks[] = $wh;
    }
    return $webhooks;
}
