#!/usr/bin/env python3
"""
═══════════════════════════════════════════════════════════════
  LITCOIN RELAY — Compute Marketplace Provider Module
  Mine + Relay. Earn more with your existing API key.
═══════════════════════════════════════════════════════════════

  Adds relay capability to the LITCOIN miner. When enabled,
  the miner also serves AI inference requests from LITCREDIT
  holders — routed through the coordinator via WebSocket.

  YOUR API KEY NEVER LEAVES YOUR MACHINE.

  The coordinator only sees prompts and responses.
  Authentication happens locally on your end.

  Usage:
    python my_miner.py --relay               # mine + relay
    python my_miner.py --relay-only           # relay only (no mining)
    python my_miner.py --relay --budget 500000 # limit daily token budget

═══════════════════════════════════════════════════════════════
"""

import json
import time
import hashlib
import threading
import traceback
from datetime import datetime, timezone
from collections import deque

# ─── Relay Configuration ─────────────────────────────────────────────────────

DEFAULT_RELAY_CONFIG = {
    "enabled": False,
    "relay_only": False,
    "daily_token_budget": 1_000_000,   # max tokens/day to relay (0 = unlimited)
    "max_concurrent": 3,                # max simultaneous relay requests
    "supported_models": [],             # empty = auto-detect from miner config
    "provider_name": "",                # auto-set from AI_BASE_URL
    "quality_check": True,              # validate own responses before sending
    "heartbeat_interval": 30,           # seconds between heartbeats
    "reconnect_delay": 5,               # initial reconnect delay (doubles on failure)
    "max_reconnect_delay": 300,         # cap at 5 min
}


class RelayStats:
    """Track relay performance metrics."""
    def __init__(self):
        self.requests_served = 0
        self.requests_failed = 0
        self.tokens_used_today = 0
        self.total_litcoin_earned = 0
        self.day_start = time.time()
        self.latencies = deque(maxlen=100)  # last 100 request latencies
        self.lock = threading.Lock()

    def record_success(self, tokens_used, latency_ms, reward=0):
        with self.lock:
            self.requests_served += 1
            self.tokens_used_today += tokens_used
            self.total_litcoin_earned += reward
            self.latencies.append(latency_ms)

    def record_failure(self):
        with self.lock:
            self.requests_failed += 1

    def reset_daily(self):
        with self.lock:
            self.tokens_used_today = 0
            self.day_start = time.time()

    @property
    def avg_latency(self):
        with self.lock:
            if not self.latencies:
                return 0
            return sum(self.latencies) / len(self.latencies)

    @property
    def success_rate(self):
        total = self.requests_served + self.requests_failed
        if total == 0:
            return 1.0
        return self.requests_served / total

    def summary(self):
        return {
            "served": self.requests_served,
            "failed": self.requests_failed,
            "tokens_today": self.tokens_used_today,
            "litcoin_earned": self.total_litcoin_earned,
            "avg_latency_ms": round(self.avg_latency),
            "success_rate": round(self.success_rate, 3),
        }


