<?php
// =============================================================
// DASHBOARD/LIB/TRADERS_XLSX — экспорт/импорт настроек трейдеров в XLSX
// =============================================================
// Один лист: строка = трейдер. Колонки профиля (id/label/address/…) +
// колонка на каждый override-параметр из карточки трейдера.
//
// Семантика ячеек настроек — та же, что в UI (parse_wallet_overrides):
//   пустая ячейка  = null = наследовать глобальную настройку;
//   значение       = per-trader override (валидируется теми же правилами).
// Вложенные конфиги (stop_loss/take_profit/grid) — JSON-строкой.
// Ключи настроек, которых нет в колонках файла (например webhooks),
// при импорте НЕ трогаются.

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

// ---------------------------------------------------------------
// Спецификация колонок
// ---------------------------------------------------------------

/** Колонки профиля трейдера (свойства кошелька, не settings). */
function traders_xlsx_profile_fields(): array
{
    return ['id', 'label', 'address', 'enabled', 'status', 'rating', 'tags', 'notes', 'source'];
}

/**
 * Колонки настроек: ключ => тип конвертера.
 * Набор и правила валидации зеркалят parse_wallet_overrides (per-trader UI).
 */
function traders_xlsx_setting_fields(): array
{
    return [
        'allowed_symbols'               => 'symbols',
        'scale_factor'                  => 'float',
        'default_leverage'              => 'int_min1',
        'symbol_leverage'               => 'leverage_map',
        'cooldown_seconds'              => 'int_min0',
        'poll_interval_minutes'         => 'float_min0',
        'telegram_notify'               => 'bool',
        'executor_mode'                 => 'enum:mock,live',
        'executor_url'                  => 'executor_url',
        'execution_mode'                => 'enum:webhook,direct',
        'hl_margin_mode'                => 'enum:cross,isolated',
        'hl_subaccount'                 => 'hl_address',
        'virtual_balance'               => 'float_min0',
        'sizing_mode'                   => 'enum:fixed,percent_balance,proportional',
        'sizing_fixed_usd'              => 'float_min0',
        'sizing_percent'                => 'float_0_100',
        'position_min_usd'              => 'float_min0',
        'position_max_usd'              => 'float_min0',
        'fee_pct_per_side'              => 'float_min0',
        'copy_after_drawdown_enabled'   => 'bool',
        'copy_after_drawdown_pct'       => 'float_0_100',
        'copy_after_drawdown_ttl_hours' => 'float_min0',
        'stop_loss'                     => 'protective',
        'take_profit'                   => 'protective',
        'grid'                          => 'grid',
    ];
}

/** Полный заголовок листа (порядок колонок при экспорте). */
function traders_xlsx_header(): array
{
    return array_merge(traders_xlsx_profile_fields(), array_keys(traders_xlsx_setting_fields()));
}

// ---------------------------------------------------------------
// Экспорт
// ---------------------------------------------------------------

/** Собирает строки листа (заголовок + трейдеры) из конфига кошельков. */
function traders_xlsx_export_rows(array $config): array
{
    $rows = [traders_xlsx_header()];

    foreach ($config['wallets'] as $w) {
        $settings = is_array($w['settings'] ?? null) ? $w['settings'] : [];
        $row = [
            (string) ($w['id'] ?? ''),
            (string) ($w['label'] ?? ''),
            (string) ($w['address'] ?? ''),
            !empty($w['enabled']) ? 1 : 0,
            (string) ($w['status'] ?? 'WATCH'),
            (int) ($w['rating'] ?? 0),
            implode(', ', $w['tags'] ?? []),
            (string) ($w['notes'] ?? ''),
            (string) ($w['source'] ?? ''),
        ];
        foreach (traders_xlsx_setting_fields() as $key => $type) {
            $row[] = traders_xlsx_format_setting($settings[$key] ?? null, $type);
        }
        $rows[] = $row;
    }

    return $rows;
}

/** Значение override → представление в ячейке ('' = null = наследование). */
function traders_xlsx_format_setting($value, string $type)
{
    if ($value === null) return '';

    switch ($type) {
        case 'bool':
            return $value ? 1 : 0;
        case 'symbols':
            return implode(', ', (array) $value);
        case 'leverage_map':
            $parts = [];
            foreach ((array) $value as $sym => $lev) $parts[] = $sym . '=' . $lev;
            return implode(', ', $parts);
        case 'protective':
        case 'grid':
            return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        default:
            // Числа отдаём числами (Excel), enum/url — строками
            return is_int($value) || is_float($value) ? $value : (string) $value;
    }
}

