from __future__ import annotations

import argparse
import os
import sys
import time

from pmls.api.gamma_client import extract_outcome_token_ids, fetch_event_markets, fetch_market_by_slug, fetch_market_by_token_id
from pmls.api.get_order import list_orders
from pmls.api.orderbook_client import fetch_market_orderbooks
from pmls.api.positions import list_positions
from pmls.api.trade_hist import list_trade_history
from pmls.constants import DEFAULT_GAMMA_BASE_URL, DEFAULT_TIMEOUT_SECONDS
from pmls.credentials import create_clob_client_from_env
from pmls.display import _print_outcome_table
from pmls.trading.cancel import cancel_all_orders, cancel_by_order_ids
from pmls.trading.limit_order import run_limito
from pmls.trading.market_order import run_marketo


def _cmd_get_markets(args):
    slug = args.slug
    try:
        market_slugs = fetch_event_markets(
            slug,
            base_url=DEFAULT_GAMMA_BASE_URL,
            timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
        )
    except Exception as exc:
        print(exc, file=sys.stderr)
        return 1

    if not market_slugs:
        print(f"No markets found for event '{slug}'.", file=sys.stderr)
        return 1

    for market_slug in market_slugs:
        print(market_slug)
    return 0


def _cmd_orderbook(args):
    market_slug = args.market_slug
    depth = args.depth

    if depth < 1 or depth > 10:
        print("--depth must be between 1 and 10", file=sys.stderr)
        return 1

    def clear_terminal():
        os.system("cls" if os.name == "nt" else "clear")

    if args.once:
        try:
            snapshots = fetch_market_orderbooks(market_slug, depth)
        except Exception as exc:
            print(f"Orderbook fetch failed: {exc}", file=sys.stderr)
            return 1

        clear_terminal()
        for snapshot in snapshots:
            _print_outcome_table(snapshot)
        print("-" * 28)
    else:
        try:
            while True:
                try:
                    snapshots = fetch_market_orderbooks(market_slug, depth)
                except Exception as exc:
                    print(f"Orderbook fetch failed: {exc}", file=sys.stderr)
                    time.sleep(0.5)
                    continue

                clear_terminal()
                for snapshot in snapshots:
                    _print_outcome_table(snapshot)
                print("-" * 28)
                time.sleep(0.5)
        except KeyboardInterrupt:
            print("Stopped orderbook stream.")

    return 0


def _resolve_token_id(args) -> str | None:
    if args.token_id:
        return args.token_id

    outcome_name = args.outcome
    if not outcome_name:
        print("Provide either --token-id or --slug + --outcome.", file=sys.stderr)
        return None

    market_slug = args.market_slug
    if not market_slug:
        print("Provide either --token-id or --slug + --outcome.", file=sys.stderr)
        return None

    try:
        market = fetch_market_by_slug(market_slug)
        outcome_map = extract_outcome_token_ids(market)
    except Exception as exc:
        print(f"Failed to fetch market: {exc}", file=sys.stderr)
        return None

    token_id = outcome_map.get(outcome_name.lower())
    if not token_id:
        available = ", ".join(outcome_map.keys())
        print(f"Outcome '{outcome_name}' not found. Available: {available}", file=sys.stderr)
        return None

    return token_id


def _resolve_sell_all_size(token_id: str) -> float | None:
    try:
        positions = list_positions()
    except Exception as exc:
        print(f"Failed to fetch positions: {exc}", file=sys.stderr)
        return None
    match = next((p for p in positions if p.asset == token_id), None)
    if match is None or match.size is None:
        print(f"No open position for token {token_id}.", file=sys.stderr)
        return None
    return match.size


