from __future__ import annotations

import json
from pathlib import Path
from typing import Annotated, Any

import typer
from rich.console import Console

from .config import EasyBinanceConfig, load_credentials
from .core import EasyBinance
from .errors import EasyBinanceError
from .formatting import render_output
from .products import PRODUCTS, get_product_spec
from .utils import load_json_object, parse_key_value_pairs

app = typer.Typer(
    help="Unified CLI for Binance derivatives APIs built on official Binance SDKs.",
    no_args_is_help=True,
    rich_markup_mode="markdown",
)
console = Console()


class AppContext:
    def __init__(self, client: EasyBinance) -> None:
        self.client = client


def get_ctx(ctx: typer.Context) -> AppContext:
    return ctx.obj


@app.callback()
def main_callback(
    ctx: typer.Context,
    env_file: Annotated[Path | None, typer.Option(help="Custom .env file path.")] = None,
    api_key: Annotated[str | None, typer.Option(help="Override BINANCE_API_KEY.")] = None,
    api_secret: Annotated[str | None, typer.Option(help="Override BINANCE_API_SECRET.")] = None,
    testnet: Annotated[bool, typer.Option(help="Use testnet URLs when supported.")] = False,
    rest_timeout: Annotated[int, typer.Option(help="REST request timeout in milliseconds.")] = 1000,
    ws_timeout: Annotated[int, typer.Option(help="WebSocket API timeout in milliseconds.")] = 5000,
    reconnect_delay: Annotated[int, typer.Option(help="WebSocket reconnect delay in milliseconds.")] = 5000,
    pool_size: Annotated[int, typer.Option(help="WebSocket pool size.")] = 2,
    retries: Annotated[int, typer.Option(help="REST retry count.")] = 3,
    backoff: Annotated[int, typer.Option(help="REST retry backoff in milliseconds.")] = 1000,
    keep_alive: Annotated[bool, typer.Option(help="Enable HTTP keep-alive.")] = True,
    rest_url: Annotated[str | None, typer.Option(help="Override REST base URL.")] = None,
    ws_api_url: Annotated[str | None, typer.Option(help="Override WebSocket API URL.")] = None,
    ws_stream_url: Annotated[str | None, typer.Option(help="Override WebSocket stream URL.")] = None,
    proxy_json: Annotated[
        str | None,
        typer.Option(help="Proxy configuration as JSON object compatible with official SDK."),
    ] = None,
    time_unit: Annotated[str | None, typer.Option(help="Time unit passed to the official SDK.")] = None,
    return_rate_limits: Annotated[
        bool,
        typer.Option(help="Return WebSocket API rate limit payloads when supported."),
    ] = True,
) -> None:
    proxy = json.loads(proxy_json) if proxy_json else None
    config = EasyBinanceConfig(
        env_file=env_file,
        api_key=api_key,
        api_secret=api_secret,
        testnet=testnet,
        rest_timeout=rest_timeout,
        ws_timeout=ws_timeout,
        reconnect_delay=reconnect_delay,
        pool_size=pool_size,
        retries=retries,
        backoff=backoff,
        keep_alive=keep_alive,
        proxy=proxy,
        time_unit=time_unit,
        return_rate_limits=return_rate_limits,
        rest_url=rest_url,
        ws_api_url=ws_api_url,
        ws_stream_url=ws_stream_url,
    )
    ctx.obj = AppContext(EasyBinance(config=config))


@app.command("products")
def products_command(
    ctx: typer.Context,
    output: Annotated[str, typer.Option(help="Output format: json, yaml, table.")] = "table",
) -> None:
    client = get_ctx(ctx).client
    rows = [
        {
            "name": product.name,
            "aliases": ", ".join(product.aliases),
            "apis": ", ".join(product.supported_apis),
            "docs_url": product.docs_url,
        }
        for product in client.list_products()
    ]
    render_output(console, rows, output)