class RelayProvider:
    """
    WebSocket-based relay provider that runs alongside the miner.

    Connects to the coordinator via WebSocket. Receives inference
    requests, processes them locally using the miner's own API key,
    and returns signed responses.

    The API key NEVER leaves this machine.
    """

    def __init__(self, config, ai_config, wallet_address, sign_fn, log_fn=None):
        """
        Args:
            config: dict with relay configuration
            ai_config: dict with {base_url, api_key, model, anthropic_mode}
            wallet_address: miner's wallet address (for signing receipts)
            sign_fn: function(message) -> signature (Bankr sign)
            log_fn: optional logging function
        """
        self.config = {**DEFAULT_RELAY_CONFIG, **config}
        self.ai = ai_config
        self.wallet = wallet_address
        self.sign_fn = sign_fn
        self.log = log_fn or (lambda msg, level="INFO": print(f"[RELAY] [{level}] {msg}"))
        self.stats = RelayStats()

        self.ws = None
        self.running = False
        self.connected = False
        self._active_requests = 0
        self._active_lock = threading.Lock()
        self._reconnect_delay = self.config["reconnect_delay"]

        # Auto-detect provider name from URL
        if not self.config["provider_name"]:
            url = self.ai.get("base_url", "")
            if "venice" in url:
                self.config["provider_name"] = "venice"
            elif "openai" in url:
                self.config["provider_name"] = "openai"
            elif "groq" in url:
                self.config["provider_name"] = "groq"
            elif "together" in url:
                self.config["provider_name"] = "together"
            elif "anthropic" in url:
                self.config["provider_name"] = "anthropic"
            elif "localhost" in url or "127.0.0.1" in url:
                self.config["provider_name"] = "local"
            else:
                self.config["provider_name"] = "unknown"

        # Auto-detect supported models
        if not self.config["supported_models"]:
            self.config["supported_models"] = [self.ai.get("model", "unknown")]

    def start(self, coordinator_url, auth_token):
        """Start the relay provider in a background thread."""
        self.running = True
        self._coordinator_url = coordinator_url
        self._auth_token = auth_token

        self._thread = threading.Thread(target=self._run_loop, daemon=True)
        self._thread.start()
        self.log("Relay provider started")

    def stop(self):
        """Stop the relay provider."""
        self.running = False
        if self.ws:
            try:
                self.ws.close()
            except:
                pass
        self.log("Relay provider stopped")
        self.log(f"Session: {json.dumps(self.stats.summary())}")

    def update_auth(self, new_token):
        """Update auth token (called when miner re-authenticates)."""
        self._auth_token = new_token

    # ─── WebSocket Connection Loop ────────────────────────────────────────────

    def _run_loop(self):
        """Main connection loop with reconnection logic."""
        try:
            import websocket
        except ImportError:
            self.log("Installing websocket-client...", "WARN")
            import subprocess
            subprocess.check_call(["pip", "install", "websocket-client", "--break-system-packages", "-q"])
            import websocket

        while self.running:
            try:
                self._connect(websocket)
            except Exception as e:
                if not self.running:
                    break
                self.log(f"Connection error: {e}", "ERROR")
                self.connected = False

            if not self.running:
                break

            # Reconnect with exponential backoff
            self.log(f"Reconnecting in {self._reconnect_delay}s...")
            time.sleep(self._reconnect_delay)
            self._reconnect_delay = min(
                self._reconnect_delay * 2,
                self.config["max_reconnect_delay"]
            )

    def _connect(self, websocket_module):
        """Establish WebSocket connection to coordinator."""
        # Build WS URL from coordinator URL
        ws_base = self._coordinator_url.replace("https://", "wss://").replace("http://", "ws://")
        ws_url = f"{ws_base}/v1/compute/relay"

        self.log(f"Connecting to {ws_url}...")

        self.ws = websocket_module.WebSocketApp(
            ws_url,
            header={
                "Authorization": f"Bearer {self._auth_token}",
                "X-Relay-Wallet": self.wallet,
            },
            on_open=self._on_open,
            on_message=self._on_message,
            on_error=self._on_error,
            on_close=self._on_close,
        )

        # Run with ping/pong to detect dead connections
        self.ws.run_forever(
            ping_interval=self.config["heartbeat_interval"],
            ping_timeout=10,
        )

    def _on_open(self, ws):
        """Called when WebSocket connection established."""
        self.connected = True
        self._reconnect_delay = self.config["reconnect_delay"]  # Reset backoff
        self.log("Connected to coordinator")

        # Register capabilities
        registration = {
            "type": "register",
            "wallet": self.wallet,
            "provider": self.config["provider_name"],
            "models": self.config["supported_models"],
            "daily_budget": self.config["daily_token_budget"],
            "max_concurrent": self.config["max_concurrent"],
            "tokens_used_today": self.stats.tokens_used_today,
        }
        ws.send(json.dumps(registration))
        self.log(f"Sent registration, awaiting auth challenge...")

    def _on_message(self, ws, message):
        """Handle incoming messages from coordinator."""
        try:
            msg = json.loads(message)
            msg_type = msg.get("type")

            if msg_type == "challenge":
                # ── Auth challenge: sign the nonce to prove wallet ownership ──
                nonce = msg.get("nonce", "")
                self.log(f"Auth challenge received, signing...")
                try:
                    signature = self.sign_fn(nonce)
                    self._send({"type": "auth", "signature": signature})
                    self.log("Auth signature sent")
                except Exception as e:
                    self.log(f"Failed to sign auth challenge: {e}", "ERROR")

            elif msg_type == "compute_request":
                # Process in a separate thread to not block the WS
                threading.Thread(
                    target=self._handle_compute_request,
                    args=(msg,),
                    daemon=True,
                ).start()

            elif msg_type == "health_check":
                # Coordinator checking if we're alive + capacity
                self._send({
                    "type": "health_response",
                    "request_id": msg.get("request_id"),
                    "status": "ok",
                    "active_requests": self._active_requests,
                    "tokens_today": self.stats.tokens_used_today,
                    "budget_remaining": self._budget_remaining(),
                    "stats": self.stats.summary(),
                })

            elif msg_type == "reward_notification":
                # Coordinator confirming reward for a completed request
                reward = msg.get("reward", 0)
                self.stats.total_litcoin_earned += reward
                self.log(f"+{reward:,} LITCOIN relay reward (request {msg.get('request_id', '?')[:8]})")

            elif msg_type == "error":
                self.log(f"Coordinator error: {msg.get('message', 'unknown')}", "WARN")

            elif msg_type == "registered":
                self.log(f"Registration confirmed. Provider ID: {msg.get('provider_id', 'n/a')}")

        except Exception as e:
            self.log(f"Message handling error: {e}", "ERROR")

    def _on_error(self, ws, error):
        self.log(f"WebSocket error: {error}", "ERROR")

    def _on_close(self, ws, close_status_code, close_msg):
        self.connected = False
        self.log(f"Disconnected (code={close_status_code})")

    # ─── Compute Request Handler ──────────────────────────────────────────────

    def _handle_compute_request(self, msg):
        """
        Process an inference request locally using the miner's own API key.

        Flow:
        1. Check capacity (budget, concurrency)
        2. Call AI provider locally
        3. Hash and sign the response
        4. Send back to coordinator
        """
        request_id = msg.get("request_id", "unknown")
        prompt = msg.get("prompt", "")
        model_requested = msg.get("model")
        max_tokens = msg.get("max_tokens", 1024)
        system_prompt = msg.get("system_prompt")

        start_time = time.time()

        # ── Capacity checks ──
        if self._budget_remaining() <= 0:
            self._send_error(request_id, "budget_exhausted", "Daily token budget exhausted")
            return

        with self._active_lock:
            if self._active_requests >= self.config["max_concurrent"]:
                self._send_error(request_id, "at_capacity", "Max concurrent requests reached")
                return
            self._active_requests += 1

        try:
            self.log(f"Processing request {request_id[:8]}... ({len(prompt)} chars)")

            # ── Call AI provider locally ──
            model = model_requested or self.ai.get("model")
            response_text, tokens_used = self._call_ai(prompt, model, max_tokens, system_prompt)

            if not response_text:
                self.stats.record_failure()
                self._send_error(request_id, "inference_failed", "AI provider returned empty response")
                return

            elapsed_ms = int((time.time() - start_time) * 1000)

            # ── Quality check (optional) ──
            if self.config["quality_check"]:
                if len(response_text.strip()) < 5:
                    self.stats.record_failure()
                    self._send_error(request_id, "quality_check_failed", "Response too short")
                    return

            # ── Sign the response ──
            response_hash = hashlib.sha256(
                f"{request_id}:{response_text}".encode()
            ).hexdigest()

            # Sign with wallet (via Bankr)
            sign_message = f"LITCOIN Compute Receipt\nRequest: {request_id}\nHash: {response_hash}\nTokens: {tokens_used}"
            signature = self.sign_fn(sign_message)

            # ── Send response back ──
            self._send({
                "type": "compute_response",
                "request_id": request_id,
                "response": response_text,
                "tokens_used": tokens_used,
                "response_hash": response_hash,
                "signature": signature,
                "model_used": model,
                "latency_ms": elapsed_ms,
            })

            self.stats.record_success(tokens_used, elapsed_ms)
            self.log(f"Completed {request_id[:8]} — {tokens_used} tokens, {elapsed_ms}ms")

        except Exception as e:
            self.stats.record_failure()
            self.log(f"Request {request_id[:8]} failed: {e}", "ERROR")
            traceback.print_exc()
            self._send_error(request_id, "internal_error", str(e)[:200])

        finally:
            with self._active_lock:
                self._active_requests -= 1

    def _call_ai(self, prompt, model, max_tokens, system_prompt=None):
        """
        Call the AI provider using the miner's own API key.
        Returns (response_text, tokens_used).
        The API key stays right here on this machine.
        """
        import requests as http

        headers = {"Content-Type": "application/json"}
        anthropic_mode = self.ai.get("anthropic_mode", False)

        if anthropic_mode:
            # Anthropic API format
            headers["x-api-key"] = self.ai["api_key"]
            headers["anthropic-version"] = "2023-06-01"

            body = {
                "model": model,
                "max_tokens": max_tokens,
                "messages": [{"role": "user", "content": prompt}],
            }
            if system_prompt:
                body["system"] = system_prompt

            url = f"{self.ai['base_url']}/messages"
        else:
            # OpenAI-compatible format (Venice, Groq, Together, etc.)
            headers["Authorization"] = f"Bearer {self.ai['api_key']}"

            messages = []
            if system_prompt:
                messages.append({"role": "system", "content": system_prompt})
            messages.append({"role": "user", "content": prompt})

            body = {
                "model": model,
                "max_tokens": max_tokens,
                "messages": messages,
                "temperature": 0.7,
            }

            url = f"{self.ai['base_url']}/chat/completions"

        try:
            resp = http.post(url, headers=headers, json=body, timeout=120)

            if resp.status_code == 429:
                self.log("API rate limited", "WARN")
                return None, 0
            if resp.status_code in (402, 403):
                self.log("API auth/billing error", "WARN")
                return None, 0
            if resp.status_code >= 500:
                self.log(f"API server error: {resp.status_code}", "WARN")
                return None, 0

            data = resp.json()

            if anthropic_mode:
                text = data.get("content", [{}])[0].get("text", "")
                usage = data.get("usage", {})
                tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
            else:
                text = data.get("choices", [{}])[0].get("message", {}).get("content", "")
                usage = data.get("usage", {})
                tokens = usage.get("total_tokens", 0)
                if tokens == 0:
                    # Estimate if usage not provided
                    tokens = len(prompt.split()) + len(text.split())

            return text, tokens

        except http.exceptions.Timeout:
            self.log("API call timed out", "WARN")
            return None, 0
        except Exception as e:
            self.log(f"API call failed: {e}", "ERROR")
            return None, 0

    # ─── Helpers ──────────────────────────────────────────────────────────────

    def _send(self, msg):
        """Send a message through the WebSocket."""
        if self.ws and self.connected:
            try:
                self.ws.send(json.dumps(msg))
            except Exception as e:
                self.log(f"Send failed: {e}", "ERROR")

    def _send_error(self, request_id, code, message):
        """Send an error response for a compute request."""
        self._send({
            "type": "compute_error",
            "request_id": request_id,
            "error_code": code,
            "message": message,
        })

    def _budget_remaining(self):
        """Tokens remaining in daily budget."""
        budget = self.config["daily_token_budget"]
        if budget <= 0:
            return float("inf")

        # Reset daily counter if new day
        elapsed = time.time() - self.stats.day_start
        if elapsed > 86400:
            self.stats.reset_daily()

        return budget - self.stats.tokens_used_today