/** Пишет xlsx экспорта во временный файл, возвращает путь или null. */
function traders_xlsx_export_file(array $config): ?string
{
    $file = tempnam(sys_get_temp_dir(), 'traders_xlsx_');
    if ($file === false) return null;

    $header = traders_xlsx_header();
    $widths = [];
    foreach ($header as $i => $name) {
        $widths[$i] = match ($name) {
            'id'       => 6,
            'address', 'hl_subaccount' => 44,
            'label', 'tags', 'notes', 'source', 'executor_url' => 24,
            'stop_loss', 'take_profit', 'grid' => 40,
            default    => max(12, strlen($name) + 2),
        };
    }

    if (!xlsx_write($file, traders_xlsx_export_rows($config), 'Traders', $widths)) {
        @unlink($file);
        return null;
    }
    return $file;
}

// ---------------------------------------------------------------
// Импорт: конвертеры значений ячеек
// ---------------------------------------------------------------

/** '1'/'true'/'on'/'yes'/'да' → true; '0'/'false'/'off'/'no'/'нет' → false; иначе null. */
function traders_xlsx_parse_bool(string $raw): ?bool
{
    $v = mb_strtolower(trim($raw));
    if (in_array($v, ['1', 'true', 'on', 'yes', 'да'], true))  return true;
    if (in_array($v, ['0', 'false', 'off', 'no', 'нет'], true)) return false;
    return null;
}

/**
 * Ячейка настройки → значение override ('' → null = наследовать).
 * Возвращает [ok(bool), value] — ok=false, если значение непустое, но
 * невалидное (поле пропускается, строка получает предупреждение).
 */
function traders_xlsx_parse_setting(string $raw, string $type): array
{
    $raw = trim($raw);
    if ($raw === '') return [true, null];

    if (str_starts_with($type, 'enum:')) {
        $allowed = explode(',', substr($type, 5));
        return in_array($raw, $allowed, true) ? [true, $raw] : [false, null];
    }

    switch ($type) {
        case 'bool':
            $b = traders_xlsx_parse_bool($raw);
            return $b === null ? [false, null] : [true, $b];

        case 'float':
            return is_numeric($raw) ? [true, (float) $raw] : [false, null];
        case 'float_min0':
            return is_numeric($raw) ? [true, max(0, (float) $raw)] : [false, null];
        case 'float_0_100':
            return is_numeric($raw) ? [true, max(0, min(100, (float) $raw))] : [false, null];
        case 'int_min0':
            return is_numeric($raw) ? [true, max(0, (int) $raw)] : [false, null];
        case 'int_min1':
            return is_numeric($raw) ? [true, max(1, (int) $raw)] : [false, null];

        case 'symbols':
            return [true, parse_symbols_field($raw)];

        case 'leverage_map':
            $map = parse_leverage_field($raw);
            // Непустая ячейка без единой валидной пары — явно битый формат
            return empty($map) ? [false, null] : [true, $map];

        case 'executor_url':
            return dashboard_executor_url_is_valid($raw) ? [true, $raw] : [false, null];

        case 'hl_address':
            return preg_match('/^0x[0-9a-fA-F]{40}$/', $raw)
                ? [true, strtolower($raw)] : [false, null];

        case 'protective': {
            $d = json_decode($raw, true);
            if (!is_array($d)) return [false, null];
            $ptype = $d['type'] ?? 'pnl_pct';
            return [true, [
                'enabled' => !empty($d['enabled']),
                'type'    => in_array($ptype, ['pnl_pct', 'price_pct', 'price'], true) ? $ptype : 'pnl_pct',
                'value'   => max(0, (float) ($d['value'] ?? 0)),
            ]];
        }

        case 'grid': {
            $d = json_decode($raw, true);
            if (!is_array($d)) return [false, null];
            $dist = $d['distribution'] ?? 'equal';
            return [true, [
                'enabled'      => !empty($d['enabled']),
                'levels'       => max(0, (int) ($d['levels'] ?? 0)),
                'step_pct'     => max(0, (float) ($d['step_pct'] ?? 0)),
                'size_factor'  => max(0, (float) ($d['size_factor'] ?? 1)),
                'distribution' => in_array($dist, ['equal', 'multiplicative'], true) ? $dist : 'equal',
            ]];
        }
    }

    return [false, null];
}

// ---------------------------------------------------------------
// Импорт: применение строк к конфигу (чистая функция — тестируется)
// ---------------------------------------------------------------

/**
 * Применяет строки xlsx к конфигу кошельков.
 *
 * Матчинг: по id, затем по address (без регистра). Строка с неизвестным
 * валидным адресом создаёт нового трейдера (если $createNew). Колонки
 * матчатся по заголовку — порядок и подмножество произвольные.
 *
 * НЕ сохраняет конфиг и НЕ трогает БД — это делает вызывающий код.
 *
 * @return array ['config' => обновлённый конфиг,
 *                'updated' => int, 'created' => int, 'skipped' => int,
 *                'created_ids' => string[],  // для инициализации виртуальных счетов
 *                'warnings' => string[]]
 */