@app.command("apis")
def apis_command(
    ctx: typer.Context,
    product: str,
    output: Annotated[str, typer.Option(help="Output format: json, yaml, table.")] = "table",
) -> None:
    client = get_ctx(ctx).client
    spec = get_product_spec(product)
    rows = [{"product": spec.name, "api": api} for api in client.list_apis(spec.name)]
    render_output(console, rows, output)


@app.command("methods")
def methods_command(
    ctx: typer.Context,
    product: str,
    api: str,
    output: Annotated[str, typer.Option(help="Output format: json, yaml, table.")] = "table",
) -> None:
    methods = get_ctx(ctx).client.list_methods(product, api)
    rows = [
        {"name": method.name, "signature": method.signature, "summary": method.summary}
        for method in methods
    ]
    render_output(console, rows, output)


@app.command("describe")
def describe_command(
    ctx: typer.Context,
    product: str,
    api: str,
    method: str,
    output: Annotated[str, typer.Option(help="Output format: json, yaml, table.")] = "yaml",
) -> None:
    payload = get_ctx(ctx).client.describe_method(product, api, method)
    render_output(console, payload, output)


@app.command("credentials")
def credentials_command(
    ctx: typer.Context,
    output: Annotated[str, typer.Option(help="Output format: json, yaml, table.")] = "table",
) -> None:
    config = get_ctx(ctx).client.config
    loaded = load_credentials(
        env_file=config.env_file,
        api_key=config.api_key,
        api_secret=config.api_secret,
    )
    payload = {
        "source": loaded.source,
        "env_file": str(loaded.env_file) if loaded.env_file else None,
        "has_api_key": bool(loaded.api_key),
        "has_api_secret": bool(loaded.api_secret),
    }
    render_output(console, payload, output)


@app.command("call")
def call_command(
    ctx: typer.Context,
    product: str,
    api: str,
    method: str,
    param: Annotated[
        list[str] | None,
        typer.Option(help="Method parameters as KEY=VALUE. VALUE may be JSON."),
    ] = None,
    params_file: Annotated[
        Path | None,
        typer.Option(help="JSON file containing method parameters."),
    ] = None,
    output: Annotated[str, typer.Option(help="Output format: json, yaml, table.")] = "json",
) -> None:
    params: dict[str, Any] = {}
    params.update(load_json_object(str(params_file)) if params_file else {})
    params.update(parse_key_value_pairs(param))
    payload = get_ctx(ctx).client.call(product, api, method, params=params)
    render_output(console, payload, output)


@app.command("listen")
def listen_command(
    ctx: typer.Context,
    product: str,
    method: str,
    param: Annotated[
        list[str] | None,
        typer.Option(help="Stream parameters as KEY=VALUE. VALUE may be JSON."),
    ] = None,
    params_file: Annotated[
        Path | None,
        typer.Option(help="JSON file containing stream parameters."),
    ] = None,
    duration: Annotated[
        float | None,
        typer.Option(help="Subscription duration in seconds. Use 0 for indefinite."),
    ] = 5,
    max_messages: Annotated[
        int | None,
        typer.Option(help="Stop after N messages."),
    ] = None,
    output: Annotated[str, typer.Option(help="Output format: json, yaml, table.")] = "json",
) -> None:
    params: dict[str, Any] = {}
    params.update(load_json_object(str(params_file)) if params_file else {})
    params.update(parse_key_value_pairs(param))
    effective_duration = None if duration == 0 else duration
    payload = get_ctx(ctx).client.listen(
        product=product,
        method_name=method,
        params=params,
        duration=effective_duration,
        max_messages=max_messages,
    )
    render_output(console, payload, output)


def main() -> None:
    try:
        app()
    except EasyBinanceError as exc:
        console.print(f"[red]error:[/red] {exc}")
        raise typer.Exit(code=1) from exc
    except ValueError as exc:
        console.print(f"[red]error:[/red] {exc}")
        raise typer.Exit(code=1) from exc
