#!/usr/bin/env python3
"""NeuralBTC.ai multi-exchange execution bridge (v4).

Receives authenticated NeuralBTC webhook signals and executes BTC on Binance,
Kraken, Bybit, Crypto.com, or CEX.IO through CCXT. API credentials remain on
the customer's bridge host and are never sent to neuralbtc.ai.

Bidirectional SELL signals require an exchange account and market that support
swap, futures, or margin shorting. Spot mode intentionally rejects SELL entries
instead of silently liquidating an existing balance.

Install:
    pip install flask waitress requests ccxt

Minimum configuration (Windows examples):
    set BRIDGE_SECRET=choose-a-long-random-string
    set BRIDGE_EXCHANGE=bybit
    set BRIDGE_MARKET_TYPE=swap
    set BRIDGE_SYMBOL=BTC/USDT:USDT
    set EXCHANGE_API_KEY=...
    set EXCHANGE_API_SECRET=...
    set BRIDGE_LIVE=true
    python neuralbtc_bridge.py

The bridge places the entry and keeps a local SL/TP monitor. The bridge must
remain running; local protection is not equivalent to exchange-native resting
protective orders. Use one-way position mode unless a venue-specific adapter
has been independently validated for hedge mode.
"""
import json
import math
import os
import secrets as _secrets
import sqlite3
import threading
import time

from flask import Flask, jsonify, request


SUPPORTED_EXCHANGES = ("binance", "kraken", "bybit", "cryptocom", "cex")
DERIVATIVE_MARKETS = ("swap", "future", "margin")

EXCHANGE = os.getenv("BRIDGE_EXCHANGE", "log").strip().lower()
MARKET_TYPE = os.getenv("BRIDGE_MARKET_TYPE", "swap").strip().lower()
SYMBOL_OVERRIDE = os.getenv("BRIDGE_SYMBOL", "").strip()
API_KEY = os.getenv("EXCHANGE_API_KEY", "").strip()
API_SECRET = os.getenv("EXCHANGE_API_SECRET", "").strip()
API_PASSWORD = os.getenv("EXCHANGE_API_PASSWORD", "").strip()
API_UID = os.getenv("EXCHANGE_API_UID", "").strip()
BRIDGE_SECRET = os.getenv("BRIDGE_SECRET", "")
BRIDGE_LIVE = os.getenv("BRIDGE_LIVE", "false").lower() in ("1", "true", "yes")
BRIDGE_SANDBOX = os.getenv("BRIDGE_SANDBOX", "false").lower() in ("1", "true", "yes")
MAX_QUOTE_USD = float(os.getenv("BRIDGE_MAX_USD_PER_TRADE", "0"))
MONITOR_SECONDS = max(1.0, float(os.getenv("BRIDGE_MONITOR_SECONDS", "5")))
DB_PATH = os.getenv("BRIDGE_DB_PATH", "neuralbtc_bridge.db")

app = Flask(__name__)
_client = None
_client_lock = threading.Lock()


def _db():
    conn = sqlite3.connect(DB_PATH, timeout=10)
    conn.execute(
        "CREATE TABLE IF NOT EXISTS processed_signals "
        "(id TEXT PRIMARY KEY, processed_at TEXT DEFAULT CURRENT_TIMESTAMP, result TEXT)"
    )
    conn.execute(
        "CREATE TABLE IF NOT EXISTS managed_positions "
        "(id TEXT PRIMARY KEY, symbol TEXT NOT NULL, side TEXT NOT NULL, "
        "amount REAL NOT NULL, sl REAL NOT NULL, tp REAL NOT NULL, "
        "status TEXT NOT NULL DEFAULT 'open', entry_order TEXT, close_result TEXT, "
        "opened_at TEXT DEFAULT CURRENT_TIMESTAMP, closed_at TEXT)"
    )
    return conn


def already_done(signal_id):
    with _db() as conn:
        return conn.execute(
            "SELECT 1 FROM processed_signals WHERE id=?", (signal_id,)
        ).fetchone() is not None


def mark_done(signal_id, result):
    with _db() as conn:
        conn.execute(
            "INSERT OR IGNORE INTO processed_signals(id,result) VALUES(?,?)",
            (signal_id, json.dumps(result, default=str)[:4000]),
        )


