from __future__ import annotations

import asyncio
import sys
from decimal import Decimal
from pathlib import Path

import httpx
import typer
from rich.console import Console
from rich.table import Table

from . import __version__
from .ai_trader import is_ai_trade_executed_status, run_live_ai_trade_batch_once, run_live_ai_trade_once
from .auto_buy import run_live_auto_buy_once
from .auto_sell import run_live_auto_sell_once
from .backtest import run_backtest, run_futures_backtest
from .config import load_settings
from .email_notify import (
    EmailNotificationError,
    send_auto_buy_protection_failure_email,
    send_ai_trade_batch_email,
    send_ai_trade_email,
    send_futures_ai_trade_batch_email,
)
from .exchange import BinanceSpotClient
from .futures_ai_trader import FUTURES_FAILED_STATUSES, run_live_futures_ai_trade_batch_once
from .market_data import load_candles_csv
from .runner import run_trade_loop
from .spot_trade_sync import sync_spot_trade_fills
from .storage import Storage


def _configure_stdio_encoding(stdout=sys.stdout, stderr=sys.stderr) -> None:
    for stream in (stdout, stderr):
        reconfigure = getattr(stream, "reconfigure", None)
        if callable(reconfigure):
            try:
                reconfigure(encoding="utf-8", errors="replace")
            except Exception:
                pass


_configure_stdio_encoding()

app = typer.Typer(no_args_is_help=True)
console = Console()
error_console = Console(stderr=True)
_FAILED_AUTO_BUY_STATUSES = {"rollback_failed", "rollback_completed"}
_FAILED_AUTO_BUY_REASONS = {"pre-buy data error"}
_FAILED_AUTO_SELL_STATUSES = {"sell_failed"}
_FAILED_AUTO_SELL_REASONS = {"pre-sell data error"}
_FAILED_AI_TRADE_STATUSES = {"rollback_failed", "rollback_completed", "sell_failed"}
_FAILED_AI_TRADE_REASONS = {"pre-ai-trade data error", "AI交易前置数据获取失败"}


def _config_option() -> Path | None:
    return typer.Option(None, exists=True, file_okay=True, dir_okay=False, readable=True, help="Optional YAML config path")


def _exit_for_http_error() -> None:
    error_console.print("Binance API request failed. Check credentials, permissions, network, and endpoint status.")
    raise typer.Exit(1)


def _live_auto_buy_exit_code(result) -> int:
    if result.status in _FAILED_AUTO_BUY_STATUSES:
        return 1
    if result.reason in _FAILED_AUTO_BUY_REASONS:
        return 1
    return 0


def _live_auto_sell_exit_code(result) -> int:
    if result.status in _FAILED_AUTO_SELL_STATUSES:
        return 1
    if result.reason in _FAILED_AUTO_SELL_REASONS:
        return 1
    return 0


def _live_ai_trade_exit_code(result) -> int:
    if result.status in _FAILED_AI_TRADE_STATUSES:
        return 1
    if result.reason in _FAILED_AI_TRADE_REASONS:
        return 1
    return 0


def _live_ai_trade_batch_exit_code(result) -> int:
    if result.status == "failed":
        return 1
    if result.reason in _FAILED_AI_TRADE_REASONS:
        return 1
    if any(item.status in _FAILED_AI_TRADE_STATUSES for item in result.results):
        return 1
    return 0


def _live_futures_ai_trade_batch_exit_code(result) -> int:
    if result.status == "failed":
        return 1
    if any(item.status in FUTURES_FAILED_STATUSES for item in result.results):
        return 1
    return 0


def _format_optional_decimal(value) -> str:
    return "-" if value is None else str(value)


def _format_compact_decimal(value) -> str:
    if value is None:
        return "-"
    if isinstance(value, Decimal):
        normalized = value.normalize()
        if normalized == 0:
            return "0"
        return format(normalized, "f")
    return str(value)


def _parse_decimal_option(value: str, name: str, *, minimum: Decimal | None = None) -> Decimal:
    try:
        parsed = Decimal(value)
    except Exception:
        raise typer.BadParameter(f"{name} must be a decimal number") from None
    if not parsed.is_finite():
        raise typer.BadParameter(f"{name} must be finite")
    if minimum is not None and parsed < minimum:
        raise typer.BadParameter(f"{name} must be greater than or equal to {minimum}")
    return parsed


def _format_ai_trade_executed(status: str) -> str:
    return "yes" if is_ai_trade_executed_status(status) else "no"


def _split_hold_results(results):
    action_results = tuple(item for item in results if item.action != "HOLD")
    hold_results = tuple(item for item in results if item.action == "HOLD")
    return action_results, hold_results