function traders_xlsx_apply_rows(array $rows, array $config, bool $createNew = true): array
{
    $wallets       = $config['wallets'];
    $availableTags = $config['available_tags'] ?? [];
    $settingFields = traders_xlsx_setting_fields();
    $knownColumns  = traders_xlsx_header();

    $stats = ['updated' => 0, 'created' => 0, 'skipped' => 0, 'created_ids' => [], 'warnings' => []];
    $warn  = function (int $rowNum, string $msg) use (&$stats) {
        $stats['warnings'][] = 'строка ' . $rowNum . ': ' . $msg;
    };

    if (empty($rows)) {
        $stats['warnings'][] = 'файл пуст';
        return ['config' => $config] + $stats;
    }

    // Заголовок → карта колонок (только известные имена)
    $colMap = [];
    foreach ($rows[0] as $i => $name) {
        $name = trim((string) $name);
        if (in_array($name, $knownColumns, true)) {
            $colMap[$name] = $i;
        }
    }
    if (!isset($colMap['id']) && !isset($colMap['address'])) {
        $stats['warnings'][] = 'в заголовке нет ни колонки id, ни address — нечем сопоставить строки с трейдерами';
        return ['config' => $config] + $stats;
    }

    // Индексы для матчинга
    $byId = [];
    $byAddress = [];
    foreach ($wallets as $i => $w) {
        $byId[$w['id']] = $i;
        if (!empty($w['address'])) {
            $byAddress[strtolower($w['address'])] = $i;
        }
    }

    $cell = fn(array $row, string $col) => isset($colMap[$col]) ? trim((string) ($row[$colMap[$col]] ?? '')) : null;

    for ($r = 1; $r < count($rows); $r++) {
        $row    = $rows[$r];
        $rowNum = $r + 1; // человекочитаемый номер строки в файле

        // Полностью пустые строки молча пропускаем
        if (trim(implode('', array_map('strval', $row))) === '') continue;

        $id      = $cell($row, 'id');
        $address = $cell($row, 'address');

        // --- матчинг с существующим трейдером ---
        $idx = null;
        if ($id !== null && $id !== '' && isset($byId[$id])) {
            $idx = $byId[$id];
        } elseif ($address !== null && $address !== '' && isset($byAddress[strtolower($address)])) {
            $idx = $byAddress[strtolower($address)];
        }

        // --- новый трейдер ---
        // Сюда попадают и строки с незнакомым id (файл от чистой установки
        // или другого инстанса): восстанавливаем трейдера, сохраняя id из
        // файла — история сделок в БД привязана к id, бэкап переносится 1:1.
        if ($idx === null) {
            if (!$createNew) {
                $stats['skipped']++;
                continue;
            }
            if ($address === null || !preg_match('/^0x[a-fA-F0-9]{40}$/', $address)) {
                $warn($rowNum, 'нет валидного адреса (0x…, 42 символа) — пропущена');
                $stats['skipped']++;
                continue;
            }
            $newId = ($id !== null && $id !== '' && !isset($byId[$id]))
                ? $id
                : generate_wallet_id($wallets);
            $wallets[] = [
                'id'             => $newId,
                'label'          => $newId,
                'address'        => $address,
                'enabled'        => true,
                'status'         => 'WATCH',
                'rating'         => 0,
                'tags'           => [],
                'notes'          => '',
                'source'         => '',
                'archive_reason' => '',
                'settings'       => [],
            ];
            $idx = count($wallets) - 1;
            $byId[$newId] = $idx;
            $byAddress[strtolower($address)] = $idx;
            $stats['created']++;
            $stats['created_ids'][] = $newId;
        } else {
            $stats['updated']++;
            // Смена адреса существующего — только на валидный
            if ($address !== null && $address !== ''
                && strtolower($address) !== strtolower($wallets[$idx]['address'] ?? '')) {
                if (preg_match('/^0x[a-fA-F0-9]{40}$/', $address)) {
                    unset($byAddress[strtolower($wallets[$idx]['address'] ?? '')]);
                    $wallets[$idx]['address'] = $address;
                    $byAddress[strtolower($address)] = $idx;
                } else {
                    $warn($rowNum, 'некорректный адрес — оставлен прежний');
                }
            }
        }

        $w = &$wallets[$idx];

        // --- профильные поля (обновляются только присутствующими колонками) ---
        $label = $cell($row, 'label');
        if ($label !== null && $label !== '') $w['label'] = $label;

        $enabled = $cell($row, 'enabled');
        if ($enabled !== null && $enabled !== '') {
            $b = traders_xlsx_parse_bool($enabled);
            if ($b !== null) $w['enabled'] = $b;
            else $warn($rowNum, "непонятное значение enabled '$enabled' — оставлено прежнее");
        }

        $status = $cell($row, 'status');
        if ($status !== null && $status !== '') {
            $status = strtoupper($status);
            if (in_array($status, ['WATCH', 'TEST', 'ACTIVE', 'ARCHIVE'], true)) $w['status'] = $status;
            else $warn($rowNum, "неизвестный статус '$status' — оставлен прежний");
        }

        $rating = $cell($row, 'rating');
        if ($rating !== null && $rating !== '' && is_numeric($rating)) {
            $w['rating'] = max(0, min(5, (int) $rating));
        }

        $tags = $cell($row, 'tags');
        if ($tags !== null) {
            $list = array_values(array_unique(array_filter(
                array_map('trim', preg_split('/,+/', $tags)),
                fn($t) => $t !== ''
            )));
            // Новые теги добавляем в глобальный список — импорт задаёт их явно
            foreach ($list as $t) {
                if (!in_array($t, $availableTags, true)) $availableTags[] = $t;
            }
            $w['tags'] = $list;
        }

        if (($notes = $cell($row, 'notes')) !== null)   $w['notes']  = $notes;
        if (($source = $cell($row, 'source')) !== null) $w['source'] = $source;

        // --- настройки (только колонки, присутствующие в файле) ---
        $settings = is_array($w['settings'] ?? null) ? $w['settings'] : [];
        foreach ($settingFields as $key => $type) {
            if (!isset($colMap[$key])) continue;
            [$ok, $value] = traders_xlsx_parse_setting((string) ($row[$colMap[$key]] ?? ''), $type);
            if ($ok) {
                $settings[$key] = $value; // null = наследовать глобальное
            } else {
                $warn($rowNum, "невалидное значение $key — поле не изменено");
            }
        }
        $w['settings'] = $settings;
        unset($w);
    }

    sort($availableTags);
    $config['wallets']        = $wallets;
    $config['available_tags'] = $availableTags;

    return ['config' => $config] + $stats;
}