def _cmd_marketo(args):
    bet = args.bet
    price_display = args.price

    token_id = _resolve_token_id(args)
    if token_id is None:
        return 1

    if bet == -1 and args.side == "SELL":
        bet = _resolve_sell_all_size(token_id)
        if bet is None:
            return 1
    elif bet <= 0:
        print("Invalid input", file=sys.stderr)
        return 1
    if price_display is not None and (price_display <= 0 or price_display > 100):
        print("Invalid input", file=sys.stderr)
        return 1

    try:
        client = create_clob_client_from_env()
    except Exception as exc:
        print(exc, file=sys.stderr)
        return 1

    try:
        max_price = 1.0 if price_display is None else price_display / 100
        result = run_marketo(client, token_id, bet, max_price, side=args.side)
        print('Note: Marketable orders in sport market are delayed by 3 seconds.')
    except Exception as e:
        print(e, file=sys.stderr)
        return 1

    if not result:
        print("No available orders.")
        return 0

    if result.success:
        print(f"{args.side} order {result.order_id} submitted.")
        return 0
    else:
        print(result.errorMsg)
        return 1


def _cmd_limito(args):
    bet = args.bet
    price_display = args.price

    token_id = _resolve_token_id(args)
    if token_id is None:
        return 1

    if bet == -1 and args.side == "SELL":
        bet = _resolve_sell_all_size(token_id)
        if bet is None:
            return 1
    if bet <= 0 or price_display <= 0 or price_display > 100:
        print("Invalid input", file=sys.stderr)
        return 1

    try:
        client = create_clob_client_from_env()
    except Exception as exc:
        print(exc, file=sys.stderr)
        return 1

    try:
        result = run_limito(client, token_id, bet, price_display / 100, side=args.side)
    except Exception as e:
        print(e, file=sys.stderr)
        return 1

    if not result:
        print("No available orders.")
        return 0

    if result.success:
        print(f"{args.side} order {result.order_id} submitted.")
        return 0

    print(result.errorMsg)
    return 1


def _cmd_orders(args):
    try:
        cli = create_clob_client_from_env()
        orders = list_orders(cli)
        for o in orders:
            print(f'{o.outcome} {o.side} {o.size}@${o.price}, token ID: {o.token_id}')
    except Exception as e:
        print(e, file=sys.stderr)
        return 1

def _cmd_cancel(args):
    if not args.all and not args.id:
        print("Error: specify --all or --id <order-id,...>", file=sys.stderr)
        return 1

    def _handle_res(resp):
        if resp.not_canceled:
            for oid, reason in (resp.not_canceled or {}).items():
                print(f"Not canceled {oid}: {reason}", file=sys.stderr)
        if resp.canceled:
            print(f"Canceled {len(resp.canceled)} order(s): {', '.join(resp.canceled)}")
        else:
            print("No orders were canceled.")

    try:
        cli = create_clob_client_from_env()
        if args.all:
            resp = cancel_all_orders(cli)
        else:
            ids = [s.strip() for s in args.id.split(',')]
            resp = cancel_by_order_ids(cli, ids)
        _handle_res(resp)
        return 0

    except Exception as e:
        print(e, file=sys.stderr)
        return 1

def _cmd_trade(args):
    try:
        cli = create_clob_client_from_env()
        resp = list_trade_history(
            cli,
            token_id=args.token_id,
            before=args.before,
            after=args.after,
            market=args.market,
        )

        if args.query:
            resp = _filter_trades_by_query(resp, args.query)

        for o in resp:
            size = float(o.size or 0)
            price = float(o.price or 0)
            total = size * price
            print(f"{o.outcome} {o.side} {size}@{price}. Total: ${round(total, 3)} ({o.market})")

        return 0

    except Exception as e:
        print(e, file=sys.stderr)
        return 1


def _filter_trades_by_query(trades, query: str):
    q = query.lower()
    market_cache: dict[str, dict] = {}
    matched = []
    for trade in trades:
        # Fast path: outcome contains keyword
        if trade.outcome and q in trade.outcome.lower():
            matched.append(trade)
            continue
        
        # Slow path: look up market question via GAMMA; skip on any error
        if trade.asset_id:
            if trade.asset_id not in market_cache:
                try:
                    market_cache[trade.asset_id] = fetch_market_by_token_id(trade.asset_id)
                except Exception:
                    market_cache[trade.asset_id] = None
            market = market_cache[trade.asset_id]
            if market:
                question = (market.get("description") or "").lower()
                if q in question:
                    matched.append(trade)
    return matched


