"""TinkClaw Bot Agent — local-first bot runtime for desktop/Pi.

Connects to TinkClaw signal stream via WebSocket,
evaluates signals against user's rules, and executes trades on-device.

Usage:
    agent = MeshAgent()
    await agent.start()     # blocks until stopped
    agent.stop()
"""

import asyncio
import json
import logging
import signal
import time
import urllib.request
from datetime import datetime, timezone
from typing import Optional

from .store import MeshStore

logger = logging.getLogger("tinkclaw.mesh")

# ── Strategy Presets ──

STRATEGY_PRESETS = {
    "conservative": {"confidence": 0.90, "risk_pct": 0.05},
    "balanced": {"confidence": 0.80, "risk_pct": 0.10},
    "aggressive": {"confidence": 0.65, "risk_pct": 0.20},
}

# ── Constants ──

HEARTBEAT_INTERVAL = 30
RECONNECT_DELAYS = [1, 2, 5, 10, 30]
COOLDOWN_SECONDS = 300
MAX_POSITIONS = 10


class MeshAgent:
    """Local bot runtime — WebSocket signal consumer + rule engine."""

    def __init__(self, store: Optional[MeshStore] = None):
        self.store = store or MeshStore()
        self._running = False
        self._ws = None
        self._trades_session = 0
        self._signals_session = 0
        self._last_trade: dict[str, float] = {}  # symbol → timestamp
        self._positions: set[str] = set()
        self._reconnect_count = 0
        self._start_time = 0.0

    async def start(self) -> None:
        """Start the bot agent. Blocks until stopped."""
        if not self.store.is_activated():
            raise RuntimeError(
                "Device not activated. Run: tinkclaw activate <token>"
            )

        self._running = True
        self._start_time = time.time()

        # Handle shutdown signals
        loop = asyncio.get_running_loop()
        for sig in (signal.SIGTERM, signal.SIGINT):
            loop.add_signal_handler(sig, self.stop)

        logger.info("Bot starting...")
        logger.info("Device: %s", self.store.get("device_name", "Unknown"))
        logger.info("Trades recorded: %d", self.store.trade_count())

        config = self.store.get_mesh_config()
        logger.info(
            "Strategy: confidence=%.0f%% risk=%.0f%% symbols=%s",
            config["confidence"] * 100,
            config["risk_pct"] * 100,
            ", ".join(config.get("symbols", [])) or "all",
        )

        # Sync any unsynced trades from previous session
        asyncio.ensure_future(self._sync_trades())

        while self._running:
            try:
                await self._connect_and_listen()
            except Exception as e:
                if not self._running:
                    break
                delay = RECONNECT_DELAYS[
                    min(self._reconnect_count, len(RECONNECT_DELAYS) - 1)
                ]
                logger.warning(
                    "Connection lost: %s — reconnecting in %ds", e, delay
                )
                self._reconnect_count += 1
                await asyncio.sleep(delay)

        logger.info(
            "Bot stopped. Session: %d signals, %d trades",
            self._signals_session,
            self._trades_session,
        )

    def stop(self) -> None:
        """Stop the bot agent."""
        self._running = False
        if self._ws:
            asyncio.ensure_future(self._ws.close())

    async def _connect_and_listen(self) -> None:
        """Connect to WS and process signals."""
        try:
            import websockets
        except ImportError:
            raise RuntimeError(
                "websockets package required: pip install tinkclaw[mesh]"
            )

        stream_url = self.store.stream_url
        auth_token = self.store.auth_token
        device_id = self.store.device_id

        logger.info("Connecting to %s...", stream_url)

        async with websockets.connect(
            stream_url,
            additional_headers={"Origin": "https://tinkclaw.com"},
            ping_interval=20,
            ping_timeout=10,
        ) as ws:
            self._ws = ws
            self._reconnect_count = 0

            # Authenticate
            await ws.send(json.dumps({
                "type": "auth",
                "token": auth_token,
            }))

            auth_resp = json.loads(await ws.recv())
            if auth_resp.get("type") == "error":
                raise RuntimeError(f"Auth failed: {auth_resp.get('message')}")

            logger.info("Authenticated")

            # Activate mesh
            await ws.send(json.dumps({
                "type": "mesh:activate",
                "device_id": device_id,
            }))

            logger.info("Bot active — listening for signals")

            # Start heartbeat
            heartbeat_task = asyncio.ensure_future(self._heartbeat(ws))

            try:
                async for raw in ws:
                    if not self._running:
                        break
                    try:
                        msg = json.loads(raw)
                        await self._handle_message(msg)
                    except json.JSONDecodeError:
                        continue
            finally:
                heartbeat_task.cancel()
                self._ws = None

    async def _heartbeat(self, ws) -> None:
        """Send periodic heartbeat to server."""
        while self._running:
            await asyncio.sleep(HEARTBEAT_INTERVAL)
            try:
                await ws.send(json.dumps({
                    "type": "mesh:heartbeat",
                    "device_id": self.store.device_id,
                    "uptime": int(time.time() - self._start_time),
                    "trades": self._trades_session,
                }))
            except Exception:
                break

    async def _handle_message(self, msg: dict) -> None:
        """Route incoming WebSocket messages."""
        msg_type = msg.get("type", "")

        if msg_type == "signal":
            await self._process_signal(msg.get("data", msg))
        elif msg_type == "mesh:config_sync":
            config = msg.get("config", {})
            if config:
                current = self.store.get_mesh_config()
                current.update(config)
                self.store.save_mesh_config(current)
                logger.info("Config synced from another device")

    async def _process_signal(self, signal: dict) -> None:
        """Evaluate signal against user rules and execute if passing."""
        self._signals_session += 1
        symbol = signal.get("symbol", "")
        direction = signal.get("direction", "")
        confidence = signal.get("confidence", 0)
        now = datetime.now(timezone.utc).isoformat()
        signal_id = signal.get("signal_id", f"sig_{int(time.time())}")

        # Skip HOLD
        if direction == "HOLD":
            return

        config = self.store.get_mesh_config()
        rules = self._evaluate_rules(signal, config)
        passed = all(r["passed"] for r in rules)

        # Log signal
        reason = "" if passed else "; ".join(
            r["reason"] for r in rules if not r["passed"]
        )
        self.store.log_signal(
            signal_id, symbol, direction, confidence, passed, reason, now
        )

        if not passed:
            rejected = [r["rule"] for r in rules if not r["passed"]]
            logger.debug(
                "SKIP %s %s (%.0f%%) — %s",
                direction, symbol, confidence * 100, ", ".join(rejected),
            )
            return

        # Execute trade
        price = signal.get("price")
        logger.info(
            "TRADE %s %s @ %.2f (%.0f%% confidence)",
            direction, symbol, price or 0, confidence * 100,
        )

        self._trades_session += 1
        self._last_trade[symbol] = time.time()
        if direction == "BUY":
            self._positions.add(symbol)
        elif direction == "SELL":
            self._positions.discard(symbol)

        trade_id = self.store.add_trade(
            signal_id, symbol, direction, confidence, price, rules, now
        )

        # Report trade to server (non-blocking)
        asyncio.ensure_future(self._report_trade({
            "signal_id": signal_id,
            "symbol": symbol,
            "direction": direction,
            "confidence": confidence,
            "price": price,
            "device_id": self.store.device_id,
            "executed_at": now,
            "rules_evaluated": rules,
        }))

    def _evaluate_rules(self, signal: dict, config: dict) -> list[dict]:
        """Run signal through local rule engine."""
        rules = []
        symbol = signal.get("symbol", "")
        direction = signal.get("direction", "")
        confidence = signal.get("confidence", 0)
        min_conf = config.get("confidence", 0.8)
        allowed_symbols = config.get("symbols", [])

        # Rule 1: Minimum confidence
        rules.append({
            "rule": "min_confidence",
            "passed": confidence >= min_conf,
            "reason": f"Confidence {confidence:.0%} {'above' if confidence >= min_conf else 'below'} {min_conf:.0%} threshold",
        })

        # Rule 2: Symbol filter
        symbol_ok = not allowed_symbols or symbol in allowed_symbols
        rules.append({
            "rule": "symbol_allowed",
            "passed": symbol_ok,
            "reason": f"{symbol} {'in' if symbol_ok else 'not in'} watchlist",
        })

        # Rule 3: Cooldown
        last = self._last_trade.get(symbol, 0)
        elapsed = time.time() - last
        cooldown_ok = elapsed >= COOLDOWN_SECONDS
        rules.append({
            "rule": "trade_cooldown",
            "passed": cooldown_ok,
            "reason": f"{'Cooldown clear' if cooldown_ok else f'{int(COOLDOWN_SECONDS - elapsed)}s remaining'}",
        })

        # Rule 4: Max positions
        pos_ok = len(self._positions) < MAX_POSITIONS or direction == "SELL"
        rules.append({
            "rule": "max_positions",
            "passed": pos_ok,
            "reason": f"{len(self._positions)}/{MAX_POSITIONS} positions open",
        })

        # Rule 5: Position exists (for SELL)
        if direction == "SELL":
            has_pos = symbol in self._positions
            rules.append({
                "rule": "position_exists",
                "passed": has_pos,
                "reason": f"{'Have' if has_pos else 'No'} open position for {symbol}",
            })

        return rules

    async def _report_trade(self, trade: dict) -> None:
        """POST trade execution to server."""
        try:
            url = f"{self.store.api_url}/api/mesh/trade"
            data = json.dumps(trade).encode()
            req = urllib.request.Request(
                url,
                data=data,
                headers={
                    "Content-Type": "application/json",
                    "Authorization": f"Bearer {self.store.auth_token}",
                },
                method="POST",
            )
            await asyncio.get_running_loop().run_in_executor(
                None,
                lambda: urllib.request.urlopen(req, timeout=10),
            )
        except Exception as e:
            logger.debug("Trade report failed (will retry): %s", e)

    async def _sync_trades(self) -> None:
        """Sync unsynced trades from previous sessions."""
        unsynced = self.store.get_unsynced_trades()
        if not unsynced:
            return
        logger.info("Syncing %d trades from previous session...", len(unsynced))
        for trade in unsynced:
            try:
                await self._report_trade({
                    "signal_id": trade["signal_id"],
                    "symbol": trade["symbol"],
                    "direction": trade["direction"],
                    "confidence": trade["confidence"],
                    "price": trade["price"],
                    "device_id": self.store.device_id,
                    "executed_at": trade["executed_at"],
                })
                self.store.mark_synced(trade["id"])
            except Exception:
                break

    @property
    def uptime(self) -> float:
        if self._start_time:
            return time.time() - self._start_time
        return 0

    def status(self) -> dict:
        config = self.store.get_mesh_config()
        return {
            "active": self._running,
            "device_id": self.store.device_id,
            "device_name": self.store.get("device_name", "Unknown"),
            "uptime": int(self.uptime),
            "trades_session": self._trades_session,
            "trades_total": self.store.trade_count(),
            "signals_session": self._signals_session,
            "positions_open": len(self._positions),
            "config": config,
        }