def _print_ai_trade_result_table(title: str, results, *, include_executed: bool = False) -> None:
    console.print(title)
    columns = ["Run ID", "Status", "Symbol", "Action", "Confidence"]
    if include_executed:
        columns.append("Executed")
    columns.append("Reason")
    table = Table(*columns)
    for item in results:
        row = [
            str(item.run_id) if item.run_id is not None else "-",
            item.status,
            item.symbol or "-",
            item.action,
            _format_optional_decimal(item.confidence),
        ]
        if include_executed:
            row.append(_format_ai_trade_executed(item.status))
        row.append(item.reason)
        table.add_row(*row)
    console.print(table)


def _futures_result_table_lines(results) -> list[str]:
    headers = ["Summary", "Reason"]
    rows = [[_futures_result_summary(item), item.reason] for item in results]
    if not rows:
        rows = [["-", "-"]]
    widths = [max(len(header), *(len(row[index]) for row in rows)) for index, header in enumerate(headers)]
    separator = "| " + " | ".join("-" * width for width in widths) + " |"
    return [_plain_table_row(headers, widths), separator, *(_plain_table_row(row, widths) for row in rows)]


def _futures_result_summary(item) -> str:
    return (
        f"Run ID: {item.run_id if item.run_id is not None else '-'}; "
        f"Status: {item.status}; "
        f"Symbol: {item.symbol or '-'}; "
        f"Action: {item.action}; "
        f"Confidence: {_format_compact_decimal(item.confidence)}; "
        f"Leverage: {'-' if item.leverage is None else item.leverage}; "
        f"Margin: {_format_compact_decimal(item.margin_amount)}; "
        f"Close%: {'-' if item.close_percent is None else item.close_percent}"
    )


def _plain_table_row(values: list[str], widths: list[int]) -> str:
    return "| " + " | ".join(value.ljust(width) for value, width in zip(values, widths)) + " |"


def _validate_max_candles(value: int | None) -> int | None:
    if value is not None and value < 0:
        raise typer.BadParameter("max-candles must be greater than or equal to 0")
    return value


@app.command()
def version() -> None:
    console.print(__version__)


@app.command()
def backtest(
    csv_path: Path = typer.Argument(..., exists=True, readable=True),
    symbol: str = typer.Option("BTCUSDT", help="Trading symbol"),
    fast_window: int = typer.Option(5, min=1, help="Fast SMA window"),
    slow_window: int = typer.Option(20, min=1, help="Slow SMA window"),
) -> None:
    if fast_window >= slow_window:
        raise typer.BadParameter("fast-window must be less than slow-window")
    result = run_backtest(load_candles_csv(csv_path, symbol), fast_window, slow_window)
    table = Table("Metric", "Value")
    table.add_row("Candles", str(result.total_candles))
    table.add_row("Buy signals", str(result.buy_signals))
    table.add_row("Sell signals", str(result.sell_signals))
    table.add_row("Hold signals", str(result.hold_signals))
    console.print(table)


@app.command("futures-backtest")
def futures_backtest(
    csv_path: Path = typer.Argument(..., exists=True, readable=True),
    symbol: str = typer.Option("BTCUSDT", help="Trading symbol"),
    fast_window: int = typer.Option(5, min=1, help="Fast SMA window"),
    slow_window: int = typer.Option(20, min=1, help="Slow SMA window"),
    initial_equity: str = typer.Option("1000", help="Starting equity"),
    margin_amount: str = typer.Option("100", help="Margin allocated per trade"),
    leverage: int = typer.Option(1, min=1, help="Futures leverage multiplier"),
    fee_rate: str = typer.Option("0.0004", help="Fee rate per side"),
    slippage_rate: str = typer.Option("0.0001", help="Slippage rate per side"),
) -> None:
    if fast_window >= slow_window:
        raise typer.BadParameter("fast-window must be less than slow-window")
    parsed_initial_equity = _parse_decimal_option(initial_equity, "initial-equity", minimum=Decimal("0.00000001"))
    parsed_margin_amount = _parse_decimal_option(margin_amount, "margin-amount", minimum=Decimal("0.00000001"))
    parsed_fee_rate = _parse_decimal_option(fee_rate, "fee-rate", minimum=Decimal("0"))
    parsed_slippage_rate = _parse_decimal_option(slippage_rate, "slippage-rate", minimum=Decimal("0"))
    result = run_futures_backtest(
        load_candles_csv(csv_path, symbol),
        fast_window,
        slow_window,
        initial_equity=parsed_initial_equity,
        margin_amount=parsed_margin_amount,
        leverage=leverage,
        fee_rate=parsed_fee_rate,
        slippage_rate=parsed_slippage_rate,
    )
    table = Table("Metric", "Value")
    table.add_row("Candles", str(result.total_candles))
    table.add_row("Trades", str(result.trade_count))
    table.add_row("Wins", str(result.win_count))
    table.add_row("Losses", str(result.loss_count))
    win_rate = Decimal("0") if result.trade_count == 0 else Decimal(result.win_count) / Decimal(result.trade_count) * Decimal("100")
    table.add_row("Win rate", f"{_format_compact_decimal(win_rate)}%")
    table.add_row("Gross PnL", _format_compact_decimal(result.gross_pnl))
    table.add_row("Fees", _format_compact_decimal(result.total_fees))
    table.add_row("Slippage", _format_compact_decimal(result.total_slippage))
    table.add_row("Net PnL", _format_compact_decimal(result.net_pnl))
    table.add_row("Initial equity", _format_compact_decimal(result.initial_equity))
    table.add_row("Final equity", _format_compact_decimal(result.final_equity))
    table.add_row("Total return", f"{_format_compact_decimal(result.total_return_percent)}%")
    table.add_row("Max drawdown", f"{_format_compact_decimal(result.max_drawdown_percent)}%")
    console.print(table)