def remember_position(signal_id, symbol, side, amount, sl, tp, order):
    with _db() as conn:
        conn.execute(
            "INSERT OR REPLACE INTO managed_positions"
            "(id,symbol,side,amount,sl,tp,status,entry_order) VALUES(?,?,?,?,?,?,'open',?)",
            (signal_id, symbol, side, amount, sl, tp, json.dumps(order, default=str)[:4000]),
        )


def validate_signal(sig):
    if not isinstance(sig, dict):
        return "payload must be an object"
    signal_id = str(sig.get("id") or "")
    if not signal_id or len(signal_id) > 128:
        return "missing or invalid signal id"
    if sig.get("symbol") != "BTCUSD":
        return "the exchange bridge accepts BTCUSD signals only; use MT5 for XAUUSD"
    side = str(sig.get("side") or "").upper()
    if side not in ("BUY", "SELL"):
        return "no actionable side"
    try:
        entry, sl, tp = (float(sig.get(key)) for key in ("entry", "sl", "tp"))
        confidence = float(sig.get("confidence"))
    except (TypeError, ValueError):
        return "entry, sl, tp, and confidence must be numeric"
    if not all(math.isfinite(value) and value > 0 for value in (entry, sl, tp)):
        return "entry, sl, and tp must be finite positive values"
    if not math.isfinite(confidence) or not 0 <= confidence <= 1:
        return "confidence must be between 0 and 1"
    if side == "BUY" and not sl < entry < tp:
        return "BUY requires sl < entry < tp"
    if side == "SELL" and not tp < entry < sl:
        return "SELL requires tp < entry < sl"
    requested_exchange = str(sig.get("sz_exchange") or EXCHANGE).lower()
    if EXCHANGE != "log" and requested_exchange != EXCHANGE:
        return f"signal is configured for {requested_exchange}, but this bridge is {EXCHANGE}"
    requested_market = str(sig.get("sz_exchange_market") or MARKET_TYPE).lower()
    if requested_market != MARKET_TYPE:
        return f"signal market is {requested_market}, but this bridge is {MARKET_TYPE}"
    if side == "SELL" and MARKET_TYPE not in DERIVATIVE_MARKETS:
        return "SELL entry rejected: spot mode cannot open a short position"
    return ""


def _load_ccxt():
    try:
        import ccxt
    except Exception as exc:
        raise RuntimeError("pip install ccxt to enable exchange execution") from exc
    return ccxt


def build_exchange():
    ccxt = _load_ccxt()
    class_name = "krakenfutures" if EXCHANGE == "kraken" and MARKET_TYPE in ("swap", "future") else EXCHANGE
    exchange_class = getattr(ccxt, class_name, None)
    if exchange_class is None:
        raise RuntimeError(f"CCXT does not provide the {class_name} adapter")
    config = {
        "apiKey": API_KEY,
        "secret": API_SECRET,
        "enableRateLimit": True,
        "options": {"defaultType": MARKET_TYPE},
    }
    if API_PASSWORD:
        config["password"] = API_PASSWORD
    if API_UID:
        config["uid"] = API_UID
    client = exchange_class(config)
    if BRIDGE_SANDBOX:
        client.set_sandbox_mode(True)
    return client


def exchange_client():
    global _client
    with _client_lock:
        if _client is None:
            _client = build_exchange()
        return _client


def resolve_market(client):
    markets = client.load_markets()
    if SYMBOL_OVERRIDE:
        if SYMBOL_OVERRIDE not in markets:
            raise RuntimeError(f"BRIDGE_SYMBOL {SYMBOL_OVERRIDE!r} is not listed on {EXCHANGE}")
        return markets[SYMBOL_OVERRIDE]
    candidates = []
    for market in markets.values():
        if market.get("base") != "BTC" or market.get("quote") not in ("USD", "USDT", "USDC"):
            continue
        if MARKET_TYPE in ("swap", "future") and not market.get(MARKET_TYPE):
            continue
        if MARKET_TYPE in ("spot", "margin") and not market.get("spot"):
            continue
        candidates.append(market)
    if not candidates:
        raise RuntimeError(f"no BTC USD/USDT/USDC {MARKET_TYPE} market found; set BRIDGE_SYMBOL explicitly")
    candidates.sort(key=lambda m: (m.get("quote") != "USD", m.get("quote") != "USDT", m["symbol"]))
    return candidates[0]


