package xyz.kusoft.tradebotmonitor

import android.app.Activity
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.InputType
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.Executors
import kotlin.math.abs

class MainActivity : Activity() {
    private lateinit var prefs: AppPrefs
    private lateinit var root: LinearLayout
    private lateinit var contentHost: FrameLayout
    private lateinit var bottomNav: LinearLayout

    private val executor = Executors.newSingleThreadExecutor()
    private val mainHandler = Handler(Looper.getMainLooper())
    private val scrollPositions = mutableMapOf<String, Int>()

    private var snapshot: BotSnapshot? = null
    private var lastError: String = ""
    private var activeTab: String = "assets"
    private var palette: AppPalette = AppPalette.dark()
    private var trainingStatusSignature: String = ""
    private var retrainRequestInFlight: Boolean = false
    private var assetsBindings: AssetsBindings? = null
    private var tradingBindings: TradingBindings? = null
    private var settingsBindings: SettingsBindings? = null

    private data class AssetsBindings(
        var positionSignature: String,
        var closedTradesSignature: String,
        var equity: TextView? = null,
        var openPnl: TextView? = null,
        var realizedPnl: TextView? = null,
        var mode: TextView? = null,
        var cash: TextView? = null,
        var exposure: TextView? = null,
        var positionCount: TextView? = null,
        var positionsContainer: LinearLayout? = null,
        var positionsScroller: HorizontalScrollView? = null,
        var positionsList: LinearLayout? = null,
        var closedTradesContainer: LinearLayout? = null,
        val positionOrder: MutableList<String> = mutableListOf(),
        val positions: MutableMap<String, PositionBinding> = linkedMapOf(),
    )

    private data class PositionBinding(
        var index: TextView? = null,
        var value: TextView? = null,
        var quantity: TextView? = null,
        var entryPrice: TextView? = null,
        var markPrice: TextView? = null,
        var pnl: TextView? = null,
        var takeProfit: TextView? = null,
        var highestPrice: TextView? = null,
    )

    private data class TradingBindings(
        val symbol: String,
        var price: TextView? = null,
        var edgeLine: TextView? = null,
        var equity: TextView? = null,
        var status: TextView? = null,
        var chart: CandleChartView? = null,
        var chartTime: TextView? = null,
        var decision: TextView? = null,
        var kelly: TextView? = null,
        var reason: TextView? = null,
    )

    private data class SettingsBindings(
        var retrainSignature: String,
        var liveSignature: String,
        var retrainContainer: LinearLayout? = null,
        var liveContainer: LinearLayout? = null,
    )

    private val refreshRunnable = object : Runnable {
        override fun run() {
            refreshData(silent = true)
            mainHandler.postDelayed(this, 5000L)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        prefs = AppPrefs(this)
        palette = if (prefs.themeMode == "light") AppPalette.light() else AppPalette.dark()
        buildShell()
        refreshData(silent = false)
        mainHandler.postDelayed(refreshRunnable, 5000L)
    }

    override fun onDestroy() {
        mainHandler.removeCallbacks(refreshRunnable)
        executor.shutdownNow()
        super.onDestroy()
    }

    private fun buildShell() {
        updateSystemBars()
        root = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setBackgroundColor(palette.page)
        }
        setContentView(root)

        contentHost = FrameLayout(this).apply {
            setBackgroundColor(palette.page)
        }
        root.addView(contentHost, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f))