def _cmd_pnl(args):
    try:
        positions = list_positions(user=args.user)
    except Exception as exc:
        print(exc, file=sys.stderr)
        return 1

    if not positions:
        print("No positions.")
        return 0

    total_cost = total_value = total_unrealized = total_realized = 0.0

    for p in positions:
        cost       = p.initialValue or 0.0
        value      = p.currentValue or 0.0
        unrealized = p.cashPnl or 0.0
        realized   = p.realizedPnl or 0.0
        pct        = p.percentPnl or 0.0

        total_cost       += cost
        total_value      += value
        total_unrealized += unrealized
        total_realized   += realized

        sign = "+" if unrealized >= 0 else ""
        print(
            f"{p.title or '-'} {p.outcome or ''}: "
            f"{p.size or 0} shares @ {p.avgPrice or '-'} | "
            f"cur {p.curPrice or '-'} | "
            f"cost ${cost:.2f} | value ${value:.2f} | "
            f"unPnL {sign}{unrealized:.2f} ({pct:.1f}%) | realPnL ${realized:.2f}"
        )

    print("-" * 60)
    net = total_unrealized + total_realized
    sign = "+" if net >= 0 else ""
    print(
        f"TOTAL {len(positions)} position(s) | "
        f"cost ${total_cost:.2f} | value ${total_value:.2f} | "
        f"unPnL ${total_unrealized:.2f} | realPnL ${total_realized:.2f} | "
        f"net {sign}{net:.2f}"
    )
    return 0


def _cmd_pos(args):
    try:
        positions = list_positions(user=args.user)
    except Exception as exc:
        print(exc, file=sys.stderr)
        return 1

    if not positions:
        print("No positions.")
        return 0

    for p in positions:
        title = p.title or "-"
        size = p.size or "-"
        avg = p.avgPrice or "-"
        cur = p.curPrice or "-"
        value = p.currentValue or "-"
        pnl = p.cashPnl or "-"
        print(
            f"{title} {p.outcome or ''}: size={size} avg={avg} curPrice={cur} "
            f"value={value} pnl={pnl} token_id={p.asset or '-'}"
        )

    return 0
        