@app.command()
def orders(database_path: Path = typer.Option(Path("binance_quant.sqlite3"), help="SQLite database path")) -> None:
    storage = Storage(database_path)
    storage.initialize()
    rows = storage.list_orders()
    if not rows:
        console.print("No orders found")
        return
    table = Table("ID", "Symbol", "Side", "Status", "Client Order ID", "Dry Run")
    for row in rows:
        table.add_row(
            str(row["id"]),
            row["symbol"],
            row["side"],
            row["status"],
            row["client_order_id"],
            "yes" if row["dry_run"] else "no",
        )
    console.print(table)


@app.command()
def account(config: Path | None = _config_option()) -> None:
    settings = load_settings(config)
    client = BinanceSpotClient(
        api_key=settings.api_key,
        api_secret=settings.api_secret.get_secret_value(),
        base_url=settings.rest_base_url,
        base_urls=settings.rest_base_urls,
        recv_window=settings.recv_window,
        proxy_url=settings.proxy_url,
    )
    try:
        snapshot = client.account()
    except httpx.HTTPError:
        _exit_for_http_error()
    table = Table("Asset", "Free", "Locked")
    for asset, balance in sorted(snapshot.balances.items()):
        if balance.free or balance.locked:
            table.add_row(asset, str(balance.free), str(balance.locked))
    console.print(table)


@app.command("sync-spot-trades")
def sync_spot_trades(
    config: Path | None = _config_option(),
    symbol: list[str] | None = typer.Option(None, "--symbol", "-s", help="USDT spot symbol to sync"),
) -> None:
    settings = load_settings(config)
    symbols = tuple(item.strip().upper() for item in (symbol or ()) if item.strip()) or None
    try:
        result = sync_spot_trade_fills(settings, symbols=symbols)
    except httpx.HTTPError:
        _exit_for_http_error()
    console.print(f"Status: {result.status}")
    console.print(f"Reason: {result.reason}")
    console.print(f"Scanned symbols: {result.scanned_symbols}")
    console.print(f"Fetched trades: {result.fetched_trades}")
    console.print(f"Inserted trades: {result.inserted_trades}")
    if result.status == "rejected":
        raise typer.Exit(1)


@app.command("live-auto-buy-once")
def live_auto_buy_once(config: Path | None = _config_option()) -> None:
    settings = load_settings(config)
    try:
        result = run_live_auto_buy_once(settings)
    except httpx.HTTPError:
        _exit_for_http_error()
    console.print(f"Run ID: {result.run_id}")
    console.print(f"Status: {result.status}")
    console.print(f"Symbol: {result.symbol or '-'}")
    console.print(f"Reason: {result.reason}")
    if result.status in _FAILED_AUTO_BUY_STATUSES:
        try:
            send_auto_buy_protection_failure_email(settings, result)
        except EmailNotificationError:
            error_console.print("Email notification failed.")
            raise typer.Exit(1)
        except Exception:
            error_console.print("Email notification failed.")
            raise typer.Exit(1)
    exit_code = _live_auto_buy_exit_code(result)
    if exit_code:
        raise typer.Exit(exit_code)