        bottomNav = LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.CENTER
            setPadding(dp(10), dp(7), dp(10), dp(9) + navigationBarInset())
            background = boxDrawable(palette.panel, palette.line, 0)
        }
        root.addView(bottomNav, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(64) + navigationBarInset()))

        renderBottomNav()
        render(saveScroll = false)
    }

    private fun updateSystemBars() {
        window.statusBarColor = palette.page
        window.navigationBarColor = palette.page
        window.decorView.systemUiVisibility = if (palette.isDark) {
            0
        } else {
            View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
        }
    }

    private fun refreshData(silent: Boolean) {
        executor.execute {
            try {
                val data = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).fetchSnapshot()
                mainHandler.post {
                    val hadSnapshot = snapshot != null
                    val hadError = lastError.isNotBlank()
                    val newTrainingStatusSignature = trainingStatusSignature(data.retrain)
                    val trainingChanged = newTrainingStatusSignature != trainingStatusSignature
                    snapshot = data
                    trainingStatusSignature = newTrainingStatusSignature
                    lastError = ""
                    ensureSelectedSymbol()
                    if (silent && hadSnapshot && updateVisibleScreen(data)) {
                        return@post
                    }
                    if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged)) {
                        render()
                    }
                }
            } catch (error: Exception) {
                mainHandler.post {
                    val hadSnapshot = snapshot != null
                    val hadError = lastError.isNotBlank()
                    lastError = error.message ?: "ошибка подключения"
                    if (!silent) toast(lastError)
                    if (silent && hadSnapshot) {
                        return@post
                    }
                    if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged = false)) {
                        render()
                    }
                }
            }
        }
    }

    private fun shouldRenderAfterRefresh(silent: Boolean, hadSnapshot: Boolean, hadError: Boolean, trainingChanged: Boolean): Boolean {
        val focused = contentHost.findFocus()
        if (activeTab == "settings" && focused is EditText) {
            return false
        }
        if (!silent) {
            return true
        }
        if (activeTab == "settings" && trainingChanged) {
            return true
        }
        if (activeTab == "assets" || activeTab == "trading") {
            return true
        }
        return !hadSnapshot || hadError
    }

    private fun renderBottomNav() {
        bottomNav.removeAllViews()
        listOf(
            "assets" to "Активы",
            "trading" to "Торговля",
            "settings" to "Настройки",
        ).forEach { (id, title) ->
            bottomNav.addView(
                navItem(title, selected = activeTab == id) {
                    if (activeTab != id) {
                        saveCurrentScroll()
                        activeTab = id
                        renderBottomNav()
                        render(saveScroll = false)
                    }
                },
                LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f),
            )
        }
    }

    private fun render(saveScroll: Boolean = true) {
        val tabForRender = activeTab
        if (saveScroll) {
            saveCurrentScroll(tabForRender)
        }
        assetsBindings = null
        tradingBindings = null
        settingsBindings = null
        val view = when (activeTab) {
            "assets" -> assetsScreen()
            "trading" -> tradingScreen()
            "settings" -> settingsScreen()
            else -> assetsScreen()
        }
        val previous = contentHost.getChildAt(0)
        contentHost.addView(view)
        restoreScroll(view, tabForRender)
        if (previous != null) {
            contentHost.removeView(previous)
        }
    }

    private fun updateVisibleScreen(data: BotSnapshot): Boolean =
        when (activeTab) {
            "assets" -> updateAssetsScreen(data)
            "trading" -> updateTradingScreen(data)
            "settings" -> updateSettingsScreen(data)
            else -> false
        }

    private fun updateAssetsScreen(data: BotSnapshot): Boolean {
        val binding = assetsBindings ?: return false
        val positionSignature = positionsSignature(data.positions)
        if (binding.positionSignature != positionSignature) {
            if (!syncPositionsList(binding, data.positions)) return false
            binding.positionSignature = positionSignature
        }
        val closedSignature = closedTradesSignature(data.closedTrades, data.closedTradesSummary)
        if (binding.closedTradesSignature != closedSignature) {
            binding.closedTradesContainer?.let { container ->
                container.removeAllViews()
                container.addView(closedTradesPanel(data.closedTrades, data.closedTradesSummary))
                binding.closedTradesSignature = closedSignature
            } ?: return false
        }

        val openPnl = data.positions.sumOf { it.unrealizedPnl }
        val realizedPnl = data.closedTradesSummary.netPnl
        setText(binding.equity, money(data.account.equity))
        setText(binding.openPnl, "P&L открытых позиций ${signedMoney(openPnl)}", colorForSigned(openPnl))
        setText(binding.realizedPnl, "Прибыль закрытых сделок ${signedMoney(realizedPnl)}", colorForSigned(realizedPnl))
        setText(binding.mode, "Режим: ${modeLabel(data.mode)} · ${if (data.running) "цикл работает" else "цикл остановлен"}")
        setText(binding.cash, money(data.account.cash))
        setText(binding.exposure, money(data.account.exposure))
        setText(binding.positionCount, "Открытые позиции: ${data.positions.size}")

        data.positions.forEach { position ->
            val row = binding.positions[positionKey(position)] ?: return@forEach
            setText(row.value, "${money(position.marketValue)} · ${signedPercent(position.unrealizedPnlPercent, 2)}", colorForSigned(position.unrealizedPnlPercent))
            setText(row.quantity, number(position.qty, 8))
            setText(row.entryPrice, price(position.entryPrice))
            setText(row.markPrice, price(position.markPrice))
            setText(row.pnl, signedMoney(position.unrealizedPnl), colorForSigned(position.unrealizedPnl))
            setText(row.takeProfit, priceOrNone(position.takeProfit))
            setText(row.highestPrice, priceOrNone(position.highestPrice))
        }
        return true
    }

    private fun updateTradingScreen(data: BotSnapshot): Boolean {
        val binding = tradingBindings ?: return false
        if (binding.symbol != prefs.selectedSymbol) return false
        val market = selectedMarket(data) ?: return false
        if (market.symbol != binding.symbol) return false
        val signal = data.signalsBySymbol[market.symbol]
        val edge = signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0
        val probability = signal?.probabilityUp ?: market.forecast?.probabilityUp ?: 0.0
        val action = normalizedAction(signal?.action)
        val decision = when (action) {
            "BUY" -> "Покупка разрешена"
            "SELL" -> "Выход из позиции"
            else -> "Ожидание"
        }
        val reason = signal?.reason?.ifBlank { market.forecast?.reason.orEmpty() } ?: market.forecast?.reason.orEmpty()

        setText(binding.price, price(latestPrice(market)), colorForSigned(edge))
        setText(binding.edgeLine, "Edge ${signedPercent(edge, 2)} · P(up) ${probability(probability)} · 1h")
        setText(binding.equity, money(data.account.equity))
        setText(binding.status, if (data.running) "РАБОТАЕТ" else "СТОП", if (data.running) palette.green else palette.amber)
        setText(binding.decision, decision, actionColor(action))
        setText(binding.kelly, money(signal?.positionNotionalUsdt ?: 0.0))
        setText(binding.reason, reason.ifBlank { "Нет объяснения от модели" })
        binding.chart?.candles = market.candles
        setText(binding.chartTime, lastCandleTime(market))
        return true
    }

    private fun updateSettingsScreen(data: BotSnapshot): Boolean {
        if (contentHost.findFocus() is EditText) {
            return true
        }
        val binding = settingsBindings ?: return false
        val retrainSignature = retrainBlockSignature(data)
        if (binding.retrainSignature != retrainSignature) {
            binding.retrainContainer?.let { container ->
                container.removeAllViews()
                container.addView(section("Переобучение Torch", retrainSettingsBlock(data.retrain, data.backtest)))
                binding.retrainSignature = retrainSignature
            } ?: return false
        }
        val liveSignature = liveBlockSignature(data)
        if (binding.liveSignature != liveSignature) {
            binding.liveContainer?.let { container ->
                container.removeAllViews()
                container.addView(section("Переход к live-торговле", liveSettingsBlock(data)))
                binding.liveSignature = liveSignature
            } ?: return false
        }
        return true
    }

    private fun saveCurrentScroll(tabId: String = activeTab) {
        val current = contentHost.getChildAt(0)
        if (current is ScrollView) {
            scrollPositions[tabId] = current.scrollY
        }
    }

    private fun restoreScroll(view: View, tabId: String) {
        if (view !is ScrollView) return
        val savedY = scrollPositions[tabId] ?: 0
        view.post {
            val maxY = (view.getChildAt(0)?.height ?: 0) - view.height
            view.scrollTo(0, savedY.coerceIn(0, maxOf(0, maxY)))
        }
    }

    private fun tradingScreen(): View {
        val box = screenContent()
        box.addView(statusLine("Торговля", modeBadgeText()))
        val data = snapshot
        if (data == null) {
            box.addView(connectionStatePanel().top(dp(18)))
            return wrapScroll(box)
        }

        val market = selectedMarket(data)
        if (market == null) {
            box.addView(emptyState("Пары еще не загружены").top(dp(18)))
            return wrapScroll(box)
        }
        val signal = data.signalsBySymbol[market.symbol]

        tradingBindings = TradingBindings(market.symbol)
        box.addView(pairSelector(data).top(dp(14)))
        box.addView(aiHero(data, market, signal).top(dp(14)))
        box.addView(chartTabs().top(dp(18)))
        box.addView(chartPanel(market).top(dp(10)))
        box.addView(decisionPanel(market, signal).top(dp(14)))
        return wrapScroll(box)
    }

    private fun marketsScreen(): View {
        val box = screenContent()
        box.addView(statusLine("Рынки", "12 spot-пар"))
        val data = snapshot
        if (data == null) {
            box.addView(connectionStatePanel().top(dp(18)))
            return wrapScroll(box)
        }

        box.addView(searchPlaceholder("Поиск пары или сигнала").top(dp(16)))
        box.addView(tableHeader(listOf("AI-рейтинг" to 1.7f, "Цена" to 1f, "Edge" to 0.9f, "Статус" to 0.9f)).top(dp(16)))

        val rows = rankedMarkets(data)
        val list = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        rows.forEach { market ->
            list.addView(marketRow(market, data.signalsBySymbol[market.symbol], selected = market.symbol == prefs.selectedSymbol))
        }
        box.addView(list.top(dp(8)))
        return wrapScroll(box)
    }

    private fun riskScreen(): View {
        val box = screenContent()
        box.addView(statusLine("Сделка и риск", prefs.selectedSymbol))
        val data = snapshot
        if (data == null) {
            box.addView(connectionStatePanel().top(dp(18)))
            return wrapScroll(box)
        }

        val market = selectedMarket(data)
        if (market == null) {
            box.addView(emptyState("Пары еще не загружены").top(dp(18)))
            return wrapScroll(box)
        }
        val signal = data.signalsBySymbol[market.symbol]
        box.addView(tradeSummaryCard(data, signal).top(dp(16)))
        box.addView(kellyPanel(signal).top(dp(16)))
        box.addView(reasonPanel(market, signal).top(dp(16)))
        box.addView(liveReadinessPanel(data).top(dp(16)))
        box.addView(controlButtons().top(dp(16)))
        return wrapScroll(box)
    }

    private fun assetsScreen(): View {
        val box = screenContent()
        box.addView(statusLine("Активы", modeBadgeText()))
        val data = snapshot
        if (data == null) {
            box.addView(connectionStatePanel().top(dp(18)))
            return wrapScroll(box)
        }

        val openPnl = data.positions.sumOf { it.unrealizedPnl }
        val realizedPnl = data.closedTradesSummary.netPnl
        assetsBindings = AssetsBindings(
            positionSignature = positionsSignature(data.positions),
            closedTradesSignature = closedTradesSignature(data.closedTrades, data.closedTradesSummary),
        )
        box.addView(assetHero(data, openPnl, realizedPnl).top(dp(14)))
        box.addView(accountMetrics(data).top(dp(14)))
        box.addView(positionsPanel(data.positions).top(dp(16)))
        val closedTradesHost = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        assetsBindings?.closedTradesContainer = closedTradesHost
        closedTradesHost.addView(closedTradesPanel(data.closedTrades, data.closedTradesSummary))
        box.addView(closedTradesHost.top(dp(16)))
        return wrapScroll(box)
    }

    private fun settingsScreen(): View {
        val box = screenContent()
        box.addView(statusLine("Настройки", prefs.apiBaseUrl))
        settingsBindings = SettingsBindings(
            retrainSignature = retrainBlockSignature(snapshot),
            liveSignature = liveBlockSignature(snapshot),
        )

        val (savedLogin, savedPassword) = authParts(prefs.commandToken)
        val apiInput = input("Адрес API бота", prefs.apiBaseUrl)
        val loginInput = input("Логин API", savedLogin)
        val passwordInput = input("Пароль API", savedPassword).apply {
            inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
        }
        box.addView(section("Подключение", LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(apiInput)
            addView(loginInput.top(dp(8)))
            addView(passwordInput.top(dp(8)))
            addView(actionRow(
                "Сохранить" to {
                    prefs.apiBaseUrl = apiInput.text.toString()
                    prefs.commandToken = authToken(loginInput.text.toString(), passwordInput.text.toString())
                    toast("Подключение сохранено")
                    refreshData(silent = false)
                },
                "Обновить" to { refreshData(silent = false) },
            ).top(dp(10)))
        }).top(dp(16)))

        box.addView(section("Тема приложения", actionRow(
            "Темная" to {
                prefs.themeMode = "dark"
                palette = AppPalette.dark()
                buildShell()
                refreshData(silent = true)
            },
            "Светлая" to {
                prefs.themeMode = "light"
                palette = AppPalette.light()
                buildShell()
                refreshData(silent = true)
            },
        )).top(dp(14)))

        val retrainHost = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        settingsBindings?.retrainContainer = retrainHost
        retrainHost.addView(section("Переобучение Torch", retrainSettingsBlock(snapshot?.retrain ?: JSONObject(), snapshot?.backtest ?: JSONObject())))
        box.addView(retrainHost.top(dp(14)))

        val liveHost = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        settingsBindings?.liveContainer = liveHost
        liveHost.addView(section("Переход к live-торговле", liveSettingsBlock(snapshot)))
        box.addView(liveHost.top(dp(14)))
        return wrapScroll(box)
    }

    private fun aiHero(data: BotSnapshot, market: MarketItem, signal: SignalData?): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(0, 0, 0, 0)

            val top = LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.TOP
            }
            top.addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.VERTICAL
                addView(text(market.symbol, 20f, Typeface.BOLD))
                val priceText = text(price(latestPrice(market)), 32f, Typeface.BOLD, colorForSigned(signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0))
                tradingBindings?.price = priceText
                addView(priceText.top(dp(6)))
                val edge = signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0
                val probability = signal?.probabilityUp ?: market.forecast?.probabilityUp ?: 0.0
                val edgeLine = text("Edge ${signedPercent(edge, 2)} · P(up) ${probability(probability)} · 1h", 13f, Typeface.NORMAL, palette.muted)
                tradingBindings?.edgeLine = edgeLine
                addView(edgeLine.top(dp(2)))
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))

            top.addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.VERTICAL
                gravity = Gravity.END
                addView(text("Equity", 12f, Typeface.NORMAL, palette.muted))
                val equityText = text(money(data.account.equity), 18f, Typeface.BOLD)
                tradingBindings?.equity = equityText
                addView(equityText.top(dp(5)))
                val statusText = statusBadge(if (data.running) "Работает" else "Стоп", if (data.running) "ok" else "warn")
                tradingBindings?.status = statusText
                addView(statusText.top(dp(8)))
            }, LinearLayout.LayoutParams(dp(132), ViewGroup.LayoutParams.WRAP_CONTENT))

            addView(top)
        }

    private fun chartTabs(): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(thinDivider())
            addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                setPadding(0, dp(12), 0, dp(6))
                addView(tabText("График", true))
                addView(tabText("Torch", false))
                addView(tabText("История", false))
            })
        }

    private fun chartPanel(market: MarketItem): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            background = boxDrawable(palette.chartBg, Color.TRANSPARENT, 2)
            val chart = CandleChartView(this@MainActivity).apply {
                palette = this@MainActivity.palette
                candles = market.candles
            }
            tradingBindings?.chart = chart
            addView(chart, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(278)))
            addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.HORIZONTAL
                setPadding(0, dp(4), 0, 0)
                addView(text("EMA50", 11f, Typeface.NORMAL, palette.muted))
                addView(text("EMA200", 11f, Typeface.NORMAL, palette.amber).left(dp(16)))
                val timeText = text(lastCandleTime(market), 11f, Typeface.NORMAL, palette.muted).apply {
                    gravity = Gravity.END
                }
                tradingBindings?.chartTime = timeText
                addView(timeText, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            })
        }

    private fun pairSelector(data: BotSnapshot): View {
        val symbols = orderedMarkets(data.markets).map { it.symbol }
        val selectedIndex = symbols.indexOf(prefs.selectedSymbol).coerceAtLeast(0)
        val adapter = object : ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, symbols) {
            override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
                val view = super.getView(position, convertView, parent) as TextView
                view.setTextColor(palette.text)
                view.textSize = 15f
                view.typeface = Typeface.DEFAULT_BOLD
                return view
            }

            override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
                val view = super.getDropDownView(position, convertView, parent) as TextView
                view.setTextColor(palette.text)
                view.setBackgroundColor(palette.panel)
                view.textSize = 15f
                view.setPadding(dp(14), dp(12), dp(14), dp(12))
                return view
            }
        }.apply {
            setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        }
        return LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(text("Торговая пара", 12f, Typeface.NORMAL, palette.muted))
            addView(Spinner(this@MainActivity).apply {
                this.adapter = adapter
                setSelection(selectedIndex, false)
                background = boxDrawable(palette.panel, palette.line, 8)
                setPadding(dp(10), 0, dp(10), 0)
                onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
                    override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
                        val symbol = symbols.getOrNull(position) ?: return
                        if (symbol != prefs.selectedSymbol) {
                            prefs.selectedSymbol = symbol
                            render(saveScroll = false)
                        }
                    }

                    override fun onNothingSelected(parent: AdapterView<*>?) = Unit
                }
            }, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48)))
        }
    }

    private fun decisionPanel(market: MarketItem, signal: SignalData?): View {
        val action = normalizedAction(signal?.action)
        val color = actionColor(action)
        val decision = when (action) {
            "BUY" -> "Покупка разрешена"
            "SELL" -> "Выход из позиции"
            else -> "Ожидание"
        }
        val reason = signal?.reason?.ifBlank { market.forecast?.reason.orEmpty() } ?: market.forecast?.reason.orEmpty()
        return section(null, LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                addView(LinearLayout(this@MainActivity).apply {
                    orientation = LinearLayout.VERTICAL
                    addView(text("Решение Torch", 12f, Typeface.NORMAL, palette.muted))
                    val decisionText = text(decision, 20f, Typeface.BOLD, color)
                    tradingBindings?.decision = decisionText
                    addView(decisionText.top(dp(8)))
                }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
                addView(LinearLayout(this@MainActivity).apply {
                    orientation = LinearLayout.VERTICAL
                    gravity = Gravity.END
                    addView(text("Kelly", 12f, Typeface.NORMAL, palette.muted))
                    val kellyText = text(money(signal?.positionNotionalUsdt ?: 0.0), 20f, Typeface.BOLD)
                    tradingBindings?.kelly = kellyText
                    addView(kellyText.top(dp(8)))
                }, LinearLayout.LayoutParams(dp(120), ViewGroup.LayoutParams.WRAP_CONTENT))
            })
            addView(thinDivider().top(dp(12)))
            addView(boundKeyValueLine("Причина", reason.ifBlank { "Нет объяснения от модели" }) { tradingBindings?.reason = it }.top(dp(8)))
        })
    }

    private fun torchFeaturePanel(forecast: ForecastData?): View {
        if (forecast == null) {
            return section("Что видит нейросеть", text("Torch еще не отдал параметры для этой пары", 13f, Typeface.NORMAL, palette.muted))
        }
        val box = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        box.addView(keyValueLine("Модель", modelLabel(forecast.model)))
        box.addView(keyValueLine("Горизонт", if (forecast.horizon > 0) "${forecast.horizon} свеч. вперед" else "нет данных"))
        box.addView(keyValueLine("Q10 / Q50 / Q90", "${signedPercent(forecast.q10Percent, 3)} / ${signedPercent(forecast.q50Percent, 3)} / ${signedPercent(forecast.q90Percent, 3)}"))
        box.addView(keyValueLine("Skill", number(forecast.skill, 4)))
        box.addView(thinDivider().top(dp(10)))
        forecast.features.take(14).forEach { feature ->
            box.addView(featureRow(feature).top(dp(8)))
        }
        return section("Что видит нейросеть", box)
    }

    private fun marketRow(market: MarketItem, signal: SignalData?, selected: Boolean): View {
        val action = normalizedAction(signal?.action)
        val edge = signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0
        val probability = signal?.probabilityUp ?: market.forecast?.probabilityUp ?: 0.0
        return LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.CENTER_VERTICAL
            setPadding(0, dp(8), 0, dp(8))
            background = if (selected) boxDrawable(palette.panelAlt, Color.TRANSPARENT, 2) else null
            setOnClickListener {
                prefs.selectedSymbol = market.symbol
                activeTab = "trading"
                renderBottomNav()
                render(saveScroll = false)
            }
            isClickable = true
            isFocusable = false

            addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.VERTICAL
                addView(text(market.symbol, 15f, Typeface.BOLD))
                addView(text("P(up) ${probability(probability)} · Kelly ${number(signal?.positionNotionalUsdt ?: 0.0, 1)}", 11f, Typeface.NORMAL, palette.muted).top(dp(3)))
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.7f))

            addView(text(price(latestPrice(market)), 14f, Typeface.BOLD).apply {
                gravity = Gravity.END
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            addView(text(signedPercent(edge, 2), 14f, Typeface.NORMAL, colorForSigned(edge)).apply {
                gravity = Gravity.END
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.9f))
            addView(text(action, 12f, Typeface.BOLD, actionColor(action)).apply {
                gravity = Gravity.END
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.9f))
        }
    }

    private fun tradeSummaryCard(data: BotSnapshot, signal: SignalData?): View {
        val action = normalizedAction(signal?.action)
        val risk = data.config.optDouble("risk_per_trade_percent", 0.0) * 100.0
        return LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.CENTER_VERTICAL
            setPadding(dp(16), dp(16), dp(16), dp(16))
            background = boxDrawable(withAlpha(actionColor(action), if (palette.isDark) 24 else 16), actionColor(action), 8)
            addView(valueStack("Действие", action, actionColor(action), 32f), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            addView(valueStack("Размер сейчас", money(signal?.positionNotionalUsdt ?: 0.0), palette.text, 18f), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            addView(valueStack("Риск", percent(risk, 2), palette.amber, 18f), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.8f))
        }
    }

    private fun kellyPanel(signal: SignalData?): View {
        val sizing = signal?.diagnostics?.optJSONObject("position_sizing") ?: JSONObject()
        val target = sizing.optDouble("kelly_target_notional_usdt", 0.0)
        val exposure = sizing.optDouble("symbol_exposure_usdt", 0.0)
        val remaining = sizing.optDouble("kelly_remaining_notional_usdt", signal?.positionNotionalUsdt ?: 0.0)
        return section("Kelly распределение", LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(allocationBar(if (target > 0.0) exposure / target else 0.0))
            addView(keyValueLine("Цель по паре", money(target)).top(dp(10)))
            addView(keyValueLine("Уже занято", money(exposure)).top(dp(4)))
            addView(keyValueLine("Можно добавить", money(remaining), palette.green).top(dp(4)))
        })
    }

    private fun reasonPanel(market: MarketItem, signal: SignalData?): View {
        val forecast = market.forecast
        return section("Почему модель так решила", LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.HORIZONTAL
                addView(text("Edge: ${signedPercent(signal?.expectedReturnPercent ?: forecast?.expectedReturnPercent ?: 0.0, 2)}", 13f, Typeface.NORMAL, palette.green), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
                addView(text("P(up): ${probability(signal?.probabilityUp ?: forecast?.probabilityUp ?: 0.0)}", 13f, Typeface.NORMAL, palette.green), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            })
            addView(text(signal?.reason?.ifBlank { forecast?.reason.orEmpty() }?.ifBlank { "Нет объяснения от модели" } ?: "Нет объяснения от модели", 13f, Typeface.BOLD).top(dp(12)))
            val candle = market.candles.lastOrNull()
            addView(text("RSI ${number(candle?.rsi14 ?: 0.0, 1)}, ATR ${price(candle?.atr14 ?: 0.0)}, спред ${percent(market.ticker?.spreadPercent ?: 0.0, 4)}", 12f, Typeface.NORMAL, palette.muted).top(dp(10)))
        })
    }

    private fun liveReadinessPanel(data: BotSnapshot): View {
        val config = data.config
        val running = data.running
        val steps = listOf(
            "Бот остановлен перед переключением" to !running,
            "TRADING_MODE=live" to (config.optStringClean("trading_mode") == "live"),
            "Mainnet, не testnet" to !config.optBoolean("bybit_testnet", true),
            "Подтвержден реальный риск" to config.optBoolean("live_ready", false),
            "Лимит live-ордера задан" to (config.optDouble("live_order_max_usdt", 0.0) > 0.0),
        )
        val failed = steps.count { !it.second }
        return section(null, LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(text("Live готовность", 12f, Typeface.NORMAL, palette.muted))
            addView(text(if (failed == 0) "Все шаги пройдены" else "$failed шага не пройдены", 16f, Typeface.BOLD, if (failed == 0) palette.green else palette.amber).top(dp(8)))
            steps.forEach { (title, ok) ->
                addView(keyValueLine(title, if (ok) "OK" else "Нужно", if (ok) palette.green else palette.amber).top(dp(7)))
            }
        })
    }

    private fun controlButtons(): View =
        actionRow(
            "Стоп бот" to { sendControl(stop = true) },
            "Запустить цикл" to { sendControl(stop = false) },
        )

    private fun assetHero(data: BotSnapshot, pnl: Double, realizedPnl: Double): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(text("Общие активы", 13f, Typeface.NORMAL, palette.muted))
            val equityText = text(money(data.account.equity), 34f, Typeface.BOLD)
            assetsBindings?.equity = equityText
            addView(equityText.top(dp(8)))
            val openPnlText = text("P&L открытых позиций ${signedMoney(pnl)}", 15f, Typeface.NORMAL, colorForSigned(pnl))
            assetsBindings?.openPnl = openPnlText
            addView(openPnlText.top(dp(8)))
            val realizedPnlText = text("Прибыль закрытых сделок ${signedMoney(realizedPnl)}", 15f, Typeface.NORMAL, colorForSigned(realizedPnl))
            assetsBindings?.realizedPnl = realizedPnlText
            addView(realizedPnlText.top(dp(5)))
            val modeText = text("Режим: ${modeLabel(data.mode)} · ${if (data.running) "цикл работает" else "цикл остановлен"}", 12f, Typeface.NORMAL, palette.muted)
            assetsBindings?.mode = modeText
            addView(modeText.top(dp(8)))
        }

    private fun accountMetrics(data: BotSnapshot): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            addView(metricCell("Доступно", money(data.account.cash)).also { cell ->
                assetsBindings?.cash = cell.findViewWithTag<TextView>("metric_value")
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { setMargins(0, 0, dp(6), 0) })
            addView(metricCell("В позициях", money(data.account.exposure)).also { cell ->
                assetsBindings?.exposure = cell.findViewWithTag<TextView>("metric_value")
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { setMargins(dp(6), 0, 0, 0) })
        }

    private fun positionsPanel(positions: List<PositionData>): View {
        val body = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            val title = text("Открытые позиции: ${positions.size}", 18f, Typeface.BOLD)
            assetsBindings?.positionCount = title
            addView(title)
            val container = LinearLayout(this@MainActivity).apply { orientation = LinearLayout.VERTICAL }
            assetsBindings?.positionsContainer = container
            rebuildPositionsContent(container, positions)
            addView(container.top(dp(12)))
        }
        return section(null, body)
    }

    private fun rebuildPositionsContent(container: LinearLayout, positions: List<PositionData>, preserveScrollX: Int = 0) {
        container.removeAllViews()
        assetsBindings?.positions?.clear()
        assetsBindings?.positionOrder?.clear()
        assetsBindings?.positionsScroller = null
        assetsBindings?.positionsList = null
        if (positions.isEmpty()) {
            container.addView(text("Открытых позиций нет", 13f, Typeface.NORMAL, palette.muted))
            return
        }
        val cardWidth = (resources.displayMetrics.widthPixels - dp(64)).coerceAtLeast(dp(260))
        val list = LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            setPadding(0, 0, dp(10), 0)
        }
        assetsBindings?.positionsList = list
        rebuildPositionsList(list, positions, cardWidth)
        val scroller = HorizontalScrollView(this).apply {
            isHorizontalScrollBarEnabled = false
            overScrollMode = View.OVER_SCROLL_NEVER
            addView(list, ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT))
            if (preserveScrollX > 0) {
                post { scrollTo(preserveScrollX, 0) }
            }
        }
        assetsBindings?.positionsScroller = scroller
        container.addView(scroller)
    }

    private fun rebuildPositionsList(
        list: LinearLayout,
        positions: List<PositionData>,
        cardWidth: Int = (resources.displayMetrics.widthPixels - dp(64)).coerceAtLeast(dp(260)),
    ) {
        list.removeAllViews()
        assetsBindings?.positionOrder?.clear()
        positions.forEachIndexed { index, position ->
            assetsBindings?.positionOrder?.add(positionKey(position))
            list.addView(positionCard(position, index + 1, positions.size), LinearLayout.LayoutParams(cardWidth, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
                if (index < positions.lastIndex) setMargins(0, 0, dp(12), 0)
            })
        }
    }

    private fun syncPositionsList(binding: AssetsBindings, positions: List<PositionData>): Boolean {
        val container = binding.positionsContainer ?: return false
        val list = binding.positionsList
        if (list == null || positions.isEmpty() || binding.positionOrder.isEmpty()) {
            rebuildPositionsContent(container, positions, binding.positionsScroller?.scrollX ?: 0)
            return true
        }

        val byKey = positions.associateBy(::positionKey)
        var index = 0
        while (index < binding.positionOrder.size) {
            val key = binding.positionOrder[index]
            if (!byKey.containsKey(key)) {
                binding.positionOrder.removeAt(index)
                binding.positions.remove(key)
                list.removeViewAt(index)
            } else {
                index += 1
            }
        }

        val cardWidth = (resources.displayMetrics.widthPixels - dp(64)).coerceAtLeast(dp(260))
        positions.forEach { position ->
            val key = positionKey(position)
            if (!binding.positionOrder.contains(key)) {
                binding.positionOrder.add(key)
                list.addView(positionCard(position, binding.positionOrder.size, positions.size), LinearLayout.LayoutParams(cardWidth, ViewGroup.LayoutParams.WRAP_CONTENT))
            }
        }
        updatePositionCardChrome(binding)
        return true
    }

    private fun updatePositionCardChrome(binding: AssetsBindings) {
        val total = binding.positionOrder.size
        binding.positionOrder.forEachIndexed { index, key ->
            setText(binding.positions[key]?.index, "${index + 1}/$total")
        }
        val list = binding.positionsList ?: return
        for (i in 0 until list.childCount) {
            val params = (list.getChildAt(i).layoutParams as? LinearLayout.LayoutParams) ?: continue
            params.setMargins(0, 0, if (i < list.childCount - 1) dp(12) else 0, 0)
            list.getChildAt(i).layoutParams = params
        }
    }

    private fun positionCard(position: PositionData, index: Int, total: Int): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(dp(14), dp(13), dp(14), dp(13))
            background = boxDrawable(palette.panelAlt, palette.line, 8)
            addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                addView(text(position.symbol, 20f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
                val indexText = text("$index/$total", 12f, Typeface.BOLD, palette.muted)
                assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.index = indexText
                addView(indexText)
            })
            val valueText = text("${money(position.marketValue)} · ${signedPercent(position.unrealizedPnlPercent, 2)}", 15f, Typeface.BOLD, colorForSigned(position.unrealizedPnlPercent))
            addView(valueText.top(dp(8)))
            addView(thinDivider().top(dp(12)))
            addView(boundKeyValueLine("Количество", number(position.qty, 8)) { textView ->
                assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.quantity = textView
            }.top(dp(10)))
            addView(boundKeyValueLine("Вход", price(position.entryPrice)) { textView ->
                assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.entryPrice = textView
            }.top(dp(5)))
            addView(boundKeyValueLine("Текущая", price(position.markPrice)) { textView ->
                assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.markPrice = textView
            }.top(dp(5)))
            addView(boundKeyValueLine("PnL", signedMoney(position.unrealizedPnl), colorForSigned(position.unrealizedPnl)) { textView ->
                assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.pnl = textView
            }.top(dp(5)))
            addView(boundKeyValueLine("Take-profit", priceOrNone(position.takeProfit)) { textView ->
                assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.takeProfit = textView
            }.top(dp(5)))
            addView(boundKeyValueLine("Максимум", priceOrNone(position.highestPrice)) { textView ->
                assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.highestPrice = textView
            }.top(dp(5)))
            assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.value = valueText
        }

    private fun closedTradesPanel(trades: List<ClosedTradeData>, summary: ClosedTradesSummary): View {
        val box = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        box.addView(keyValueLine("Всего закрыто", summary.trades.toString()))
        box.addView(keyValueLine("Реализованный итог", signedMoney(summary.netPnl), colorForSigned(summary.netPnl)).top(dp(4)))
        box.addView(keyValueLine("Комиссии", money(summary.feeUsdt), palette.muted).top(dp(4)))
        box.addView(keyValueLine("Win-rate", percent(summary.winRate * 100.0, 1)).top(dp(4)))
        if (trades.isEmpty()) {
            box.addView(text("Закрытых сделок пока нет. Рост equity выше 100 USDT сейчас может быть только плавающей оценкой открытых позиций.", 13f, Typeface.NORMAL, palette.muted).top(dp(12)))
            return section("Последние 10 закрытых сделок", box)
        }
        box.addView(tableHeader(listOf("Время" to 1.05f, "Пара" to 0.9f, "Выход" to 0.9f, "Итог" to 0.9f)).top(dp(12)))
        trades.take(10).forEach { trade ->
            box.addView(closedTradeRow(trade).top(dp(7)))
        }
        return section("Последние 10 закрытых сделок", box)
    }

    private fun closedTradeRow(trade: ClosedTradeData): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.CENTER_VERTICAL
            addView(text(shortIsoTime(trade.closedAt), 12f, Typeface.NORMAL, palette.muted), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.05f))
            addView(text(trade.symbol, 12f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.9f))
            addView(text(priceOrNone(trade.exitPrice), 12f, Typeface.NORMAL, palette.text).apply {
                gravity = Gravity.END
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.9f))
            addView(text(signedMoney(trade.netPnl), 12f, Typeface.BOLD, colorForSigned(trade.netPnl)).apply {
                gravity = Gravity.END
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.9f))
        }

    private fun trainingComputerPanel(retrain: JSONObject): View =
        LinearLayout(this).apply {
            val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
            val activeJob = coordination.optJSONObject("active_job")
            val agentRecentlySeen = coordination.optBoolean(
                "agent_recently_seen",
                coordination.optBoolean("agent_online", false),
            )
            val agentBusy = coordination.optBoolean(
                "agent_busy",
                activeJob?.optStringClean("status") == "running",
            )
            val connectionText = when {
                agentRecentlySeen -> "онлайн"
                agentBusy -> "занят обучением"
                else -> "нет связи"
            }
            val connectionColor = if (agentRecentlySeen || agentBusy) palette.green else palette.amber
            orientation = LinearLayout.VERTICAL
            addView(text("Компьютер обучения", 12f, Typeface.NORMAL, palette.muted))
            addView(text(prefs.trainingComputerName, 18f, Typeface.BOLD, palette.green).top(dp(5)))
            addView(text(prefs.trainingComputerPath, 12f, Typeface.NORMAL, palette.muted).top(dp(4)))
            addView(keyValueLine("Связь агента", connectionText, connectionColor).top(dp(4)))
            addView(text("Бот ставит задания через tb.kusoft.xyz, а этот Windows-agent сам забирает их через интернет и возвращает результат.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
        }

    private fun trainingProcessPanel(coordination: JSONObject): View =
        LinearLayout(this).apply {
            val activeJob = coordination.optJSONObject("active_job")
            val latestJob = coordination.optJSONObject("latest_job")
            orientation = LinearLayout.VERTICAL
            addView(text("Процесс переобучения", 12f, Typeface.NORMAL, palette.muted))
            if (activeJob == null) {
                val agentOnline = coordination.optBoolean("agent_online", false)
                addView(keyValueLine("Состояние", if (agentOnline) "готов к запуску" else "ждет Windows-agent", if (agentOnline) palette.green else palette.amber).top(dp(8)))
                if (latestJob != null) {
                    val latestStatus = latestJob.optStringClean("status")
                    val latestPhase = latestJob.optStringClean("phase")
                    val latestMessage = latestJob.optStringClean("message")
                    addView(keyValueLine("Последнее обучение", trainingJobLabel(latestStatus, latestPhase), trainingJobColor(latestStatus, latestPhase)).top(dp(8)))
                    if (latestMessage.isNotBlank()) {
                        addView(text(latestMessage.take(160), 12f, Typeface.NORMAL, palette.muted).top(dp(6)))
                    }
                }
                return@apply
            }
            val job = activeJob
            val status = job.optStringClean("status")
            val phase = job.optStringClean("phase")
            val progress = job.optInt("progress_percent", if (status == "completed") 100 else 0).coerceIn(0, 100)
            val message = job.optStringClean("message")
            addView(keyValueLine("Состояние", trainingJobLabel(status, phase), trainingJobColor(status, phase)).top(dp(8)))
            addView(allocationBar(progress / 100.0).top(dp(10)))
            addView(keyValueLine("Прогресс", "$progress%", trainingJobColor(status, phase)).top(dp(8)))
            if (message.isNotBlank()) {
                addView(text(message.take(160), 12f, Typeface.NORMAL, palette.text).top(dp(8)))
            }
            val completedAt = job.optStringClean("completed_at")
            if (completedAt.isNotBlank()) {
                addView(keyValueLine("Завершено", completedAt.take(19).replace("T", " ")).top(dp(8)))
            }
            val artifacts = job.optJSONArray("artifacts")
            if (artifacts != null && artifacts.length() > 0) {
                addView(keyValueLine("Артефакты", artifacts.length().toString(), palette.green).top(dp(4)))
            }
        }

    private fun retrainSettingsBlock(retrain: JSONObject, backtest: JSONObject): View {
        val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
        val activeJob = coordination.optJSONObject("active_job")
        val isTrainingActive = retrainRequestInFlight || (activeJob?.optStringClean("status") in setOf("pending", "running"))
        return LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(trainingComputerPanel(retrain).top(dp(12)))
            addView(thinDivider().top(dp(12)))
            addView(trainingProcessPanel(coordination))
            if (retrain.optBoolean("available", false)) {
                addView(guardSummaryPanel(retrain).top(dp(10)))
            }
            val replay = backtest.optJSONObject("full_replay") ?: JSONObject()
            addView(keyValueLine("Replay сделок", replay.optInt("trades", 0).toString()).top(dp(4)))
            addView(keyValueLine("Replay PnL", signedMoney(replay.optDouble("net_pnl", 0.0)), colorForSigned(replay.optDouble("net_pnl", 0.0))).top(dp(4)))
            if (isTrainingActive) {
                addView(disabledActionButton(if (retrainRequestInFlight) "Отправляю команду" else "Обучение уже идет").top(dp(10)))
            } else {
                addView(actionRow(
                    "Запустить сейчас" to {
                        triggerRetrainNow()
                    },
                ).top(dp(10)))
            }
            addView(actionRow(
                "1ч" to { setRetrainInterval(1) },
                "3ч" to { setRetrainInterval(3) },
                "6ч" to { setRetrainInterval(6) },
                "12ч" to { setRetrainInterval(12) },
            ).top(dp(10)))
            addView(keyValueLine("Расписание", if (prefs.retrainScheduleEnabled) "включено, ${prefs.retrainIntervalHours}ч" else "выключено").top(dp(8)))
            addView(actionRow(
                "Включить" to {
                    prefs.retrainScheduleEnabled = true
                    RetrainScheduler.schedule(this@MainActivity, prefs.retrainIntervalHours)
                    toast("Расписание retrain включено")
                    if (activeTab != "settings" || snapshot == null || !updateSettingsScreen(snapshot!!)) {
                        render()
                    }
                },
                "Отключить" to {
                    prefs.retrainScheduleEnabled = false
                    RetrainScheduler.cancel(this@MainActivity)
                    toast("Расписание retrain отключено")
                    if (activeTab != "settings" || snapshot == null || !updateSettingsScreen(snapshot!!)) {
                        render()
                    }
                },
            ).top(dp(10)))
        }
    }

    private fun guardSummaryPanel(retrain: JSONObject): View =
        LinearLayout(this).apply {
            val accepted = retrain.optBoolean("accepted", false)
            val reason = retrain.optStringClean("reason")
            val candidate = retrain.optJSONObject("candidate") ?: JSONObject()
            val current = retrain.optJSONObject("current") ?: JSONObject()
            val candidateScore = candidate.optDouble("score", Double.NaN)
            val currentScore = current.optDouble("score", Double.NaN)
            val status = if (accepted) {
                "новая модель принята"
            } else {
                "candidate обучен, но gate не дал ему ходу"
            }
            val explanation = if (accepted) {
                "Новая модель прошла проверку и стала действующей."
            } else {
                when (reason) {
                    "candidate_score_regressed_vs_current" -> "Обучение завершилось, но текущая модель сильнее по проверке. Бот оставил действующую модель."
                    else -> "Обучение завершилось, но защитный gate не разрешил заменить действующую модель."
                }
            }

            orientation = LinearLayout.VERTICAL
            addView(keyValueLine("Последний gate", status, if (accepted) palette.green else palette.amber))
            addView(text(explanation, 12f, Typeface.NORMAL, palette.text).top(dp(7)))
            if (!candidateScore.isNaN() && !currentScore.isNaN()) {
                addView(keyValueLine("Score candidate / текущая", "${number(candidateScore, 2)} / ${number(currentScore, 2)}", if (accepted) palette.green else palette.amber).top(dp(7)))
            }
        }

    private fun liveSettingsBlock(data: BotSnapshot?): View {
        val config = data?.config ?: JSONObject()
        return LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(keyValueLine("Текущий режим", modeLabel(data?.mode.orEmpty())))
            addView(keyValueLine("Live готов", yesNo(config.optBoolean("live_ready", false))).top(dp(4)))
            addView(keyValueLine("Максимум live-ордера", money(config.optDouble("live_order_max_usdt", 0.0))).top(dp(4)))
            addView(keyValueLine("Риск на сделку", percent(config.optDouble("risk_per_trade_percent", 0.0) * 100.0, 2)).top(dp(4)))
            addView(text("Переключение live остается через серверный .env и явное подтверждение риска. Приложение показывает готовность и может остановить или запустить цикл.", 12f, Typeface.NORMAL, palette.muted).top(dp(12)))
            addView(controlButtons().top(dp(10)))
        }
    }

    private fun twoColumnMetrics(left: Pair<String, String>, right: Pair<String, String>): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            addView(metricCell(left.first, left.second), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { setMargins(0, 0, dp(6), 0) })
            addView(metricCell(right.first, right.second), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { setMargins(dp(6), 0, 0, 0) })
        }

    private fun metricCell(title: String, value: String): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(dp(14), dp(12), dp(14), dp(12))
            background = boxDrawable(palette.panel, palette.line, 8)
            addView(text(title, 12f, Typeface.NORMAL, palette.muted))
            addView(text(value, 17f, Typeface.BOLD, colorForSigned(value)).apply {
                tag = "metric_value"
            }.top(dp(6)))
        }

    private fun tableHeader(columns: List<Pair<String, Float>>): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.HORIZONTAL
                columns.forEach { (title, weight) ->
                    addView(text(title, 13f, Typeface.BOLD, palette.muted).apply {
                        gravity = if (title == columns.first().first) Gravity.START else Gravity.END
                    }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, weight))
                }
            })
            addView(thinDivider().top(dp(8)))
        }

    private fun section(title: String?, body: View): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(dp(14), dp(13), dp(14), dp(13))
            background = boxDrawable(palette.panel, palette.line, 8)
            if (!title.isNullOrBlank()) {
                addView(text(title, 18f, Typeface.BOLD))
                addView(body.top(dp(12)))
            } else {
                addView(body)
            }
        }

    private fun statusLine(title: String, badge: String): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(LinearLayout(this@MainActivity).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                addView(text(title, 21f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
                addView(statusBadge(badge, if (snapshot?.running == true) "ok" else "warn"))
            })
            if (lastError.isNotBlank() && snapshot == null) {
                addView(text(lastError.take(120), 12f, Typeface.NORMAL, palette.red).top(dp(6)))
            }
        }

    private fun connectionStatePanel(): View {
        if (!isAuthError()) {
            return emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}")
        }
        val (savedLogin, savedPassword) = authParts(prefs.commandToken)
        val apiInput = input("Адрес API бота", prefs.apiBaseUrl)
        val loginInput = input("Логин API", savedLogin)
        val passwordInput = input("Пароль API", savedPassword).apply {
            inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
        }
        return section("Нужен вход в API", LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(text("Сервер tb.kusoft.xyz отвечает 401, значит он доступен, но требует авторизацию. Введите логин и пароль, приложение само отправит Basic Auth.", 13f, Typeface.NORMAL, palette.muted))
            addView(apiInput.top(dp(12)))
            addView(loginInput.top(dp(8)))
            addView(passwordInput.top(dp(8)))
            addView(actionRow(
                "Подключиться" to {
                    prefs.apiBaseUrl = apiInput.text.toString()
                    prefs.commandToken = authToken(loginInput.text.toString(), passwordInput.text.toString())
                    toast("Доступ сохранен, проверяю API")
                    refreshData(silent = false)
                },
                "Настройки" to {
                    activeTab = "settings"
                    renderBottomNav()
                    render(saveScroll = false)
                },
            ).top(dp(10)))
        })
    }

    private fun isAuthError(): Boolean =
        lastError.contains("401")

    private fun searchPlaceholder(value: String): View =
        TextView(this).apply {
            text = value
            textSize = 13f
            setTextColor(palette.muted)
            gravity = Gravity.CENTER_VERTICAL
            setPadding(dp(14), 0, dp(14), 0)
            background = boxDrawable(palette.panel, palette.line, 8)
        }.also {
            it.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(44))
        }

    private fun statusBadge(title: String, kind: String): TextView =
        TextView(this).apply {
            text = title.uppercase(Locale.US)
            textSize = 12f
            typeface = Typeface.DEFAULT_BOLD
            gravity = Gravity.CENTER
            setPadding(dp(12), dp(5), dp(12), dp(5))
            val color = when (kind) {
                "ok" -> palette.green
                "bad" -> palette.red
                else -> palette.amber
            }
            setTextColor(color)
            background = boxDrawable(withAlpha(color, if (palette.isDark) 28 else 18), color, 7)
        }

    private fun tabText(title: String, selected: Boolean): View =
        TextView(this).apply {
            text = title
            textSize = 15f
            typeface = Typeface.DEFAULT_BOLD
            setTextColor(if (selected) palette.text else palette.muted)
            gravity = Gravity.CENTER
            if (selected) {
                background = GradientDrawable().apply {
                    setColor(Color.TRANSPARENT)
                    setStroke(0, Color.TRANSPARENT)
                }
            }
        }.also {
            it.layoutParams = LinearLayout.LayoutParams(0, dp(34), 1f)
        }

    private fun valueStack(title: String, value: String, valueColor: Int, valueSize: Float): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(text(title, 12f, Typeface.NORMAL, palette.muted))
            addView(text(value, valueSize, Typeface.BOLD, valueColor).top(dp(8)))
        }

    private fun featureRow(feature: FeatureItem): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            addView(text(feature.label.ifBlank { "Параметр" }, 13f, Typeface.BOLD))
            val details = listOf(feature.group, feature.rawDisplay, feature.modelDisplay).filter { it.isNotBlank() }.joinToString(" · ")
            if (details.isNotBlank()) addView(text(details, 12f, Typeface.NORMAL, palette.muted).top(dp(3)))
            if (feature.interpretation.isNotBlank()) addView(text(feature.interpretation, 12f, Typeface.NORMAL, palette.text).top(dp(3)))
        }

    private fun keyValueLine(key: String, value: String, valueColor: Int = colorForSigned(value)): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.CENTER_VERTICAL
            addView(text(key, 13f, Typeface.NORMAL, palette.muted), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            addView(text(value.ifBlank { "нет данных" }, 13f, Typeface.BOLD, valueColor).apply {
                gravity = Gravity.END
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.05f))
        }

    private fun boundKeyValueLine(key: String, value: String, valueColor: Int = colorForSigned(value), bindValue: (TextView) -> Unit): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.CENTER_VERTICAL
            addView(text(key, 13f, Typeface.NORMAL, palette.muted), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            val valueText = text(value.ifBlank { "нет данных" }, 13f, Typeface.BOLD, valueColor).apply {
                gravity = Gravity.END
            }
            bindValue(valueText)
            addView(valueText, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.05f))
        }

    private fun setText(view: TextView?, value: String, color: Int? = null) {
        if (view == null) return
        if (view.text.toString() != value) {
            view.text = value
        }
        if (color != null) {
            view.setTextColor(color)
        }
    }

    private fun allocationBar(fractionRaw: Double): View {
        val fraction = fractionRaw.coerceIn(0.0, 1.0).toFloat()
        return LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            background = boxDrawable(palette.panelAlt, Color.TRANSPARENT, 3)
            addView(View(this@MainActivity).apply {
                background = boxDrawable(palette.green, Color.TRANSPARENT, 3)
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, fraction))
            addView(View(this@MainActivity), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f - fraction))
        }.also {
            it.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(14))
        }
    }

    private fun actionRow(vararg items: Pair<String, () -> Unit>): View =
        LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
            items.forEachIndexed { index, (title, action) ->
                addView(actionButton(title, action), LinearLayout.LayoutParams(0, dp(48), 1f).apply {
                    if (index > 0) setMargins(dp(6), 0, 0, 0)
                    if (index < items.lastIndex) setMargins(leftMargin, 0, dp(6), 0)
                })
            }
        }

    private fun actionButton(title: String, action: () -> Unit): TextView =
        TextView(this).apply {
            text = title
            textSize = 13f
            typeface = Typeface.DEFAULT_BOLD
            gravity = Gravity.CENTER
            val danger = title.contains("Стоп", ignoreCase = true) || title.contains("Отключ", ignoreCase = true)
            val color = if (danger) palette.red else palette.green
            setTextColor(color)
            background = boxDrawable(withAlpha(color, if (palette.isDark) 16 else 10), color, 7)
            setOnClickListener { action() }
            isClickable = true
            isFocusable = false
            isFocusableInTouchMode = false
        }

    private fun disabledActionButton(title: String): TextView =
        TextView(this).apply {
            text = title
            textSize = 13f
            typeface = Typeface.DEFAULT_BOLD
            gravity = Gravity.CENTER
            setTextColor(palette.muted)
            background = boxDrawable(withAlpha(palette.muted, if (palette.isDark) 14 else 8), palette.line, 7)
            isEnabled = false
            isClickable = false
            isFocusable = false
            isFocusableInTouchMode = false
            layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48))
        }

    private fun navItem(title: String, selected: Boolean, action: () -> Unit): TextView =
        TextView(this).apply {
            text = title
            textSize = 12f
            typeface = Typeface.DEFAULT_BOLD
            gravity = Gravity.CENTER
            setTextColor(if (selected) palette.amber else palette.muted)
            setOnClickListener { action() }
            isClickable = true
            isFocusable = false
            isFocusableInTouchMode = false
        }

    private fun input(hint: String, value: String): EditText =
        EditText(this).apply {
            setText(value)
            this.hint = hint
            textSize = 13f
            setSingleLine(true)
            setTextColor(palette.text)
            setHintTextColor(palette.muted)
            setPadding(dp(12), 0, dp(12), 0)
            background = boxDrawable(palette.panelAlt, palette.line, 7)
            minHeight = dp(46)
            inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
        }

    private fun text(value: String, size: Float, style: Int, color: Int = palette.text): TextView =
        TextView(this).apply {
            text = value
            textSize = size
            setTextColor(color)
            typeface = Typeface.create(Typeface.DEFAULT, style)
            includeFontPadding = true
        }

    private fun thinDivider(): View =
        View(this).apply {
            setBackgroundColor(palette.line)
            layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(1))
        }

    private fun screenContent(): LinearLayout =
        LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(dp(18), dp(16) + statusBarInset(), dp(18), dp(22))
        }

    private fun wrapScroll(content: View): View =
        ScrollView(this).apply {
            setBackgroundColor(palette.page)
            isFillViewport = false
            addView(content, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
        }

    private fun emptyState(message: String): View =
        section("Состояние", text(message, 13f, Typeface.NORMAL, palette.muted))

    private fun rankedMarkets(data: BotSnapshot): List<MarketItem> =
        orderedMarkets(data.markets).sortedWith(
            compareByDescending<MarketItem> { marketRankScore(it, data.signalsBySymbol[it.symbol]) }
                .thenBy { fixedSymbolIndex(it.symbol) }
                .thenBy { it.symbol },
        )

    private fun orderedMarkets(markets: List<MarketItem>): List<MarketItem> =
        markets.sortedWith(compareBy({ fixedSymbolIndex(it.symbol) }, { it.symbol }))

    private fun marketRankScore(market: MarketItem, signal: SignalData?): Double {
        val actionScore = when (normalizedAction(signal?.action)) {
            "BUY" -> 10_000.0
            "SELL" -> 5_000.0
            "WAIT" -> 1_000.0
            else -> 0.0
        }
        val edge = signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0
        val probability = signal?.probabilityUp ?: market.forecast?.probabilityUp ?: 0.0
        return actionScore + edge * 100.0 + normalizedProbability(probability)
    }

    private fun selectedMarket(data: BotSnapshot): MarketItem? {
        ensureSelectedSymbol()
        return data.markets.firstOrNull { it.symbol == prefs.selectedSymbol }
            ?: orderedMarkets(data.markets).firstOrNull()
    }

    private fun ensureSelectedSymbol() {
        val data = snapshot ?: return
        if (data.markets.none { it.symbol == prefs.selectedSymbol }) {
            prefs.selectedSymbol = orderedMarkets(data.markets).firstOrNull()?.symbol ?: "BTCUSDT"
        }
    }

    private fun latestPrice(market: MarketItem): Double =
        market.ticker?.lastPrice ?: market.candles.lastOrNull()?.close ?: 0.0

    private fun lastCandleTime(market: MarketItem): String {
        val timestamp = market.candles.lastOrNull()?.timestamp ?: return "нет свечей"
        val millis = if (timestamp > 100_000_000_000L) timestamp else timestamp * 1000L
        return SimpleDateFormat("dd.MM HH:mm", Locale("ru", "RU")).format(Date(millis))
    }

    private fun shortIsoTime(value: String): String {
        if (value.isBlank()) return "-"
        return try {
            val normalized = value.replace("Z", "+00:00")
            val millis = java.time.OffsetDateTime.parse(normalized).toInstant().toEpochMilli()
            SimpleDateFormat("dd.MM HH:mm", Locale("ru", "RU")).format(Date(millis))
        } catch (_: Exception) {
            value.take(16).replace("T", " ")
        }
    }

    private fun setRetrainInterval(hours: Int) {
        prefs.retrainIntervalHours = hours
        if (prefs.retrainScheduleEnabled) {
            RetrainScheduler.schedule(this, hours)
        }
        toast("Интервал retrain: $hours ч")
        if (activeTab == "settings" && snapshot != null && updateSettingsScreen(snapshot!!)) {
            return
        }
        render()
    }

    private fun triggerRetrainNow() {
        if (retrainRequestInFlight) return
        retrainRequestInFlight = true
        if (activeTab != "settings" || snapshot == null || !updateSettingsScreen(snapshot!!)) {
            render()
        }
        executor.execute {
            try {
                val message = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).requestRetrain()
                mainHandler.post {
                    retrainRequestInFlight = false
                    toast(message.take(180))
                    refreshData(silent = false)
                }
            } catch (error: Exception) {
                mainHandler.post {
                    retrainRequestInFlight = false
                    toast(error.message ?: "retrain не запущен")
                    refreshData(silent = false)
                }
            }
        }
    }

    private fun sendControl(stop: Boolean) {
        executor.execute {
            try {
                val api = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken)
                if (stop) api.stopBot() else api.startBot()
                mainHandler.post {
                    toast(if (stop) "Команда остановки отправлена" else "Команда запуска отправлена")
                    refreshData(silent = false)
                }
            } catch (error: Exception) {
                mainHandler.post { toast(error.message ?: "Команда не выполнена") }
            }
        }
    }

    private fun fixedSymbolIndex(symbol: String): Int {
        val index = FIXED_SYMBOLS.indexOf(symbol.uppercase(Locale.US))
        return if (index >= 0) index else FIXED_SYMBOLS.size + 1
    }

    private fun trainingStatusSignature(retrain: JSONObject): String =
        (retrain.optJSONObject("coordination") ?: JSONObject()).toString()

    private fun retrainBlockSignature(data: BotSnapshot?): String =
        listOf(
            retrainRequestInFlight.toString(),
            prefs.retrainScheduleEnabled.toString(),
            prefs.retrainIntervalHours.toString(),
            data?.retrain?.toString().orEmpty(),
            data?.backtest?.optJSONObject("full_replay")?.toString().orEmpty(),
        ).joinToString("|")

    private fun liveBlockSignature(data: BotSnapshot?): String {
        val config = data?.config ?: JSONObject()
        return listOf(
            data?.mode.orEmpty(),
            data?.running?.toString().orEmpty(),
            config.optBoolean("live_ready", false).toString(),
            config.optDouble("live_order_max_usdt", 0.0).toString(),
            config.optDouble("risk_per_trade_percent", 0.0).toString(),
        ).joinToString("|")
    }

    private fun positionsSignature(positions: List<PositionData>): String =
        positions.map { positionKey(it) }.sorted().joinToString("|")

    private fun positionKey(position: PositionData): String =
        when {
            position.id != null && position.id > 0L -> "id:${position.id}"
            position.openedAt.isNotBlank() -> "${position.symbol}:${position.openedAt}"
            else -> position.symbol
        }

    private fun closedTradesSignature(trades: List<ClosedTradeData>, summary: ClosedTradesSummary): String =
        listOf(
            summary.trades.toString(),
            number(summary.netPnl, 8),
            number(summary.feeUsdt, 8),
            number(summary.winRate, 8),
            trades.take(10).joinToString(";") { trade ->
                listOf(
                    trade.closedAt,
                    trade.symbol,
                    number(trade.qty, 8),
                    priceOrNone(trade.exitPrice),
                    number(trade.netPnl, 8),
                ).joinToString(",")
            },
        ).joinToString("|")

    private fun trainingJobLabel(status: String, phase: String): String =
        when (status) {
            "pending" -> "ждет Windows-agent"
            "completed" -> "завершено успешно"
            "failed" -> "ошибка обучения"
            "running" -> when (phase) {
                "claimed" -> "задание получено"
                "training" -> "идет обучение"
                "guard" -> "проверка качества"
                "uploading" -> "загрузка результата"
                else -> "идет работа"
            }
            else -> "готов к запуску"
        }

    private fun trainingJobColor(status: String, phase: String): Int =
        when {
            status == "completed" -> palette.green
            status == "failed" -> palette.red
            status == "pending" || status == "running" || phase.isNotBlank() -> palette.amber
            else -> palette.text
        }

    private fun normalizedAction(raw: String?): String {
        val value = raw.orEmpty().uppercase(Locale.US)
        return when {
            value.contains("BUY") -> "BUY"
            value.contains("SELL") || value.contains("EXIT") -> "SELL"
            value.contains("HOLD") || value.contains("WAIT") -> "WAIT"
            value.contains("SKIP") || value.contains("BLOCK") -> "SKIP"
            else -> "WAIT"
        }
    }

    private fun actionColor(action: String): Int =
        when (action) {
            "BUY" -> palette.green
            "SELL" -> palette.red
            "WAIT" -> palette.amber
            else -> palette.muted
        }

    private fun modeBadgeText(): String =
        when {
            snapshot == null && lastError.isNotBlank() -> "Нет связи"
            snapshot?.running == true -> "Активно"
            snapshot != null -> "Пауза"
            else -> "Проверка"
        }

    private fun modeLabel(value: String): String =
        when (value.lowercase(Locale.US)) {
            "live" -> "live"
            "paper" -> "paper"
            "backtest" -> "backtest"
            else -> value.ifBlank { "нет данных" }
        }

    private fun authParts(value: String): Pair<String, String> {
        val trimmed = value.trim()
        if (trimmed.isBlank() || trimmed.startsWith("Bearer ", ignoreCase = true)) {
            return "" to ""
        }
        val basicPrefix = "Basic "
        val raw = if (trimmed.startsWith(basicPrefix, ignoreCase = true)) "" else trimmed
        val separator = raw.indexOf(':')
        return if (separator >= 0) {
            raw.substring(0, separator) to raw.substring(separator + 1)
        } else {
            raw to ""
        }
    }

    private fun authToken(login: String, password: String): String {
        val cleanLogin = login.trim()
        val cleanPassword = password.trim()
        return if (cleanLogin.isBlank() && cleanPassword.isBlank()) "" else "$cleanLogin:$cleanPassword"
    }

    private fun modelLabel(value: String): String =
        when {
            value.contains("gru", ignoreCase = true) -> "PyTorch GRU"
            value.contains("lstm", ignoreCase = true) -> "PyTorch LSTM"
            value.isBlank() -> "нет данных"
            else -> value
        }

    private fun yesNo(value: Boolean): String = if (value) "да" else "нет"

    private fun exitSignalText(position: PositionData): String {
        val action = normalizedAction(position.exitAction)
        val reason = position.exitReason.ifBlank { "нет причины" }
        return if (action == "SELL") "SELL сейчас: ${reason.take(26)}" else "держим: ${reason.take(30)}"
    }

    private fun protectiveExitText(position: PositionData): String {
        val trailing = position.atrTrailingStop ?: position.trailingStop
        if (trailing != null && trailing > 0.0) {
            return "trailing ${price(trailing)}"
        }
        val stop = position.stopLoss
        if (position.stopLossExitEnabled && stop != null && stop > 0.0) {
            return "stop-loss ${price(stop)}"
        }
        return "stop-loss выключен"
    }

    private fun protectiveExitColor(position: PositionData): Int =
        when {
            position.atrTrailingStop != null || position.trailingStop != null -> palette.amber
            position.stopLossExitEnabled && position.stopLoss != null -> palette.red
            else -> palette.muted
        }

    private fun colorForSigned(value: String): Int =
        when {
            value.trim().startsWith("-") -> palette.red
            value.trim().startsWith("+") -> palette.green
            else -> palette.text
        }

    private fun colorForSigned(value: Double): Int =
        when {
            value > 0.0 -> palette.green
            value < 0.0 -> palette.red
            else -> palette.text
        }

    private fun money(value: Double, digits: Int = 2): String =
        "%,.${digits}f USDT".format(Locale.US, value)

    private fun signedMoney(value: Double): String =
        "${if (value >= 0) "+" else ""}${money(value)}"

    private fun price(value: Double): String =
        when {
            value >= 1000.0 -> "%,.1f".format(Locale.US, value)
            value >= 1.0 -> "%,.4f".format(Locale.US, value)
            value > 0.0 -> "%.8f".format(Locale.US, value).trimEnd('0').trimEnd('.')
            else -> "0"
        }

    private fun priceOrNone(value: Double?): String =
        if (value != null && value > 0.0) price(value) else "нет"

    private fun number(value: Double, digits: Int = 2): String =
        "%,.${digits}f".format(Locale.US, value)

    private fun percent(value: Double, digits: Int = 2): String =
        "%.${digits}f%%".format(Locale.US, value)

    private fun signedPercent(value: Double, digits: Int = 2): String =
        "${if (value >= 0) "+" else ""}${percent(value, digits)}"

    private fun probability(value: Double): String =
        percent(normalizedProbability(value), 0)

    private fun normalizedProbability(value: Double): Double =
        if (abs(value) <= 1.0) value * 100.0 else value

    private fun boxDrawable(fill: Int, stroke: Int, radiusDp: Int): GradientDrawable =
        GradientDrawable().apply {
            setColor(fill)
            cornerRadius = dp(radiusDp).toFloat()
            if (stroke != Color.TRANSPARENT) {
                setStroke(dp(1), stroke)
            }
        }

    private fun withAlpha(color: Int, alpha: Int): Int =
        Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))

    private fun View.top(value: Int): View =
        apply {
            layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
                setMargins(0, value, 0, 0)
            }
        }

    private fun View.left(value: Int): View =
        apply {
            layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
                setMargins(value, 0, 0, 0)
            }
        }

    private fun dp(value: Int): Int =
        (value * resources.displayMetrics.density).toInt()

    private fun statusBarInset(): Int =
        systemBarInset("status_bar_height")

    private fun navigationBarInset(): Int =
        systemBarInset("navigation_bar_height")

    private fun systemBarInset(name: String): Int {
        val id = resources.getIdentifier(name, "dimen", "android")
        return if (id > 0) resources.getDimensionPixelSize(id) else 0
    }

    private fun toast(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }

    private companion object {
        val FIXED_SYMBOLS = listOf(
            "BTCUSDT",
            "ETHUSDT",
            "HYPEUSDT",
            "SOLUSDT",
            "XRPUSDT",
            "XPLUSDT",
            "WLDUSDT",
            "MNTUSDT",
            "HUSDT",
            "XAUTUSDT",
            "IPUSDT",
            "AAVEUSDT",
        )
    }
}