def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="pmls",
        description="Polymarket market helper CLI.",
    )
    subparsers = parser.add_subparsers(dest="command")

    # GET markets
    get_markets_parser = subparsers.add_parser(
        "get-markets", help="Get all market slugs associated with an event slug."
    )
    get_markets_parser.add_argument("slug", help="Event slug to resolve from GAMMA.")
    get_markets_parser.set_defaults(handler=_cmd_get_markets)

    # ORDERBOOK
    orderbook_parser = subparsers.add_parser(
        "orderbook", help="Stream market orderbook via REST polling, or fetch once with --once."
    )
    orderbook_parser.add_argument(
        "market_slug",
        help="Market slug to stream.",
    )
    orderbook_parser.add_argument(
        "--depth",
        type=int,
        default=3,
        help="Levels per side.",
    )
    orderbook_parser.add_argument(
        "-o", "--once",
        action="store_true",
        default=False,
        help="Fetch once via REST instead of continuous polling.",
    )
    orderbook_parser.set_defaults(handler=_cmd_orderbook)

    # MARKETO
    marketo_parser = subparsers.add_parser(
        "marketo", help="Place a market order (BUY or SELL) filled immediately at best available price."
    )
    marketo_parser.add_argument(
        "--slug", type=str, default=None, dest="market_slug",
        help="Market slug. Required with --outcome when --token-id is not provided.",
    )
    marketo_parser.add_argument(
        "--outcome", type=str, default=None, help="Outcome to trade (e.g. 'Yes').",
    )
    marketo_parser.add_argument(
        "--token-id", type=str, default=None, dest="token_id",
        help="Token ID to trade directly. Alternative to --slug + --outcome.",
    )
    marketo_parser.add_argument(
        "--bet", type=float, required=True,
        help="USDC to spend (BUY) or number of shares to sell (SELL). Use -1 with SELL to sell entire position."
    )
    marketo_parser.add_argument(
        "--price",
        type=float,
        default=None,
        dest="price",
        help="Max price cap in cents 0-100 (BUY only). Order rejected if market exceeds this.",
    )
    marketo_parser.add_argument(
        "--side",
        type=str,
        default="BUY",
        choices=["BUY", "SELL"],
        dest="side",
        help="BUY (default) or SELL.",
    )
    marketo_parser.set_defaults(handler=_cmd_marketo)

    # LIMITO
    limito_parser = subparsers.add_parser(
        "limito", help="Place a GTC limit order at an explicit price."
    )
    limito_parser.add_argument(
        "--slug", type=str, default=None, dest="market_slug",
        help="Market slug. Required with --outcome when --token-id is not provided.",
    )
    limito_parser.add_argument(
        "--outcome", type=str, default=None, help="Outcome to trade (e.g. 'Yes').",
    )
    limito_parser.add_argument(
        "--token-id", type=str, default=None, dest="token_id",
        help="Token ID to trade directly. Alternative to --slug + --outcome.",
    )
    limito_parser.add_argument(
        "--bet", type=float, required=True,
        help="USDC to spend (BUY) or number of shares to sell (SELL). Use -1 with SELL to sell entire position."
    )
    limito_parser.add_argument(
        "--price",
        type=float,
        required=True,
        dest="price",
        help="Limit price in cents 0-100.",
    )
    limito_parser.add_argument(
        "--side",
        type=str,
        default="BUY",
        choices=["BUY", "SELL"],
        dest="side",
        help="Order side: BUY (default) or SELL.",
    )
    limito_parser.set_defaults(handler=_cmd_limito)
    
    # GET all orders
    order_parser = subparsers.add_parser(
        "orders", help="Get all open orders."
    ).set_defaults(handler=_cmd_orders)
    
    # CANCEL order(s)
    cancel_parser = subparsers.add_parser("cancel", help="Cancel partial or all open orders.")
    cancel_parser.set_defaults(handler=_cmd_cancel)
    cancel_parser.add_argument(
        '-a', '--all', action="store_true", help="Cancel all open orders."
    )
    cancel_parser.add_argument(
        '--id', type=str, help='Comma-separated order IDs to cancel.'
    )
    
    # TRADE
    trade_parser = subparsers.add_parser("trade")
    trade_parser.add_argument(
        "--token-id",
        type=str,
        default=None,
        dest="token_id",
        help="Filter trades by token id.",
    )
    trade_parser.add_argument(
        "--before",
        type=int,
        default=None,
        help="Filter trades before a unix timestamp.",
    )
    trade_parser.add_argument(
        "--after",
        type=int,
        default=None,
        help="Filter trades after a unix timestamp.",
    )
    trade_parser.add_argument(
        "--market",
        type=str,
        default=None,
        help="Filter trades by market id.",
    )
    trade_parser.add_argument(
        "-q",
        type=str,
        default=None,
        dest="query",
        help="Filter trades by keyword (matched against outcome or market question).",
    )
    trade_parser.set_defaults(handler=_cmd_trade)

    # POSITIONS
    pos_parser = subparsers.add_parser("pos", help="Get current positions for a user.")
    pos_parser.add_argument(
        "--user",
        type=str,
        default=os.environ.get("POLY_ADDR"),
        help="User address. Defaults to POLY_ADDR env var.",
    )
    pos_parser.set_defaults(handler=_cmd_pos)

    # PNL
    pnl_parser = subparsers.add_parser("pnl", help="Show P&L summary across all open positions.")
    pnl_parser.add_argument(
        "--user",
        type=str,
        default=os.environ.get("POLY_ADDR"),
        help="User address. Defaults to POLY_ADDR env var.",
    )
    pnl_parser.set_defaults(handler=_cmd_pnl)

    return parser


def main() -> int:
    parser = build_parser()
    argv = sys.argv[1:]

    if not argv:
        parser.print_help()
        return 0

    args = parser.parse_args(argv)
    handler = args.handler
    if handler is None:
        parser.print_help()
        return 0

    return handler(args)

if __name__ == "__main__":
    raise SystemExit(main())
