from __future__ import annotations

import json
from contextlib import asynccontextmanager
from typing import Any

from fastapi import FastAPI, HTTPException, Response
from fastapi.responses import JSONResponse, PlainTextResponse

from crypto_spot_bot.analytics import analytics_snapshot
from crypto_spot_bot.bot import CryptoSpotBot
from crypto_spot_bot.bybit import BybitClient
from crypto_spot_bot.config import Settings, load_settings, update_env_value
from crypto_spot_bot.execution import LiveBroker, PaperBroker
from crypto_spot_bot.learning import TradeLearner
from crypto_spot_bot.market_data import MarketData
from crypto_spot_bot.patterns import PatternAnalyzer
from crypto_spot_bot.reconciliation import reconciliation_snapshot
from crypto_spot_bot.storage import Storage
from crypto_spot_bot.strategy import SpotStrategy
from crypto_spot_bot.time_series import TimeSeriesForecaster
from crypto_spot_bot.training_coordination import TrainingCoordinator


WEB_UI_REMOVED_MESSAGE = "Web UI removed. Use the Android TradeBot AI app and /api/* endpoints."


def create_app(settings: Settings | None = None) -> FastAPI:
    settings = settings or load_settings()
    storage = Storage(settings.database_path)
    runtime_fast_trading = storage.get_runtime("fast_trading_enabled", None)
    if isinstance(runtime_fast_trading, bool):
        settings.fast_trading_enabled = runtime_fast_trading
    client = BybitClient(settings)
    market = MarketData(settings, client, storage)
    broker: PaperBroker | LiveBroker
    if settings.trading_mode == "live":
        broker = LiveBroker(settings, storage, client)
    else:
        broker = PaperBroker(settings, storage)
    strategy = SpotStrategy(settings)
    pattern_analyzer = PatternAnalyzer()
    learner = TradeLearner(settings, storage)
    forecaster = TimeSeriesForecaster(settings)
    bot = CryptoSpotBot(settings, storage, market, broker, strategy, pattern_analyzer, learner, forecaster)
    training = TrainingCoordinator(settings.time_series_lstm_model_path.parent)

    @asynccontextmanager
    async def lifespan(_: FastAPI):
        await bot.start()
        try:
            yield
        finally:
            await bot.stop()

    app = FastAPI(title="Крипто спот-бот", lifespan=lifespan)
    app.state.settings = settings
    app.state.storage = storage
    app.state.bot = bot
    app.state.market = market
    app.state.training = training

    @app.get("/", response_class=PlainTextResponse, status_code=410)
    async def index() -> str:
        return WEB_UI_REMOVED_MESSAGE

    @app.get("/api/health")
    async def health() -> dict[str, Any]:
        return {"ok": True, "running": bot.running, "mode": settings.trading_mode}

    @app.get("/api/status")
    async def status() -> dict[str, Any]:
        return {
            "status": bot.status().as_dict(),
            "account": bot.account_snapshot(),
            "positions": bot.positions_snapshot(),
            "learning": bot.learning_snapshot(),
            "latest_equity": storage.latest_equity(),
        }

    @app.get("/api/markets")
    async def markets() -> dict[str, Any]:
        return market.snapshot()

    @app.get("/api/trades")
    async def trades(limit: int = 80) -> dict[str, Any]:
        row_limit = _limit(limit)
        return {
            "items": storage.recent_trades(row_limit),
            "closed_items": storage.closed_trades(row_limit),
            "closed_summary": storage.closed_trade_summary(),
        }

    @app.get("/api/signals")
    async def signals(limit: int = 120) -> dict[str, Any]:
        return {"items": storage.recent_signals(_limit(limit))}

    @app.get("/api/events")
    async def events(limit: int = 120) -> dict[str, Any]:
        return {"items": storage.recent_events(_limit(limit))}

    @app.get("/api/analytics")
    async def analytics() -> dict[str, Any]:
        return analytics_snapshot(settings, storage)

    @app.get("/api/quality")
    async def quality() -> dict[str, Any]:
        return market.snapshot().get("quality", {})

    @app.get("/api/reconciliation")
    async def reconciliation() -> dict[str, Any]:
        return reconciliation_snapshot(
            settings=settings,
            storage=storage,
            client=client,
            instruments=market.instruments,
        )

    @app.get("/api/backtest")
    async def backtest() -> dict[str, Any]:
        return _runtime_json(settings, "torch_threshold_calibration.json")

    @app.get("/api/retrain")
    async def retrain() -> dict[str, Any]:
        data = _runtime_json(settings, "torch_retrain_guard.json")
        data["coordination"] = training.status()
        return data

    @app.get("/api/training/status")
    async def training_status() -> dict[str, Any]:
        return training.status()

    @app.post("/api/training/retrain")
    async def training_retrain(payload: dict[str, Any] | None = None) -> dict[str, Any]:
        return training.request_retrain(payload)

    @app.post("/api/training/heartbeat")
    async def training_heartbeat(payload: dict[str, Any] | None = None) -> dict[str, Any]:
        return training.heartbeat(payload)

    @app.post("/api/training/claim")
    async def training_claim(payload: dict[str, Any] | None = None) -> dict[str, Any]:
        return training.claim(payload)

    @app.post("/api/training/jobs/{job_id}/artifacts/chunk")
    async def training_artifact_chunk(job_id: str, payload: dict[str, Any]) -> dict[str, Any]:
        try:
            return training.save_artifact_chunk(job_id, payload)
        except ValueError as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc

    @app.post("/api/training/jobs/{job_id}/progress")
    async def training_progress(job_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
        try:
            return training.progress(job_id, payload)
        except ValueError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc

    @app.post("/api/training/jobs/{job_id}/complete")
    async def training_complete(job_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
        try:
            return training.complete(job_id, payload)
        except ValueError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc

    @app.get("/api/config")
    async def config() -> dict[str, Any]:
        return _safe_config(settings)

    @app.post("/api/config/fast-trading")
    async def set_fast_trading(payload: dict[str, Any]) -> dict[str, Any]:
        enabled = _enabled_from_payload(payload)
        env_persisted = _apply_fast_trading(settings, storage, enabled)
        response = _safe_config(settings)
        response["env_persisted"] = env_persisted
        return response

    @app.post("/api/control/start")
    async def start() -> dict[str, Any]:
        await bot.start()
        return bot.status().as_dict()

    @app.post("/api/control/stop")
    async def stop() -> dict[str, Any]:
        await bot.stop()
        return bot.status().as_dict()

    @app.get("/metrics")
    async def metrics() -> Response:
        account = bot.account_snapshot()
        lines = [
            "# HELP tradebot_equity_usdt Current account equity.",
            "# TYPE tradebot_equity_usdt gauge",
            f"tradebot_equity_usdt {account['equity']:.8f}",
            "# HELP tradebot_cash_usdt Current free USDT cash.",
            "# TYPE tradebot_cash_usdt gauge",
            f"tradebot_cash_usdt {account['cash']:.8f}",
            "# HELP tradebot_open_positions Open positions count.",
            "# TYPE tradebot_open_positions gauge",
            f"tradebot_open_positions {len(bot.positions_snapshot())}",
            "# HELP tradebot_websocket_connected Bybit WebSocket connection status.",
            "# TYPE tradebot_websocket_connected gauge",
            f"tradebot_websocket_connected {1 if market.ws_connected else 0}",
            "# HELP tradebot_fast_trading_enabled Fast trading mode status.",
            "# TYPE tradebot_fast_trading_enabled gauge",
            f"tradebot_fast_trading_enabled {1 if settings.fast_trading_enabled else 0}",
            "# HELP tradebot_loop_interval_seconds Effective bot decision loop interval.",
            "# TYPE tradebot_loop_interval_seconds gauge",
            f"tradebot_loop_interval_seconds {settings.effective_loop_interval_seconds:.4f}",
        ]
        return PlainTextResponse("\n".join(lines) + "\n")

    @app.exception_handler(Exception)
    async def error_handler(_, exc: Exception) -> JSONResponse:
        storage.event(f"API error: {exc}", "ERROR")
        return JSONResponse({"error": str(exc)}, status_code=500)

    return app


def _limit(value: int) -> int:
    return max(1, min(int(value), 500))


def _enabled_from_payload(payload: dict[str, Any]) -> bool:
    value = payload.get("enabled")
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        return value.strip().lower() in {"1", "true", "yes", "y", "on", "вкл", "включено"}
    return bool(value)


def _apply_fast_trading(settings: Settings, storage: Storage, enabled: bool) -> bool:
    settings.fast_trading_enabled = enabled
    storage.set_runtime("fast_trading_enabled", enabled)
    env_persisted = True
    try:
        update_env_value(settings.env_file_path, "FAST_TRADING_ENABLED", "true" if enabled else "false")
    except OSError as exc:
        env_persisted = False
        storage.event(f"Быстрая торговля изменена только в runtime, .env не записан: {exc}", "WARN")
    state = "включена" if enabled else "выключена"
    storage.event(f"Быстрая торговля {state}")
    return env_persisted


def _safe_config(settings: Settings) -> dict[str, Any]:
    return {
        "trading_mode": settings.trading_mode,
        "bybit_testnet": settings.bybit_testnet,
        "starting_balance_usdt": settings.starting_balance_usdt,
        "auto_select_symbols": settings.auto_select_symbols,
        "top_symbols_count": settings.top_symbols_count,
        "symbols": settings.symbols,
        "strategy_mode": settings.strategy_mode,
        "base_interval": settings.base_interval,
        "kline_limit": settings.kline_limit,
        "trend_interval": settings.trend_interval,
        "trend_kline_limit": settings.trend_kline_limit,
        "loop_interval_seconds": settings.loop_interval_seconds,
        "fast_trading_enabled": settings.fast_trading_enabled,
        "fast_loop_interval_seconds": settings.fast_loop_interval_seconds,
        "effective_loop_interval_seconds": settings.effective_loop_interval_seconds,
        "fast_entry_cooldown_seconds": settings.fast_entry_cooldown_seconds,
        "effective_entry_cooldown_seconds": settings.effective_entry_cooldown_seconds,
        "max_entries_per_minute": settings.max_entries_per_minute,
        "websocket_enabled": settings.websocket_enabled,
        "min_signal_confidence": settings.min_signal_confidence,
        "max_spread_percent": settings.max_spread_percent,
        "min_24h_turnover_usdt": settings.min_24h_turnover_usdt,
        "pattern_analysis_enabled": settings.pattern_analysis_enabled,
        "pattern_score_weight": settings.pattern_score_weight,
        "learning_enabled": settings.learning_enabled,
        "learning_lookback_trades": settings.learning_lookback_trades,
        "learning_min_samples": settings.learning_min_samples,
        "learning_max_adjustment": settings.learning_max_adjustment,
        "learning_max_position_multiplier": settings.learning_max_position_multiplier,
        "min_position_usdt": settings.min_position_usdt,
        "max_position_usdt": settings.max_position_usdt,
        "max_symbol_exposure_usdt": settings.max_symbol_exposure_usdt,
        "max_total_exposure_usdt": settings.max_total_exposure_usdt,
        "max_open_positions": settings.max_open_positions,
        "max_positions_per_symbol": settings.max_positions_per_symbol,
        "grid_trading_enabled": settings.grid_trading_enabled,
        "grid_entry_confidence": settings.grid_entry_confidence,
        "grid_buy_zone": settings.grid_buy_zone,
        "grid_max_position_usdt": settings.grid_max_position_usdt,
        "rebound_trading_enabled": settings.rebound_trading_enabled,
        "rebound_entry_confidence": settings.rebound_entry_confidence,
        "rebound_min_probability": settings.rebound_min_probability,
        "rebound_max_position_usdt": settings.rebound_max_position_usdt,
        "kelly_sizing_enabled": settings.kelly_sizing_enabled,
        "kelly_fraction": settings.kelly_fraction,
        "kelly_max_fraction": settings.kelly_max_fraction,
        "risk_per_trade_percent": settings.risk_per_trade_percent,
        "risk_guard_enabled": settings.risk_guard_enabled,
        "risk_symbol_guard_enabled": settings.risk_symbol_guard_enabled,
        "risk_recent_trade_window": settings.risk_recent_trade_window,
        "risk_max_consecutive_losses": settings.risk_max_consecutive_losses,
        "risk_min_recent_profit_factor": settings.risk_min_recent_profit_factor,
        "risk_reduce_multiplier": settings.risk_reduce_multiplier,
        "atr_trailing_multiplier": settings.atr_trailing_multiplier,
        "trend_rsi_min": settings.trend_rsi_min,
        "trend_rsi_max": settings.trend_rsi_max,
        "time_series_forecast_enabled": settings.time_series_forecast_enabled,
        "time_series_min_candles": settings.time_series_min_candles,
        "time_series_forecast_horizon": settings.time_series_forecast_horizon,
        "time_series_min_edge_percent": settings.time_series_min_edge_percent,
        "time_series_min_probability_up": settings.time_series_min_probability_up,
        "time_series_min_confidence": settings.time_series_min_confidence,
        "time_series_max_adjustment": settings.time_series_max_adjustment,
        "time_series_lstm_enabled": settings.time_series_lstm_enabled,
        "time_series_lstm_model_path": str(settings.time_series_lstm_model_path),
        "time_series_probe_enabled": settings.time_series_probe_enabled,
        "time_series_probe_min_edge_percent": settings.time_series_probe_min_edge_percent,
        "time_series_probe_min_probability_up": settings.time_series_probe_min_probability_up,
        "time_series_probe_size_multiplier": settings.time_series_probe_size_multiplier,
        "time_series_rebound_fallback_enabled": settings.time_series_rebound_fallback_enabled,
        "time_series_model_artifact": _time_series_model_artifact(settings),
        "stop_loss_percent": settings.stop_loss_percent,
        "stop_loss_exit_enabled": settings.stop_loss_exit_enabled,
        "take_profit_percent": settings.take_profit_percent,
        "trailing_stop_percent": settings.trailing_stop_percent,
        "min_hold_seconds": settings.min_hold_seconds,
        "min_exit_net_percent": settings.min_exit_net_percent,
        "entry_cooldown_seconds": settings.entry_cooldown_seconds,
        "max_daily_drawdown_usdt": settings.max_daily_drawdown_usdt,
        "min_cash_reserve_usdt": settings.min_cash_reserve_usdt,
        "taker_fee_rate": settings.taker_fee_rate,
        "slippage_rate": settings.slippage_rate,
        "live_ready": settings.live_ready,
        "live_order_max_usdt": settings.live_order_max_usdt,
    }


def _runtime_json(settings: Settings, name: str) -> dict[str, Any]:
    path = settings.time_series_lstm_model_path.parent / name
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return {"available": False, "path": str(path)}
    if not isinstance(data, dict):
        return {"available": False, "path": str(path)}
    data["available"] = True
    data["path"] = str(path)
    return data


def _time_series_model_artifact(settings: Settings) -> dict[str, Any]:
    path = settings.time_series_lstm_model_path
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return {
            "available": False,
            "type": "missing",
            "label": "нет файла модели",
            "symbol_count": 0,
            "models": [],
        }
    if not isinstance(data, dict):
        return {
            "available": False,
            "type": "invalid",
            "label": "файл модели не распознан",
            "symbol_count": 0,
            "models": [],
        }
    artifact_type = str(data.get("type", "")).strip()
    symbols = data.get("symbols")
    rows = list(symbols.values()) if isinstance(symbols, dict) else []
    models = sorted(
        {
            _forecast_model_label(
                str(row.get("model", row.get("architecture", "lstm"))),
                torch_artifact=artifact_type == "pytorch_recurrent_forecaster",
            )
            for row in rows
            if isinstance(row, dict)
        }
    )
    if artifact_type != "pytorch_recurrent_forecaster":
        return {
            "available": False,
            "type": artifact_type or "unknown",
            "label": "устаревший файл модели не используется",
            "created_at": data.get("created_at", ""),
            "symbol_count": len(rows),
            "models": models,
        }
    return {
        "available": True,
        "type": artifact_type,
        "label": "PyTorch LSTM/GRU",
        "created_at": data.get("created_at", ""),
        "symbol_count": len(rows),
        "models": models,
        "feature_count": _artifact_feature_count(data, rows),
        "target_horizon": _artifact_target_horizon(data, rows),
        "direct_horizon": _artifact_direct_horizon(data, rows),
    }


def _artifact_feature_count(data: dict[str, Any], rows: list[Any]) -> int:
    feature_count = data.get("feature_count")
    if isinstance(feature_count, int):
        return feature_count
    counts = [
        int(row.get("input_size", 0))
        for row in rows
        if isinstance(row, dict) and isinstance(row.get("input_size"), int)
    ]
    return max(counts) if counts else 1


def _artifact_target_horizon(data: dict[str, Any], rows: list[Any]) -> int:
    horizon = data.get("target_horizon")
    if isinstance(horizon, int):
        return horizon
    horizons = [
        int(row.get("target_horizon", 0))
        for row in rows
        if isinstance(row, dict) and isinstance(row.get("target_horizon"), int)
    ]
    return max(horizons) if horizons else 0


def _artifact_direct_horizon(data: dict[str, Any], rows: list[Any]) -> bool:
    if bool(data.get("direct_horizon")):
        return True
    return any(isinstance(row, dict) and bool(row.get("direct_horizon")) for row in rows)


def _forecast_model_label(model: str, *, torch_artifact: bool = False) -> str:
    normalized = model.strip().lower()
    if normalized in {"torch_lstm", "lstm"} and torch_artifact:
        return "PyTorch LSTM"
    if normalized in {"torch_gru", "gru"} and torch_artifact:
        return "PyTorch GRU"
    if normalized == "lstm":
        return "устаревший артефакт"
    if normalized == "gru":
        return "устаревший артефакт"
    return model