// ---------------------------------------------------------------
// Импорт: обработчик загрузки файла (вызывается из bulk.php после CSRF)
// ---------------------------------------------------------------

/**
 * Полный цикл импорта: валидация upload → чтение xlsx → применение →
 * сохранение конфига → инициализация виртуальных счетов новых трейдеров.
 * Возвращает ['message' =>, 'messageType' =>] как остальные handler'ы.
 */
function dashboard_handle_traders_xlsx_import(array $files, array $post, array $config): array
{
    $f = $files['xlsx_file'] ?? null;
    if (!$f || ($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        return ['message' => 'Файл не загружен (выберите .xlsx)', 'messageType' => 'error'];
    }
    if (($f['size'] ?? 0) > 5 * 1024 * 1024) {
        return ['message' => 'Файл слишком большой (лимит 5 МБ)', 'messageType' => 'error'];
    }

    $rows = xlsx_read_rows($f['tmp_name']);
    if ($rows === null) {
        return ['message' => 'Не удалось прочитать файл как XLSX', 'messageType' => 'error'];
    }

    $createNew = !empty($post['xlsx_create_new']);
    $res = traders_xlsx_apply_rows($rows, $config, $createNew);

    if ($res['updated'] === 0 && $res['created'] === 0) {
        $msg = 'Ничего не импортировано';
        if (!empty($res['warnings'])) {
            $msg .= ': ' . implode('; ', array_slice($res['warnings'], 0, 3));
        }
        return ['message' => $msg, 'messageType' => 'error'];
    }

    if (!save_wallets_config($res['config'])) {
        return ['message' => 'Ошибка сохранения конфига', 'messageType' => 'error'];
    }

    // Виртуальные счета новых трейдеров — как в bulk_import
    $globalSettings = $res['config']['global_settings'];
    foreach ($res['created_ids'] as $newId) {
        foreach ($res['config']['wallets'] as $w) {
            if ($w['id'] === $newId) {
                $s = resolve_wallet_settings($w, $globalSettings);
                storage_save_balance('wallet', $newId,
                    dashboard_fresh_wallet_balance((float) $s['virtual_balance']));
                break;
            }
        }
    }

    $parts = ['Обновлено: ' . $res['updated']];
    if ($res['created'] > 0) $parts[] = 'создано: ' . $res['created'];
    if ($res['skipped'] > 0) $parts[] = 'пропущено: ' . $res['skipped'];
    $message = 'Импорт завершён. ' . implode(', ', $parts);
    if (!empty($res['warnings'])) {
        $extra = count($res['warnings']) - 3;
        $message .= '. Предупреждения: ' . implode('; ', array_slice($res['warnings'], 0, 3))
            . ($extra > 0 ? " (и ещё $extra)" : '');
    }

    return ['message' => $message, 'messageType' => empty($res['warnings']) ? 'success' : 'info'];
}