# ─── Integration with existing miner ─────────────────────────────────────────

def parse_relay_args(argv):
    """Parse --relay flags from command line args."""
    config = dict(DEFAULT_RELAY_CONFIG)

    if "--relay" in argv:
        config["enabled"] = True
    if "--relay-only" in argv:
        config["enabled"] = True
        config["relay_only"] = True

    # Parse --budget N
    for i, arg in enumerate(argv):
        if arg == "--budget" and i + 1 < len(argv):
            try:
                config["daily_token_budget"] = int(argv[i + 1])
            except ValueError:
                pass
        if arg == "--max-concurrent" and i + 1 < len(argv):
            try:
                config["max_concurrent"] = int(argv[i + 1])
            except ValueError:
                pass

    return config


def start_relay(relay_config, ai_config, wallet, sign_fn, coordinator_url, auth_token, log_fn=None):
    """
    Start the relay provider. Call this from the miner after auth.

    Args:
        relay_config: dict from parse_relay_args()
        ai_config: {"base_url": ..., "api_key": ..., "model": ..., "anthropic_mode": ...}
        wallet: miner wallet address
        sign_fn: Bankr sign function
        coordinator_url: coordinator base URL
        auth_token: current JWT token
        log_fn: logging function

    Returns:
        RelayProvider instance (or None if relay not enabled)
    """
    if not relay_config.get("enabled"):
        return None

    provider = RelayProvider(
        config=relay_config,
        ai_config=ai_config,
        wallet_address=wallet,
        sign_fn=sign_fn,
        log_fn=log_fn,
    )
    provider.start(coordinator_url, auth_token)
    return provider