def _positive_float(value, name):
    try:
        number = float(value)
    except (TypeError, ValueError) as exc:
        raise RuntimeError(f"{name} must be numeric") from exc
    if not math.isfinite(number) or number <= 0:
        raise RuntimeError(f"{name} must be positive")
    return number


def _free_quote_balance(client, quote):
    params = {"type": MARKET_TYPE} if MARKET_TYPE != "spot" else {}
    balance = client.fetch_balance(params)
    free = balance.get("free", {}).get(quote)
    if free is None and quote in balance and isinstance(balance[quote], dict):
        free = balance[quote].get("free")
    return max(0.0, float(free or 0))


def order_amount(sig, client, market, price):
    mode = str(sig.get("sz_exchange_mode") or "fixed_usd")
    if mode == "fixed_base":
        base_amount = _positive_float(sig.get("sz_exchange_base"), "fixed BTC quantity")
    else:
        if mode == "portfolio_percent":
            pct = _positive_float(sig.get("sz_exchange_pct"), "portfolio percentage")
            if pct > 100:
                raise RuntimeError("portfolio percentage cannot exceed 100")
            notional = _free_quote_balance(client, market["quote"]) * pct / 100.0
        else:
            notional = _positive_float(sig.get("sz_exchange_usd"), "fixed USD order size")
        if MAX_QUOTE_USD > 0:
            notional = min(notional, MAX_QUOTE_USD)
        base_amount = notional / price
    contract_size = float(market.get("contractSize") or 1.0) if market.get("contract") else 1.0
    amount = base_amount / contract_size if market.get("contract") else base_amount
    amount = float(client.amount_to_precision(market["symbol"], amount))
    minimum = ((market.get("limits") or {}).get("amount") or {}).get("min")
    if amount <= 0 or (minimum is not None and amount < float(minimum)):
        raise RuntimeError(f"calculated amount {amount} is below the exchange minimum {minimum}")
    return amount


def execute_signal(sig):
    if not BRIDGE_LIVE:
        return {"logged": True, "live_execution": False, "reason": "BRIDGE_LIVE is not true"}
    client = exchange_client()
    market = resolve_market(client)
    symbol = market["symbol"]
    ticker = client.fetch_ticker(symbol)
    price = float((ticker.get("ask") if sig["side"] == "BUY" else ticker.get("bid")) or 0)
    if price <= 0:
        price = _positive_float(ticker.get("last"), "market price")
    amount = order_amount(sig, client, market, price)
    leverage = int(_positive_float(sig.get("sz_exchange_leverage") or 1, "leverage"))
    if MARKET_TYPE in ("swap", "future") and client.has.get("setLeverage"):
        client.set_leverage(leverage, symbol)
    params = {}
    if MARKET_TYPE in ("swap", "future"):
        params["reduceOnly"] = False
    elif MARKET_TYPE == "margin":
        params["marginMode"] = "cross"
    order = client.create_order(symbol, "market", sig["side"].lower(), amount, None, params)
    remember_position(sig["id"], symbol, sig["side"], amount, sig["sl"], sig["tp"], order)
    return {
        "placed": True,
        "exchange": EXCHANGE,
        "market_type": MARKET_TYPE,
        "symbol": symbol,
        "side": sig["side"],
        "amount": amount,
        "order_id": order.get("id") if isinstance(order, dict) else None,
        "protection": "local_sl_tp_monitor",
    }


def _close_position(client, row, price):
    signal_id, symbol, side, amount, sl, tp = row
    triggered = (side == "BUY" and (price <= sl or price >= tp)) or (
        side == "SELL" and (price >= sl or price <= tp)
    )
    if not triggered:
        return
    params = {"reduceOnly": True} if MARKET_TYPE in ("swap", "future") else {}
    if MARKET_TYPE == "margin":
        params["marginMode"] = "cross"
        params["reduceOnly"] = True
    result = client.create_order(
        symbol, "market", "sell" if side == "BUY" else "buy", amount, None, params
    )
    with _db() as conn:
        conn.execute(
            "UPDATE managed_positions SET status='closed', close_result=?, closed_at=CURRENT_TIMESTAMP WHERE id=?",
            (json.dumps(result, default=str)[:4000], signal_id),
        )