@app.command("live-auto-sell-once")
def live_auto_sell_once(config: Path | None = _config_option()) -> None:
    settings = load_settings(config)
    try:
        result = run_live_auto_sell_once(settings)
    except httpx.HTTPError:
        _exit_for_http_error()
    console.print(f"Run ID: {result.run_id}")
    console.print(f"Status: {result.status}")
    console.print(f"Symbol: {result.symbol or '-'}")
    console.print(f"Reason: {result.reason}")
    exit_code = _live_auto_sell_exit_code(result)
    if exit_code:
        raise typer.Exit(exit_code)


@app.command("live-ai-trade-once")
def live_ai_trade_once(config: Path | None = _config_option()) -> None:
    settings = load_settings(config)
    try:
        result = run_live_ai_trade_once(settings)
    except httpx.HTTPError:
        _exit_for_http_error()
    console.print(f"Run ID: {result.run_id if result.run_id is not None else '-'}")
    console.print(f"Status: {result.status}")
    console.print(f"Symbol: {result.symbol or '-'}")
    console.print(f"Action: {result.action}")
    console.print(f"Confidence: {_format_optional_decimal(result.confidence)}")
    console.print(f"Reason: {result.reason}")
    try:
        send_ai_trade_email(settings, result)
    except EmailNotificationError:
        error_console.print("Email notification failed.")
        raise typer.Exit(1)
    except Exception:
        error_console.print("Email notification failed.")
        raise typer.Exit(1)
    exit_code = _live_ai_trade_exit_code(result)
    if exit_code:
        raise typer.Exit(exit_code)


@app.command("live-ai-trade-batch-once")
def live_ai_trade_batch_once(config: Path | None = _config_option()) -> None:
    settings = load_settings(config)
    try:
        result = run_live_ai_trade_batch_once(settings)
    except httpx.HTTPError:
        _exit_for_http_error()
    console.print(f"Status: {result.status}")
    console.print(f"Reason: {result.reason}")
    console.print(f"Max actions: {result.max_actions}")
    console.print(f"Executed actions: {result.executed_count}")
    console.print(f"Predictions: {result.prediction_count}")

    action_results, hold_results = _split_hold_results(result.results)
    if action_results:
        _print_ai_trade_result_table("Action results", action_results, include_executed=True)
    if hold_results:
        _print_ai_trade_result_table("HOLD predictions", hold_results)

    try:
        send_ai_trade_batch_email(settings, result)
    except EmailNotificationError:
        error_console.print("Email notification failed.")
        raise typer.Exit(1)
    except Exception:
        error_console.print("Email notification failed.")
        raise typer.Exit(1)

    exit_code = _live_ai_trade_batch_exit_code(result)
    if exit_code:
        raise typer.Exit(exit_code)


@app.command("live-futures-ai-trade-batch-once")
def live_futures_ai_trade_batch_once(config: Path | None = _config_option()) -> None:
    settings = load_settings(config)
    try:
        result = run_live_futures_ai_trade_batch_once(settings)
    except httpx.HTTPError:
        _exit_for_http_error()
    console.print(f"Status: {result.status}")
    console.print(f"Reason: {result.reason}")
    console.print(f"Max actions: {result.max_actions}")
    console.print(f"Executed actions: {result.executed_count}")
    console.print(f"Predictions: {result.prediction_count}")
    console.print(f"Daily net PnL: {_format_compact_decimal(result.daily_realized_pnl)}")
    console.print(f"Total isolated margin: {_format_compact_decimal(result.total_isolated_margin)}")
    action_results, hold_results = _split_hold_results(result.results)
    if action_results:
        console.print("Action results")
        for line in _futures_result_table_lines(action_results):
            console.print(line, soft_wrap=True)
    if hold_results:
        console.print("HOLD predictions")
        for line in _futures_result_table_lines(hold_results):
            console.print(line, soft_wrap=True)

    try:
        send_futures_ai_trade_batch_email(settings, result)
    except EmailNotificationError:
        error_console.print("Email notification failed.")
        raise typer.Exit(1)
    except Exception:
        error_console.print("Email notification failed.")
        raise typer.Exit(1)

    exit_code = _live_futures_ai_trade_batch_exit_code(result)
    if exit_code:
        raise typer.Exit(exit_code)


@app.command()
def trade(
    config: Path | None = _config_option(),
    max_candles: int | None = typer.Option(
        None,
        callback=_validate_max_candles,
        help="Stop after this many closed candles",
    ),
) -> None:
    settings = load_settings(config)
    try:
        summary = asyncio.run(run_trade_loop(settings, max_candles=max_candles))
    except httpx.HTTPError:
        _exit_for_http_error()
    console.print(f"Processed candles: {summary.processed_candles}")
    console.print(f"Signals: {summary.signals}")
    console.print(f"Approved: {summary.approved}")
    console.print(f"Rejected: {summary.rejected}")
    console.print(f"Orders: {summary.orders}")