def monitor_positions():
    while True:
        try:
            if BRIDGE_LIVE and EXCHANGE != "log":
                client = exchange_client()
                with _db() as conn:
                    rows = conn.execute(
                        "SELECT id,symbol,side,amount,sl,tp FROM managed_positions WHERE status='open'"
                    ).fetchall()
                for row in rows:
                    ticker = client.fetch_ticker(row[1])
                    price = float(ticker.get("last") or ticker.get("bid") or ticker.get("ask") or 0)
                    if price > 0:
                        _close_position(client, row, price)
        except Exception as exc:
            print(f"[NeuralBTC] protection monitor error: {exc}", flush=True)
        time.sleep(MONITOR_SECONDS)


def _request_authorized():
    supplied = request.headers.get("Authorization", "")
    if supplied.lower().startswith("bearer "):
        supplied = supplied[7:].strip()
    else:
        supplied = request.args.get("token", "")
    return bool(BRIDGE_SECRET and _secrets.compare_digest(supplied, BRIDGE_SECRET))


@app.route("/hook", methods=["POST"])
def hook():
    if not _request_authorized():
        return jsonify({"ok": False, "reason": "bad token"}), 403
    sig = request.get_json(force=True, silent=True) or {}
    if sig.get("test"):
        return jsonify({"ok": True, "test": True, "exchange": EXCHANGE})
    invalid = validate_signal(sig)
    if invalid:
        return jsonify({"ok": False, "reason": invalid}), 400
    signal_id = str(sig["id"])
    if already_done(signal_id):
        return jsonify({"ok": True, "result": {"skipped": "duplicate signal"}})
    try:
        result = {"logged": True} if EXCHANGE == "log" else execute_signal(sig)
    except Exception as exc:
        result = {"error": str(exc)}
    if not result.get("error"):
        mark_done(signal_id, result)
    print(f"[NeuralBTC] {sig.get('side')} {sig.get('symbol')} result: {result}", flush=True)
    return jsonify({"ok": not bool(result.get("error")), "result": result}), 502 if result.get("error") else 200


@app.route("/check")
def check_connection():
    if not _request_authorized():
        return jsonify({"ok": False, "reason": "bad token"}), 403
    if EXCHANGE == "log":
        return jsonify({"ok": True, "exchange": "log", "live_execution": False})
    try:
        client = exchange_client()
        market = resolve_market(client)
        client.fetch_balance({"type": MARKET_TYPE} if MARKET_TYPE != "spot" else {})
        return jsonify(
            {
                "ok": True,
                "exchange": EXCHANGE,
                "market_type": MARKET_TYPE,
                "symbol": market["symbol"],
                "can_open_short": MARKET_TYPE in DERIVATIVE_MARKETS,
                "live_execution": BRIDGE_LIVE,
            }
        )
    except Exception as exc:
        return jsonify({"ok": False, "exchange": EXCHANGE, "error": str(exc)}), 502


@app.route("/health")
def health():
    return jsonify(
        {
            "ok": True,
            "exchange": EXCHANGE,
            "market_type": MARKET_TYPE,
            "auth": bool(BRIDGE_SECRET),
            "credentials_configured": EXCHANGE == "log" or bool(API_KEY and API_SECRET),
            "live_execution": BRIDGE_LIVE,
            "protection": "local_sl_tp_monitor",
        }
    )


if __name__ == "__main__":
    if EXCHANGE not in (*SUPPORTED_EXCHANGES, "log"):
        print("BRIDGE_EXCHANGE must be binance, kraken, bybit, cryptocom, cex, or log.")
        raise SystemExit(1)
    if MARKET_TYPE not in ("swap", "future", "margin", "spot"):
        print("BRIDGE_MARKET_TYPE must be swap, future, margin, or spot.")
        raise SystemExit(1)
    if not BRIDGE_SECRET:
        suggestion = _secrets.token_urlsafe(24)
        print("BRIDGE_SECRET is not set; refusing unauthenticated trade commands.")
        print(f"Set it first, for example: set BRIDGE_SECRET={suggestion}")
        raise SystemExit(1)
    try:
        from waitress import serve
    except Exception:
        print("waitress is required: pip install waitress")
        raise SystemExit(1)
    threading.Thread(target=monitor_positions, name="sl-tp-monitor", daemon=True).start()
    print(
        f"NeuralBTC bridge listening on :8777 "
        f"(exchange={EXCHANGE}, market={MARKET_TYPE}, live={BRIDGE_LIVE})"
    )
    serve(app, host="0.0.0.0", port=8777, threads=4)
